tests: Misc cleanup
MODIFY
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
@@ -17,6 +17,7 @@ export type TestRef = {
};
export type TestWorkerBridgeRunOptions = {
+ id: number;
file: JSONFileReference;
projectFolder: string;
code: string;
@@ -33,13 +34,21 @@ export default class TestWorkerBridge extends Bridge {
},
);
- runTest = this.createEvent<TestWorkerBridgeRunOptions, void>({
+ prepareTest = this.createEvent<TestWorkerBridgeRunOptions, void>({
+ name: 'prepareTest',
+ direction: 'server->client',
+ });
+
+ runTest = this.createEvent<number, void>({
name: 'runTest',
direction: 'server->client',
});
- testFound = this.createEvent<{ref: TestRef; isSkipped: boolean}, void>({
- name: 'onTestFound',
+ testsFound = this.createEvent<
+ Array<{ref: TestRef; isSkipped: boolean}>,
+ void
+ >({
+ name: 'onTestFounds',
direction: 'server<-client',
});

MODIFY
packages/@romejs/core/master/bundler/BundleRequest.ts
packages/@romejs/core/master/bundler/BundleRequest.ts
@@ -23,6 +23,7 @@ import {add} from '@romejs/ob1';
import {readFile} from '@romejs/fs';
import crypto = require('crypto');
import {Dict} from '@romejs/typescript-helpers';
+import {Reporter} from '@romejs/cli-reporter';
export type BundleOptions = {
prefix?: string;
@@ -30,12 +31,20 @@ export type BundleOptions = {
};
export default class BundleRequest {
- constructor(
- bundler: Bundler,
- mode: BundlerMode,
- resolvedEntry: AbsoluteFilePath,
- options: BundleOptions,
- ) {
+ constructor({
+ bundler,
+ reporter,
+ mode,
+ resolvedEntry,
+ options,
+ }: {
+ bundler: Bundler;
+ reporter: Reporter;
+ mode: BundlerMode;
+ resolvedEntry: AbsoluteFilePath;
+ options: BundleOptions;
+ }) {
+ this.reporter = reporter;
this.interpreter = options.interpreter;
this.bundler = bundler;
this.cached = true;
@@ -62,6 +71,7 @@ export default class BundleRequest {
interpreter: undefined | string;
cached: boolean;
+ reporter: Reporter;
bundler: Bundler;
resolvedEntry: AbsoluteFilePath;
resolvedEntryUid: string;
@@ -72,7 +82,8 @@ export default class BundleRequest {
mode: BundlerMode;
async stepAnalyze(): Promise<DependencyOrder> {
- const {reporter, graph} = this.bundler;
+ const {graph} = this.bundler;
+ const {reporter} = this;
const analyzeProgress = reporter.progress({
name: `bundler:analyze:${this.resolvedEntryUid}`,
@@ -93,7 +104,8 @@ export default class BundleRequest {
}
async stepCompile(paths: Array<AbsoluteFilePath>) {
- const {reporter, master} = this.bundler;
+ const {master} = this.bundler;
+ const {reporter} = this;
this.diagnostics.setThrowAfter(undefined);
const compilingSpinner = reporter.progress({

MODIFY
packages/@romejs/core/master/bundler/Bundler.ts
packages/@romejs/core/master/bundler/Bundler.ts
@@ -34,10 +34,10 @@ export type BundlerEntryResoluton = {
};
export default class Bundler {
- constructor(req: MasterRequest, reporter: Reporter, config: BundlerConfig) {
+ constructor(req: MasterRequest, config: BundlerConfig) {
this.config = config;
this.master = req.master;
- this.reporter = reporter;
+ this.reporter = req.reporter;
this.request = req;
this.entries = [];
@@ -53,7 +53,7 @@ export default class Bundler {
config: BundlerConfig;
static createFromMasterRequest(req: MasterRequest): Bundler {
- return new Bundler(req, req.reporter, req.getBundlerConfigFromFlags());
+ return new Bundler(req, req.getBundlerConfigFromFlags());
}
async getResolvedEntry(
@@ -95,6 +95,7 @@ export default class Bundler {
createBundleRequest(
resolvedEntry: AbsoluteFilePath,
options: BundleOptions,
+ reporter: Reporter,
): BundleRequest {
const project = this.master.projectManager.assertProjectExisting(
resolvedEntry,
@@ -102,11 +103,17 @@ export default class Bundler {
const mode: BundlerMode = project.config.bundler.mode;
this.entries.push(resolvedEntry);
- return new BundleRequest(this, mode, resolvedEntry, options);
+ return new BundleRequest({
+ bundler: this,
+ mode,
+ resolvedEntry,
+ options,
+ reporter,
+ });
}
async compile(path: AbsoluteFilePath): Promise<WorkerCompileResult> {
- const bundleRequest = this.createBundleRequest(path, {});
+ const bundleRequest = this.createBundleRequest(path, {}, this.reporter);
await bundleRequest.stepAnalyze();
bundleRequest.diagnostics.maybeThrowDiagnosticsError();
return await bundleRequest.compileJS(path);
@@ -116,6 +123,9 @@ export default class Bundler {
async bundleMultiple(
entries: Array<AbsoluteFilePath>,
): Promise<Map<AbsoluteFilePath, BundleResult>> {
+ // Clone so we can mess with it
+ entries = [...entries];
+
// Seed the dependency graph with all the entries at the same time
const processor = new DiagnosticsProcessor({
origins: [
@@ -139,16 +149,50 @@ export default class Bundler {
analyzeProgress,
validate: false,
});
+ analyzeProgress.end();
processor.maybeThrowDiagnosticsError();
// Now actually bundle them
const map: Map<AbsoluteFilePath, BundleResult> = new Map();
+ const progress = this.reporter.progress();
+ progress.setTitle('Bundling');
+ progress.setTotal(entries.length);
+
+ const silentReporter = this.reporter.fork({
+ silent: true,
+ });
+
+ const promises: Set<Promise<void>> = new Set();
+
// Could maybe do some of this in parallel?
- for (const resolvedEntry of entries) {
- map.set(resolvedEntry, await this.bundle(resolvedEntry));
+ while (entries.length > 0) {
+ const entry = entries.shift();
+ if (entry === undefined) {
+ throw new Error('Impossible. We just checked.');
}
+ const promise = (async () => {
+ const text = `<filelink target="${entry.join()}" />`;
+ progress.pushText(text);
+ map.set(entry, await this.bundle(entry, {}, silentReporter));
+ progress.popText(text);
+ progress.tick();
+ })();
+ promise.then(() => {
+ promises.delete(promise);
+ });
+ promises.add(promise);
+
+ if (promises.size > 5) {
+ await Promise.race(Array.from(promises));
+ }
+ }
+
+ await Promise.all(Array.from(promises));
+
+ progress.end();
+
return map;
}
@@ -295,14 +339,13 @@ export default class Bundler {
async bundle(
resolvedEntry: AbsoluteFilePath,
options: BundleOptions = {},
+ reporter: Reporter = this.reporter,
): Promise<BundleResult> {
- const {reporter} = this;
-
reporter.info(
`Bundling <filelink emphasis target="${resolvedEntry.join()}" />`,
);
- const req = this.createBundleRequest(resolvedEntry, options);
+ const req = this.createBundleRequest(resolvedEntry, options, reporter);
const res = await req.bundle();
const processor = new DiagnosticsProcessor({origins: []});

MODIFY
packages/@romejs/core/master/commands/test.ts
packages/@romejs/core/master/commands/test.ts
@@ -16,6 +16,7 @@ import Bundler from '../bundler/Bundler';
import {AbsoluteFilePath} from '@romejs/path';
import {JS_EXTENSIONS} from '../../common/fileHandlers';
import {TEST_FOLDER_NAME} from '@romejs/core/common/constants';
+import {TestRunnerOptions} from '../testing/types';
function isTestFile(path: AbsoluteFilePath): boolean {
const parts = path.getSegments();
@@ -42,12 +43,7 @@ function isTestFile(path: AbsoluteFilePath): boolean {
return false;
}
-type Flags = {
- coverage: boolean;
- showAllCoverage: boolean;
- updateSnapshots: boolean;
- freezeSnapshots: boolean;
-};
+type Flags = Omit<TestRunnerOptions, 'verboseDiagnostics'>;
export default createMasterCommand({
category: commandCategories.CODE_QUALITY,
@@ -59,6 +55,7 @@ export default createMasterCommand({
showAllCoverage: c.get('showAllCoverage').asBoolean(false),
updateSnapshots: c.get('updateSnapshots').asBoolean(false),
freezeSnapshots: c.get('freezeSnapshots').asBoolean(false),
+ syncTests: c.get('syncTests').asBoolean(false),
};
},
@@ -91,9 +88,7 @@ export default createMasterCommand({
return;
}
- reporter.info(
- `Bundling <number emphasis>${files.length}</number> test files`,
- );
+ reporter.info(`Bundling test files`);
let addDiagnostics: PartialDiagnostics = [];
@@ -108,9 +103,6 @@ export default createMasterCommand({
const bundler = new Bundler(
req,
- req.reporter.fork({
- silent: true,
- }),
req.getBundlerConfigFromFlags({
mocks: true,
}),
@@ -133,6 +125,7 @@ export default createMasterCommand({
showAllCoverage: commandFlags.showAllCoverage,
updateSnapshots: commandFlags.updateSnapshots,
freezeSnapshots: commandFlags.freezeSnapshots,
+ syncTests: commandFlags.syncTests,
verboseDiagnostics: req.query.requestFlags.verboseDiagnostics,
},
sources: tests,

MODIFY
packages/@romejs/core/master/testing/TestRunner.ts
packages/@romejs/core/master/testing/TestRunner.ts
* LICENSE file in the root directory of this source tree.
*/
-import {Reporter} from '@romejs/cli-reporter';
-import {DiagnosticOrigin} from '@romejs/diagnostics';
+import {Reporter, ProgressShape} from '@romejs/cli-reporter';
+import {DiagnosticOrigin, deriveDiagnosticFromError} from '@romejs/diagnostics';
import {TestRef} from '../../common/bridges/TestWorkerBridge';
import {Master, MasterRequest} from '@romejs/core';
import {DiagnosticsPrinter} from '@romejs/cli-diagnostics';
import {createClient} from '@romejs/codec-websocket';
import {humanizeNumber} from '@romejs/string-utils';
-import {createBridgeFromChildProcess} from '@romejs/events';
+import {createBridgeFromChildProcess, Bridge} from '@romejs/events';
import {
InspectorClientCloseError,
InspectorClient,
CoverageCollector,
sourceMapManager,
NativeStructuredError,
+ StructuredError,
} from '@romejs/v8';
-import {deriveDiagnosticFromError} from '@romejs/diagnostics';
import {TestWorkerBridge} from '@romejs/core';
import fork from '../../common/utils/fork';
import {ManifestDefinition} from '@romejs/codec-js-manifest';
@@ -39,8 +39,20 @@ import {
CoverageFolder,
} from './types';
import {percentInsideCoverageFolder, formatPercent, sortMapKeys} from './utils';
+import {escapeMarkup} from '@romejs/string-markup';
-class BridgeStructuredError extends NativeStructuredError {}
+class BridgeStructuredError extends NativeStructuredError {
+ constructor(struct: Partial<StructuredError>, bridge: Bridge) {
+ super(struct);
+ this.bridge = bridge;
+ }
+
+ bridge: Bridge;
+}
+
+function getProgressTestRefText(ref: TestRef) {
+ return `<filelink target="${ref.filename}" />: ${escapeMarkup(ref.testName)}`;
+}
export default class TestRunner {
constructor(opts: TestRunnerConstructorOptions) {
@@ -51,6 +63,8 @@ export default class TestRunner {
this.request = opts.request;
this.options = opts.options;
+ this.ignoreBridgeEndError = new Set();
+
this.sourcesQueue = Array.from(opts.sources.entries());
this.coverageCollector = new CoverageCollector();
@@ -62,6 +76,7 @@ export default class TestRunner {
};
this.runningTests = new Map();
+ this.testFileCounter = 0;
this.printer = opts.request.createDiagnosticsPrinter({
category: 'test',
@@ -80,6 +95,8 @@ export default class TestRunner {
cwd: AbsoluteFilePath;
printer: DiagnosticsPrinter;
sourcesQueue: Array<[string, TestSource]>;
+ testFileCounter: number;
+ ignoreBridgeEndError: Set<Bridge>;
runningTests: Map<
string,
@@ -130,8 +147,12 @@ export default class TestRunner {
sourceMap,
);
+ const id = this.testFileCounter;
+ this.testFileCounter++;
+
try {
- await bridge.runTest.call({
+ await bridge.prepareTest.call({
+ id,
options: opts,
projectFolder: req.master.projectManager
.assertProjectExisting(path)
@@ -141,6 +162,8 @@ export default class TestRunner {
code,
sourceMap,
});
+
+ await bridge.runTest.call(id);
} finally {
removeSourceMap();
}
@@ -152,9 +175,14 @@ export default class TestRunner {
await nextTest();
} catch (err) {
if (err instanceof BridgeError || err instanceof BridgeStructuredError) {
- // When a worker dies, we automatically mark all it's currently running tests as failed
- // However, it will cause all the pending messages to throw an error which will bubble up here
- // We can safely ignore these since we've already handled it
+ if (!this.ignoreBridgeEndError.has(err.bridge)) {
+ this.printer.addDiagnostic(
+ deriveDiagnosticFromError({
+ category: 'test',
+ error: err,
+ }),
+ );
+ }
} else {
throw err;
}
@@ -259,7 +287,7 @@ export default class TestRunner {
async init() {
this.workers = await this.setupWorkers();
- this.setupProgress();
+ const teardown = this.setupProgress();
const workerContainers: TestWorkerContainers = this.getWorkers();
@@ -267,6 +295,8 @@ export default class TestRunner {
workerContainers.map(container => this.runWorker(container)),
);
+ teardown();
+
this.printTestResults();
}
@@ -356,10 +386,20 @@ export default class TestRunner {
}
bridge.endWithError(
- new BridgeStructuredError({
+ new BridgeStructuredError(
+ {
message: `Test worker was unresponsive for <emphasis>${duration}</emphasis>. Possible infinite loop. Below is a stack trace before the test was terminated.`,
frames,
- }),
+ advice: [
+ {
+ type: 'log',
+ category: 'info',
+ message: `You can find the specific test that caused this by running <command>rome test --sync-tests</command>`,
+ },
+ ],
+ },
+ bridge,
+ ),
);
}
@@ -420,9 +460,14 @@ export default class TestRunner {
this.progress.finished++;
}
- setupProgress() {
+ setupProgress(): () => void {
const workers = this.getWorkers();
+ const progress = this.request.reporter.progress({
+ persistent: true,
+ });
+ progress.setTitle('Running tests');
+
for (let i = 0; i < workers.length; i++) {
const container = workers[i];
const {bridge} = container;
@@ -431,30 +476,54 @@ export default class TestRunner {
bridge.endEvent.subscribe(error => {
// Cancel all currently running tests
+
+ const cancelTests: Array<TestRef> = [];
+
for (const key of ourRunningTests) {
const test = this.runningTests.get(key);
- if (test === undefined) {
- // Test has already finished
- continue;
+ if (test !== undefined) {
+ cancelTests.push(test.ref);
+ }
}
- const {ref} = test;
+ for (const ref of cancelTests) {
this.onTestFinished(ref);
+
+ if (cancelTests.length === 1) {
+ // If we only have one test to cancel then let's only point the bridge error to this test
+ this.ignoreBridgeEndError.add(bridge);
+
+ this.printer.addDiagnostic({
+ ...deriveDiagnosticFromError({
+ category: ref.testName,
+ filename: ref.filename,
+ error: error,
+ }),
+
+ // We don't care about the advice
+ advice: [],
+ });
+ } else {
this.printer.addDiagnostic({
category: ref.testName,
filename: ref.filename,
message: 'Test was cancelled',
});
}
+ }
});
- bridge.testFound.subscribe(data => {
- this.onTestFound(data.ref, data.isSkipped);
+ bridge.testsFound.subscribe(tests => {
+ for (const {ref, isSkipped} of tests) {
+ this.onTestFound(ref, isSkipped);
+ }
+ progress.setTotal(this.progress.total);
});
bridge.testStart.subscribe(data => {
ourRunningTests.add(this.refToKey(data.ref));
this.onTestStart(container, data.ref, data.timeout);
+ progress.pushText(getProgressTestRefText(data.ref));
});
bridge.testError.subscribe(data => {
@@ -471,6 +540,8 @@ export default class TestRunner {
);
origin.message = `Generated from the file <filelink target="${uid}" /> and test name "${ref.testName}"`;
this.onTestFinished(ref);
+ progress.popText(getProgressTestRefText(ref));
+ progress.tick();
}
this.printer.addDiagnostic(data.diagnostic, origin);
@@ -478,8 +549,14 @@ export default class TestRunner {
bridge.testSuccess.subscribe(data => {
this.onTestFinished(data.ref);
+ progress.popText(getProgressTestRefText(data.ref));
+ progress.tick();
});
}
+
+ return function() {
+ progress.end();
+ };
}
printCoverageReport() {

MODIFY
packages/@romejs/core/master/testing/types.ts
packages/@romejs/core/master/testing/types.ts
@@ -35,6 +35,7 @@ export type TestRunnerOptions = {
updateSnapshots: boolean;
freezeSnapshots: boolean;
verboseDiagnostics: boolean;
+ syncTests: boolean;
};
export type CoverageFolder = {

MODIFY
packages/@romejs/core/master/web/WebRequest.ts
packages/@romejs/core/master/web/WebRequest.ts
@@ -200,17 +200,13 @@ export default class WebRequest {
const {res} = this;
res.writeHead(200, {'Content-Type': 'application/javascript'});
- const bundler = new Bundler(
- this.masterRequest,
- this.masterRequest.reporter,
- {
+ const bundler = new Bundler(this.masterRequest, {
inlineSourceMap: false,
cwd: this.masterRequest.client.flags.cwd,
resolver: {
platform: 'web',
},
- },
- );
+ });
const resolved = await this.master.resolver.resolveEntryAssertPath({
origin: this.masterRequest.client.flags.cwd,
source: createUnknownFilePath('@romejs-web/frontend'),

MODIFY
packages/@romejs/core/master/web/index.ts
packages/@romejs/core/master/web/index.ts
@@ -284,11 +284,7 @@ export class WebServer {
},
);
- const bundler = new Bundler(
- this.masterRequest,
- this.masterRequest.reporter,
- bundlerConfig,
- );
+ const bundler = new Bundler(this.masterRequest, bundlerConfig);
bundler.graph.watch(async () => {
// TODO HMR

MODIFY
packages/@romejs/core/test-worker/SnapshotManager.ts
packages/@romejs/core/test-worker/SnapshotManager.ts
@@ -163,7 +163,10 @@ export default class SnapshotManager {
async save() {
const {folder, path} = this;
- // TODO `only` will mess this up
+ // If there'a s focused test then we don't write or validate a snapshot
+ if (this.runner.hasFocusedTest) {
+ return;
+ }
// No point producing an empty snapshot file
if (this.entries.size === 0) {

MODIFY
packages/@romejs/core/test-worker/SnapshotParser.ts
packages/@romejs/core/test-worker/SnapshotParser.ts
@@ -154,7 +154,7 @@ export default createParser(
parse(): Array<Node> {
const nodes: Array<Node> = [];
- while (!this.isEOF()) {
+ while (!this.matchToken('EOF')) {
const start = this.getPosition();
const token = this.getToken();

MODIFY
packages/@romejs/core/test-worker/TestAPI.ts
packages/@romejs/core/test-worker/TestAPI.ts
@@ -17,6 +17,8 @@ import diff from '@romejs/string-diff';
import {createErrorFromStructure} from '@romejs/v8';
import prettyFormat from '@romejs/pretty-format';
import {Class} from '@romejs/typescript-helpers';
+import {FileReference} from '../common/types/files';
+import {markup} from '@romejs/string-markup';
type AsyncFunc = () => undefined | Promise<void>;
type SyncThrower = () => void;
@@ -75,16 +77,24 @@ function maybeTruncate(str: string, noTruncate: boolean): string {
}
export default class TestAPI {
- constructor(
- testName: string,
- onTimeout: OnTimeout,
- snapshotManager: SnapshotManager,
- options: TestRunnerOptions,
- ) {
+ constructor({
+ testName,
+ onTimeout,
+ file,
+ snapshotManager,
+ options,
+ }: {
+ file: FileReference;
+ testName: string;
+ onTimeout: OnTimeout;
+ snapshotManager: SnapshotManager;
+ options: TestRunnerOptions;
+ }) {
this.testName = testName;
this.options = options;
this.snapshotManager = snapshotManager;
this.snapshotCounter = 0;
+ this.file = file;
this.teardownEvent = new Event({name: 'TestAPI.teardown'});
@@ -99,6 +109,7 @@ export default class TestAPI {
startTime: number;
options: TestRunnerOptions;
+ file: FileReference;
onTimeout: OnTimeout;
timeoutId: undefined | NodeJS.Timeout;
@@ -111,10 +122,18 @@ export default class TestAPI {
snapshotCounter: number;
snapshotManager: SnapshotManager;
- buildMatchParts(
+ buildMatchAdvice(
received: unknown,
expected: unknown,
- visualMethod?: string,
+ {
+ visualMethod,
+ expectedAlias,
+ receivedAlias,
+ }: {
+ visualMethod?: string;
+ expectedAlias?: string;
+ receivedAlias?: string;
+ } = {},
): PartialDiagnosticAdvice {
let expectedFormat;
let receivedFormat;
@@ -229,7 +248,24 @@ export default class TestAPI {
advice.push({
type: 'diff',
- diff: diff(receivedFormat, expectedFormat),
+ diff: diff(expectedFormat, receivedFormat),
+ });
+
+ advice.push({
+ type: 'log',
+ category: 'none',
+ message: receivedAlias
+ ? `<green>+ ${receivedAlias}</green>`
+ : '<green>+ What we received</green>',
+ compact: true,
+ });
+
+ advice.push({
+ type: 'log',
+ category: 'none',
+ message: expectedAlias
+ ? `<red>- ${expectedAlias}</red>`
+ : '<red>- What we expected</red>',
});
}
@@ -305,18 +341,15 @@ export default class TestAPI {
}
fail(
- message?: string,
- advice?: PartialDiagnosticAdvice,
+ message: string = 'Test failure triggered by t.fail()',
+ advice: PartialDiagnosticAdvice = [],
framesToPop: number = 0,
) {
- const actualMessage =
- message === undefined ? 'Test failure triggered by t.fail()' : message;
- const error = createErrorFromStructure({
- message: actualMessage,
+ throw createErrorFromStructure({
+ message,
advice,
framesToPop: framesToPop + 1,
});
- throw error;
}
truthy(value: unknown, message: string = 'Expected value to be truthy') {
@@ -407,7 +440,7 @@ export default class TestAPI {
if (Object.is(received, expected) !== true) {
this.fail(
message,
- this.buildMatchParts(received, expected, 'looksLike'),
+ this.buildMatchAdvice(received, expected, {visualMethod: 'looksLike'}),
1,
);
}
@@ -421,7 +454,9 @@ export default class TestAPI {
if (Object.is(received, expected) === true) {
this.fail(
message,
- this.buildMatchParts(received, expected, 'notLooksLike'),
+ this.buildMatchAdvice(received, expected, {
+ visualMethod: 'notLooksLike',
+ }),
1,
);
}
@@ -436,7 +471,7 @@ export default class TestAPI {
const expectedInspect = prettyFormat(expected);
if (actualInspect !== expectedInspect) {
- this.fail(message, this.buildMatchParts(received, expected), 1);
+ this.fail(message, this.buildMatchAdvice(received, expected), 1);
}
}
@@ -449,7 +484,7 @@ export default class TestAPI {
const expectedInspect = prettyFormat(expected);
if (actualInspect === expectedInspect) {
- this.fail(message, this.buildMatchParts(received, expected), 1);
+ this.fail(message, this.buildMatchAdvice(received, expected), 1);
}
}
@@ -523,7 +558,7 @@ export default class TestAPI {
_snapshotNamed(
name: string,
expected: unknown,
- message: string = `Snapshot ${name} doesn't match`,
+ message?: string,
framesToPop?: number,
) {
let language: undefined | string;
@@ -551,21 +586,32 @@ export default class TestAPI {
// Compare the snapshots
if (formatted !== existingSnapshot) {
- this.fail(
- message,
- [
+ const advice: PartialDiagnosticAdvice = this.buildMatchAdvice(
+ formatted,
+ existingSnapshot,
{
- type: 'log',
- category: 'info',
- message: 'Snapshot diff',
- },
- {
- type: 'diff',
- diff: diff(existingSnapshot, formatted),
+ receivedAlias: 'What the code gave us',
+ expectedAlias: 'Existing snapshot',
},
- ],
- framesToPop,
);
+
+ if (message === undefined) {
+ message = markup`Snapshot ${name} at <filelink emphasis target="${this.snapshotManager.path.join()}" /> doesn't match`;
+ } else {
+ advice.push({
+ type: 'log',
+ category: 'info',
+ message: `Snapshot can be found at <filelink emphasis target="${this.snapshotManager.path.join()}" />`,
+ });
+ }
+
+ advice.push({
+ type: 'log',
+ category: 'info',
+ message: markup`Run <command>rome test ${this.file.uid} --update-snapshots</command> to update this snapshot`,
+ });
+
+ this.fail(message, advice, framesToPop);
}
}
}

MODIFY
packages/@romejs/core/test-worker/TestWorker.ts
packages/@romejs/core/test-worker/TestWorker.ts
@@ -15,8 +15,10 @@ import inspector = require('inspector');
export default class TestWorker {
constructor() {
this.bridge = this.buildBridge();
+ this.runners = new Map();
}
+ runners: Map<number, TestWorkerRunner>;
bridge: TestWorkerBridge;
async init() {
@@ -47,15 +49,29 @@ export default class TestWorker {
};
});
- bridge.runTest.subscribe((data: TestWorkerBridgeRunOptions) => {
- return this.runTest(data);
+ bridge.prepareTest.subscribe(data => {
+ return this.prepareTest(data);
+ });
+
+ bridge.runTest.subscribe((id: number) => {
+ return this.runTest(id);
});
return bridge;
}
- async runTest(opts: TestWorkerBridgeRunOptions): Promise<void> {
- const testRun = new TestWorkerRunner(opts, this.bridge);
- await testRun.run();
+ async runTest(id: number): Promise<void> {
+ const runner = this.runners.get(id);
+ if (runner === undefined) {
+ throw new Error(`No runner ${id} found`);
+ } else {
+ await runner.run();
+ }
+ }
+
+ async prepareTest(opts: TestWorkerBridgeRunOptions): Promise<void> {
+ const runner = new TestWorkerRunner(opts, this.bridge);
+ await runner.prepare();
+ this.runners.set(opts.id, runner);
}
}

MODIFY
packages/@romejs/core/test-worker/TestWorkerRunner.ts
packages/@romejs/core/test-worker/TestWorkerRunner.ts
@@ -264,12 +264,13 @@ export default class TestWorkerRunner {
};
});
- const api = new TestAPI(
+ const api = new TestAPI({
+ file: this.file,
testName,
onTimeout,
- this.snapshotManager,
- this.options,
- );
+ snapshotManager: this.snapshotManager,
+ options: this.options,
+ });
try {
const res = callback(api);
@@ -296,11 +297,23 @@ export default class TestWorkerRunner {
}
}
- async runTests(): Promise<void> {
+ async run(): Promise<void> {
const promises: Set<Promise<void>> = new Set();
+ const {foundTests} = this;
+ if (foundTests.size === 0) {
+ this.bridge.testError.send({
+ ref: undefined,
+ diagnostic: {
+ filename: this.file.uid,
+ message: 'No tests declared in this file',
+ category: 'tests',
+ },
+ });
+ }
+
// Execute all the tests
- for (const [testName, {options, callback}] of this.foundTests) {
+ for (const [testName, {options, callback}] of foundTests) {
if (callback === undefined) {
continue;
}
@@ -314,6 +327,10 @@ export default class TestWorkerRunner {
});
const promise = this.runTest(testName, callback);
+
+ if (this.options.syncTests) {
+ await promise;
+ } else {
promise.then(() => {
promises.delete(promise);
});
@@ -322,10 +339,7 @@ export default class TestWorkerRunner {
// if there's 5 promises, then wait for one of them to finish
if (promises.size > MAX_RUNNING_TESTS) {
await Promise.race(Array.from(promises));
-
- await new Promise(resolve => {
- setTimeout(resolve, 0);
- });
+ }
}
}
@@ -337,7 +351,7 @@ export default class TestWorkerRunner {
}
async emitFoundTests() {
- const promises: Array<Promise<void>> = [];
+ const tests = [];
for (const [testName, {callback, options}] of this.foundTests) {
let isSkipped = callback === undefined;
@@ -345,30 +359,21 @@ export default class TestWorkerRunner {
isSkipped = true;
}
- promises.push(
- this.bridge.testFound.call({
+ tests.push({
ref: {
filename: this.file.real.join(),
testName,
},
isSkipped,
- }),
- );
+ });
}
- await Promise.all(promises);
+ await this.bridge.testsFound.call(tests);
}
- async run(): Promise<void> {
+ async wrap(callback: () => Promise<void>): Promise<void> {
try {
- // Setup
- await this.snapshotManager.load();
- await this.discoverTests();
- await this.emitFoundTests();
-
- // Execute
- this.lockTests();
- await this.runTests();
+ await callback();
} catch (err) {
const diagnostics = getDiagnosticsFromError(err);
if (diagnostics === undefined) {
@@ -394,4 +399,16 @@ export default class TestWorkerRunner {
}
}
}
+
+ async prepare(): Promise<void> {
+ return this.wrap(async () => {
+ // Setup
+ await this.snapshotManager.load();
+ await this.discoverTests();
+ await this.emitFoundTests();
+
+ // Execute
+ this.lockTests();
+ });
+ }
}

tests: Misc cleanup