Skip to content

Commit

Permalink
feat(betterer ✨): run tests in parallel (#815)
Browse files Browse the repository at this point in the history
  • Loading branch information
phenomnomnominal authored Aug 17, 2021
1 parent f691bfa commit 581cf51
Show file tree
Hide file tree
Showing 76 changed files with 2,011 additions and 1,270 deletions.
19 changes: 12 additions & 7 deletions goldens/api/@betterer/betterer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export declare type BettererConfig = {
resultsPath: string;
silent: boolean;
tsconfigPath: string | null;
workers: number;
ci: boolean;
precommit: boolean;
strict: boolean;
Expand Down Expand Up @@ -92,7 +93,9 @@ export declare type BettererFileIssue = {

export declare type BettererFileIssues = ReadonlyArray<BettererFileIssue>;

export declare type BettererFilePaths = ReadonlyArray<string>;
export declare type BettererFilePath = string;

export declare type BettererFilePaths = ReadonlyArray<BettererFilePath>;

export declare type BettererFilePatterns = ReadonlyArray<RegExp | ReadonlyArray<RegExp>>;

Expand Down Expand Up @@ -138,6 +141,7 @@ export declare type BettererOptionsBase = Partial<{
resultsPath: string;
silent: boolean;
tsconfigPath: string;
workers: number;
}>;

export declare type BettererOptionsExcludes = Array<string | RegExp> | string;
Expand Down Expand Up @@ -228,6 +232,8 @@ export declare type BettererResult = {
};

export declare type BettererRun = {
readonly baseline: BettererResult | null;
readonly expected: BettererResult | null;
readonly filePaths: BettererFilePaths | null;
readonly name: string;
readonly isNew: boolean;
Expand All @@ -249,12 +255,11 @@ export declare type BettererRuns = ReadonlyArray<BettererRun>;
export declare type BettererRunSummaries = Array<BettererRunSummary>;

export declare type BettererRunSummary = BettererRun & {
readonly diff: BettererDiff;
readonly diff: BettererDiff | null;
readonly delta: BettererDelta | null;
readonly error: BettererError;
readonly expected: BettererResult;
readonly error: Error | null;
readonly printed: string | null;
readonly result: BettererResult;
readonly result: BettererResult | null;
readonly timestamp: number;
readonly isBetter: boolean;
readonly isComplete: boolean;
Expand All @@ -277,7 +282,7 @@ export declare type BettererSuite = {
readonly runs: BettererRuns;
};

export declare type BettererSuiteSummaries = Array<BettererSuiteSummary>;
export declare type BettererSuiteSummaries = ReadonlyArray<BettererSuiteSummary>;

export declare type BettererSuiteSummary = BettererSuite & {
readonly runs: BettererRunSummaries;
Expand All @@ -296,7 +301,7 @@ export declare type BettererSuiteSummary = BettererSuite & {
readonly worse: BettererRunSummaries;
};

export declare class BettererTest<DeserialisedType, SerialisedType, DiffType> implements BettererTestBase<DeserialisedType, SerialisedType, DiffType> {
export declare class BettererTest<DeserialisedType, SerialisedType = DeserialisedType, DiffType = null> implements BettererTestBase<DeserialisedType, SerialisedType, DiffType> {
readonly config: BettererTestConfig<DeserialisedType, SerialisedType, DiffType>;
get isOnly(): boolean;
get isSkipped(): boolean;
Expand Down
1 change: 1 addition & 0 deletions goldens/api/@betterer/cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export declare type BettererCLIConfig = BettererCLIEnvConfig & {
strict: boolean;
tsconfig: string;
update: boolean;
workers: number;
};

export declare type BettererCLIEnvConfig = {
Expand Down
12 changes: 4 additions & 8 deletions packages/betterer/src/betterer.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { debug } from '@phenomnomnominal/debug';

import { BettererOptionsRunner, BettererOptionsStart, BettererOptionsWatch } from './config';
import { createGlobals } from './globals';
import { BettererRunner, BettererRunnerΩ, BettererWatcherΩ } from './runner';
import { BettererSuiteSummary } from './suite';

export async function betterer(options: BettererOptionsStart = {}): Promise<BettererSuiteSummary> {
initDebug();
const globals = await createGlobals(options);
const runner = new BettererRunnerΩ(globals);
return runner.run(globals.config.filePaths);
const runner = await BettererRunnerΩ.create(options);
return runner.run(runner.config.filePaths);
}

export async function runner(options: BettererOptionsRunner = {}): Promise<BettererRunner> {
initDebug();
return new BettererRunnerΩ(await createGlobals(options));
return BettererRunnerΩ.create(options);
}
betterer.runner = runner;

export async function watch(options: BettererOptionsWatch = {}): Promise<BettererRunner> {
initDebug();
const watcher = new BettererWatcherΩ(await createGlobals({ ...options, watch: true }));
await watcher.setup();
return watcher;
return BettererWatcherΩ.create({ ...options, watch: true });
}
betterer.watch = watch;

Expand Down
28 changes: 27 additions & 1 deletion packages/betterer/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BettererError } from '@betterer/errors';
import assert from 'assert';
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';

import { BettererFileResolverΩ, BettererVersionControlWorker } from '../fs';
import { isBoolean, isRegExp, isString, isUndefined } from '../utils';
import { isBoolean, isNumber, isRegExp, isString, isUndefined } from '../utils';
import {
BettererConfig,
BettererConfigReporter,
Expand All @@ -14,6 +15,8 @@ import {
BettererOptionsWatch
} from './types';

const TOTAL_CPUS = os.cpus().length;

export async function createConfig(
options: unknown = {},
versionControl: BettererVersionControlWorker
Expand All @@ -36,6 +39,7 @@ export async function createConfig(
resultsPath: baseOptions.resultsPath || './.betterer.results',
silent: isDebug || baseOptions.silent || false,
tsconfigPath: baseOptions.tsconfigPath || null,
workers: baseOptions.workers || Math.max(TOTAL_CPUS - 2, 1),

// Runner:
ignores: toArray<string>(runnerOptions.ignores),
Expand Down Expand Up @@ -85,6 +89,7 @@ function validateConfig(config: BettererConfig): void {
validateStringRegExpArray('filters', config);
validateString('resultsPath', config);
validateBool('silent', config);
validateWorkers('workers', config);

// Start:
validateBool('ci', config);
Expand All @@ -100,6 +105,11 @@ function validateConfig(config: BettererConfig): void {
}

function overrideConfig(config: BettererConfig) {
// Silent mode:
if (config.silent) {
config.reporters = [];
}

// CI mode:
if (config.ci) {
config.precommit = false;
Expand Down Expand Up @@ -147,6 +157,11 @@ function validateBool<Config, PropertyName extends keyof Config>(propertyName: P
validate(isBoolean(value), `"${propertyName.toString()}" must be \`true\` or \`false\`. ${recieved(value)}`);
}

function validateNumber<Config, PropertyName extends keyof Config>(propertyName: PropertyName, config: Config): void {
const value = config[propertyName];
validate(isNumber(value), `"${propertyName.toString()}" must be a number. ${recieved(value)}`);
}

function validateString<Config, PropertyName extends keyof Config>(propertyName: PropertyName, config: Config): void {
const value = config[propertyName];
validate(isString(value), `"${propertyName.toString()}" must be a string. ${recieved(value)}`);
Expand Down Expand Up @@ -198,6 +213,17 @@ async function validateFilePath<Config, PropertyName extends keyof Config>(
);
}

function validateWorkers<Config, PropertyName extends keyof Config>(propertyName: PropertyName, config: Config): void {
const value = config[propertyName];
validateNumber(propertyName, config);
validate(
isNumber(value) && value > 0 && value <= TOTAL_CPUS,
`"${propertyName.toString()}" must be more than zero and not more than the number of available CPUs (${TOTAL_CPUS}). ${recieved(
value
)}`
);
}

function validate(value: unknown, message: string): asserts value is boolean {
// Wrap the AssertionError in a BettererError for logging:
try {
Expand Down
2 changes: 2 additions & 0 deletions packages/betterer/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type BettererConfig = {
resultsPath: string;
silent: boolean;
tsconfigPath: string | null;
workers: number;
// Start:
ci: boolean;
precommit: boolean;
Expand Down Expand Up @@ -45,6 +46,7 @@ export type BettererOptionsBase = Partial<{
resultsPath: string;
silent: boolean;
tsconfigPath: string;
workers: number;
}>;

export type BettererOptionsStartBase = BettererOptionsBase &
Expand Down
11 changes: 3 additions & 8 deletions packages/betterer/src/context/context-summary.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { BettererConfig } from '../config';
import { BettererSuiteSummaries, BettererSuiteSummary } from '../suite';
import { BettererSuiteSummariesΩ, BettererSuiteSummaryΩ } from '../suite';
import { BettererContextSummary } from './types';
import { BettererGlobals } from '../types';

export class BettererContextSummaryΩ implements BettererContextSummary {
public readonly config: BettererConfig;
constructor(public readonly config: BettererConfig, public readonly suites: BettererSuiteSummariesΩ) {}

constructor(private _globals: BettererGlobals, public readonly suites: BettererSuiteSummaries) {
this.config = this._globals.config;
}

public get lastSuite(): BettererSuiteSummary {
public get lastSuite(): BettererSuiteSummaryΩ {
return this.suites[this.suites.length - 1];
}
}
36 changes: 25 additions & 11 deletions packages/betterer/src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ import { BettererConfig } from '../config';
import { BettererFilePaths, BettererVersionControlWorker } from '../fs';
import { BettererReporterΩ } from '../reporters';
import { BettererResultsΩ } from '../results';
import { BettererRunWorkerPoolΩ, BettererRunΩ, createWorkerConfig } from '../run';
import { BettererSuiteΩ, BettererSuiteSummariesΩ, BettererSuiteSummaryΩ } from '../suite';
import { loadTestMeta } from '../test';
import { defer } from '../utils';
import { BettererSuiteSummaryΩ, BettererSuiteSummary, BettererSuiteSummaries, BettererSuiteΩ } from '../suite';
import { BettererContext, BettererContextStarted, BettererContextSummary } from './types';
import { BettererGlobals } from '../types';
import { BettererContextSummaryΩ } from './context-summary';
import { BettererContext, BettererContextStarted, BettererContextSummary } from './types';

export class BettererContextΩ implements BettererContext, BettererGlobals {
export class BettererContextΩ implements BettererContext {
public readonly config: BettererConfig;
public readonly reporter: BettererReporterΩ;
public readonly results: BettererResultsΩ;
public readonly versionControl: BettererVersionControlWorker;

private _suiteSummaries: BettererSuiteSummaries = [];
private _suiteSummaries: BettererSuiteSummariesΩ = [];

constructor(private _globals: BettererGlobals) {
constructor(private _globals: BettererGlobals, private _runWorkerPool: BettererRunWorkerPoolΩ) {
this.config = this._globals.config;
this.reporter = this._globals.reporter;
this.results = this._globals.results;
Expand All @@ -27,19 +29,20 @@ export class BettererContextΩ implements BettererContext, BettererGlobals {

public start(): BettererContextStarted {
const contextLifecycle = defer<BettererContextSummary>();

// Don't await here! A custom reporter could be awaiting
// the lifecycle promise which is unresolved right now!
const reportContextStart = this.reporter.contextStart(this, contextLifecycle.promise);
return {
end: async (): Promise<BettererContextSummary> => {
const contextSummary = new BettererContextSummaryΩ(this._globals, this._suiteSummaries);
const contextSummary = new BettererContextSummaryΩ(this.config, this._suiteSummaries);
contextLifecycle.resolve(contextSummary);
await reportContextStart;
await this.reporter.contextEnd(contextSummary);

await this.versionControl.writeCache();

const suiteSummaryΩ = contextSummary.lastSuite as BettererSuiteSummaryΩ;
const suiteSummaryΩ = contextSummary.lastSuite;
if (suiteSummaryΩ.shouldWrite) {
await this.results.write(suiteSummaryΩ.result);
if (this.config.precommit) {
Expand All @@ -56,15 +59,26 @@ export class BettererContextΩ implements BettererContext, BettererGlobals {
};
}

public async run(filePaths: BettererFilePaths): Promise<BettererSuiteSummary> {
public async run(filePaths: BettererFilePaths): Promise<BettererSuiteSummaryΩ> {
await this.results.sync();
await this.versionControl.sync();

const validFilePaths = await this.versionControl.filterIgnored(filePaths);
filePaths = await this.versionControl.filterIgnored(filePaths);

const testMeta = loadTestMeta(this.config);
const testNames = Object.keys(testMeta);

const workerConfig = createWorkerConfig(this.config);

const runs = await Promise.all(
testNames.map(async (testName) => {
return BettererRunΩ.create(this._runWorkerPool, testName, workerConfig, filePaths, this.versionControl);
})
);

const suite = new BettererSuiteΩ(this, validFilePaths);
const suite = new BettererSuiteΩ(this, filePaths, runs);
const suiteSummary = await suite.run();
this._suiteSummaries.push(suiteSummary);
this._suiteSummaries = [...this._suiteSummaries, suiteSummary];
return suiteSummary;
}
}
1 change: 0 additions & 1 deletion packages/betterer/src/fs/file-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export class BettererFileResolverΩ implements BettererFileResolver {
private _excluded: Array<RegExp> = [];
private _included: Array<string> = [];
private _includedResolved: Array<string> | null = null;

private _validatedFilePaths: Array<string> = [];
private _validatedFilePathsMap: Record<string, boolean> = {};

Expand Down
8 changes: 7 additions & 1 deletion packages/betterer/src/fs/public.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { BettererFileGlobs, BettererFilePaths, BettererFilePatterns, BettererFileResolver } from './types';
export {
BettererFileGlobs,
BettererFilePath,
BettererFilePaths,
BettererFilePatterns,
BettererFileResolver
} from './types';
4 changes: 2 additions & 2 deletions packages/betterer/src/fs/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { WorkerRequireModule, WorkerRequireModuleAsync } from '@phenomnomnominal/worker-require';

export type BettererFileGlobs = ReadonlyArray<string | ReadonlyArray<string>>;
export type BettererFilePaths = ReadonlyArray<string>;
export type BettererFilePath = string;
export type BettererFilePaths = ReadonlyArray<BettererFilePath>;
export type BettererFilePatterns = ReadonlyArray<RegExp | ReadonlyArray<RegExp>>;

export type BettererFileCacheMap = Record<string, string>;
Expand All @@ -22,7 +23,6 @@ export type BettererVersionControl = BettererFileCache & {
};

export type BettererVersionControlWorkerModule = WorkerRequireModule<typeof import('./version-control-worker')>;

export type BettererVersionControlWorker =
WorkerRequireModuleAsync<BettererVersionControlWorkerModule>['versionControl'];

Expand Down
21 changes: 20 additions & 1 deletion packages/betterer/src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createConfig } from './config';
import { createVersionControl } from './fs';
import { BettererVersionControlWorker, createVersionControl } from './fs';
import { registerExtensions } from './register';
import { DEFAULT_REPORTER, loadReporters } from './reporters';
import { BettererResultsΩ } from './results';
Expand Down Expand Up @@ -28,3 +28,22 @@ export async function createGlobals(options: unknown = {}): Promise<BettererGlob
throw error;
}
}

export async function createWorkerGlobals(
options: unknown = {},
versionControl: BettererVersionControlWorker
): Promise<BettererGlobals> {
const config = await createConfig(options, versionControl);
const { cache } = config;

if (cache) {
await versionControl.enableCache(config.cachePath);
}

await registerExtensions(config);

const reporter = loadReporters([]);
const results = new BettererResultsΩ(config);

return { config, reporter, results, versionControl };
}
8 changes: 7 additions & 1 deletion packages/betterer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export {
BettererOptionsWatch
} from './config/public';
export { BettererContext, BettererContextSummary, BettererDelta } from './context/public';
export { BettererFileGlobs, BettererFilePaths, BettererFilePatterns, BettererFileResolver } from './fs/public';
export {
BettererFileGlobs,
BettererFilePath,
BettererFilePaths,
BettererFilePatterns,
BettererFileResolver
} from './fs/public';
export { BettererResult } from './results/public';
export { BettererReporter } from './reporters/public';
export { BettererRun, BettererRunNames, BettererRunSummary, BettererRunSummaries, BettererRuns } from './run/public';
Expand Down
1 change: 0 additions & 1 deletion packages/betterer/src/results/result.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import assert from 'assert';

import { BettererResult } from './types';

const NO_PREVIOUS_RESULT = Symbol('No Previous Result');
Expand Down
Loading

0 comments on commit 581cf51

Please sign in to comment.