From 63f23fa509240358a9b38a1d2b053cb4f4e3c4bf Mon Sep 17 00:00:00 2001 From: Craig Spence Date: Wed, 26 Aug 2020 04:20:08 +1200 Subject: [PATCH] =?UTF-8?q?fix(betterer=20=F0=9F=90=9B):=20tidy=20up=20res?= =?UTF-8?q?ult=20and=20diff=20APIs=20(#248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * simplify BettererRun object * better types for BettererDiffer BREAKING CHANGE: Changes to the public API --- .betterer.results | 4 +- goldens/api/@betterer/betterer.d.ts | 54 ++++-- packages/betterer/src/context/context.ts | 65 ++++--- packages/betterer/src/context/run.ts | 75 ++----- packages/betterer/src/context/types.ts | 10 +- packages/betterer/src/index.ts | 2 + packages/betterer/src/results/deserialiser.ts | 12 -- packages/betterer/src/results/diff.ts | 24 --- packages/betterer/src/results/differ.ts | 25 +++ packages/betterer/src/results/index.ts | 10 +- packages/betterer/src/results/printer.ts | 11 +- packages/betterer/src/results/public.ts | 1 + packages/betterer/src/results/result.ts | 23 +++ packages/betterer/src/results/results.ts | 57 ++++++ packages/betterer/src/results/serialiser.ts | 11 -- packages/betterer/src/results/types.ts | 14 +- packages/betterer/src/runner/runner.ts | 9 +- .../betterer/src/test/file-test/constraint.ts | 162 +--------------- .../betterer/src/test/file-test/differ.ts | 183 ++++++++++++++++-- .../betterer/src/test/file-test/file-test.ts | 45 ++--- packages/betterer/src/test/file-test/file.ts | 2 +- packages/betterer/src/test/file-test/files.ts | 3 +- packages/betterer/src/test/file-test/index.ts | 5 +- .../betterer/src/test/file-test/printer.ts | 3 +- .../betterer/src/test/file-test/public.ts | 2 +- .../betterer/src/test/file-test/serialiser.ts | 5 +- packages/betterer/src/test/file-test/types.ts | 14 +- packages/betterer/src/test/index.ts | 15 +- packages/betterer/src/test/public.ts | 3 +- packages/betterer/src/test/test.ts | 20 +- packages/betterer/src/test/types.ts | 11 +- packages/extension/src/server/validator.ts | 16 +- packages/reporter/src/reporter.ts | 4 +- 33 files changed, 475 insertions(+), 425 deletions(-) delete mode 100644 packages/betterer/src/results/deserialiser.ts delete mode 100644 packages/betterer/src/results/diff.ts create mode 100644 packages/betterer/src/results/differ.ts create mode 100644 packages/betterer/src/results/public.ts create mode 100644 packages/betterer/src/results/result.ts create mode 100644 packages/betterer/src/results/results.ts delete mode 100644 packages/betterer/src/results/serialiser.ts diff --git a/.betterer.results b/.betterer.results index 3d6e5297e..323a20810 100644 --- a/.betterer.results +++ b/.betterer.results @@ -57,9 +57,9 @@ exports[`new eslint rules`] = { [62, 4, 68, "Promises must be handled appropriately or explicitly marked as ignored with the \`void\` operator.", "280309533"], [63, 4, 71, "Promises must be handled appropriately or explicitly marked as ignored with the \`void\` operator.", "2936064845"] ], - "packages/extension/src/server/validator.ts:1579883463": [ + "packages/extension/src/server/validator.ts:3834209490": [ [48, 10, 90, "Promises must be handled appropriately or explicitly marked as ignored with the \`void\` operator.", "1821734533"], - [96, 12, 94, "Promises must be handled appropriately or explicitly marked as ignored with the \`void\` operator.", "2449957056"] + [94, 12, 94, "Promises must be handled appropriately or explicitly marked as ignored with the \`void\` operator.", "2449957056"] ] }` }; diff --git a/goldens/api/@betterer/betterer.d.ts b/goldens/api/@betterer/betterer.d.ts index 73450843f..add94ca62 100644 --- a/goldens/api/@betterer/betterer.d.ts +++ b/goldens/api/@betterer/betterer.d.ts @@ -40,7 +40,14 @@ export declare type BettererContext = { export declare type BettererDeserialise = (serialised: SerialisedType) => DeserialisedType; -export declare type BettererDiffer = (run: BettererRun) => void; +export declare type BettererDiff = { + expected: DeserialisedType; + result: DeserialisedType; + diff: DiffType; + log: () => void; +}; + +export declare type BettererDiffer = (expected: DeserialisedType, result: DeserialisedType) => DiffType; export declare type BettererFileGlobs = ReadonlyArray>; @@ -88,23 +95,24 @@ export declare type BettererFiles = { getFileΔ(absolutePath: string): BettererFile | void; }; -export declare class BettererFileTest extends BettererTest { - get diff(): BettererFileTestDiff | null; +export declare type BettererFilesDiff = Record; + existing?: ReadonlyArray; + neww?: ReadonlyArray; +}>; + +export declare class BettererFileTest extends BettererTest { isBettererFileTest: string; constructor(_resolver: BettererFileResolver, fileTest: BettererFileTestFunction); exclude(...excludePatterns: BettererFilePatterns): this; include(...includePatterns: BettererFileGlobs): this; } -export declare type BettererFileTestDiff = Record; - existing?: ReadonlyArray; - neww?: ReadonlyArray; -}>; +export declare type BettererFileTestDiff = BettererDiff; export declare type BettererFileTestFunction = (files: BettererFilePaths) => MaybeAsync; -export declare type BettererPrinter = (run: BettererRun, serialised: SerialisedType) => MaybeAsync; +export declare type BettererPrinter = (serialised: SerialisedType) => MaybeAsync; export declare type BettererReporter = { contextStart?(context: BettererContext): void; @@ -118,12 +126,17 @@ export declare type BettererReporter = { export declare type BettererReporterNames = ReadonlyArray; +export declare type BettererResult = { + isNew: boolean; + value: unknown; +}; + export declare type BettererRun = { - readonly expected: unknown | typeof NO_PREVIOUS_RESULT; + readonly diff: BettererDiff; + readonly expected: BettererResult; readonly files: BettererFilePaths; readonly name: string; - readonly result: unknown; - readonly shouldPrint: boolean; + readonly result: BettererResult; readonly test: BettererTest; readonly timestamp: number; readonly isBetter: boolean; @@ -135,7 +148,6 @@ export declare type BettererRun = { readonly isSkipped: boolean; readonly isUpdated: boolean; readonly isWorse: boolean; - diff(): void; }; export declare type BettererRuns = ReadonlyArray; @@ -161,16 +173,16 @@ export declare type BettererStats = { readonly worse: BettererTestNames; }; -export declare class BettererTest extends BettererTestState { +export declare class BettererTest extends BettererTestState { readonly constraint: BettererTestConstraint; readonly deadline: number; - differ?: BettererDiffer; + readonly differ?: BettererDiffer; readonly goal: BettererTestGoal; - isBettererTest: string; - printer?: BettererPrinter; - serialiser?: BettererSerialiser; + readonly isBettererTest = "isBettererTest"; + readonly printer?: BettererPrinter; + readonly serialiser?: BettererSerialiser; readonly test: BettererTestFunction; - constructor(options: BettererTestOptions); + constructor(options: BettererTestOptions); } export declare type BettererTestConstraint = (result: DeserialisedType, expected: DeserialisedType) => MaybeAsync; @@ -181,12 +193,12 @@ export declare type BettererTestGoal = (result: DeserialisedTy export declare type BettererTestNames = Array; -export declare type BettererTestOptions = { +export declare type BettererTestOptions = { constraint: BettererTestConstraint; deadline?: Date | string; goal?: DeserialisedType | BettererTestGoal; test: BettererTestFunction; - differ?: BettererDiffer; + differ?: BettererDiffer; printer?: BettererPrinter; serialiser?: BettererSerialiser; } & BettererTestStateOptions; diff --git a/packages/betterer/src/context/context.ts b/packages/betterer/src/context/context.ts index fa0905fbb..111c11427 100644 --- a/packages/betterer/src/context/context.ts +++ b/packages/betterer/src/context/context.ts @@ -1,10 +1,10 @@ import * as assert from 'assert'; -import { BettererConfig, BettererConfigFilters, BettererConfigPaths } from '../config'; +import { BettererConfig } from '../config'; import { COULDNT_READ_CONFIG } from '../errors'; import { BettererReporter } from '../reporters'; import { requireUncached } from '../require'; -import { print, read, write, NO_PREVIOUS_RESULT, BettererExpectedResults, BettererExpectedResult } from '../results'; +import { BettererResults, BettererDiff } from '../results'; import { BettererTest, isBettererTest, @@ -26,6 +26,7 @@ enum BettererContextStatus { } export class BettererContextΩ implements BettererContext { + private _results: BettererResults; private _stats: BettererStatsΩ | null = null; private _tests: BettererTestMap = {}; private _status = BettererContextStatus.notReady; @@ -34,6 +35,7 @@ export class BettererContextΩ implements BettererContext { private _finish: Resolve | null = null; constructor(public readonly config: BettererConfig, private _reporter?: BettererReporter) { + this._results = new BettererResults(config); this._reporter?.contextStart?.(this); } @@ -42,8 +44,9 @@ export class BettererContextΩ implements BettererContext { await this._running; } assert(this._status === BettererContextStatus.notReady || this._status === BettererContextStatus.end); - this._tests = this._initTests(this.config.configPaths); - this._initFilters(this.config.filters); + + this._tests = this._initTests(); + this._initFilters(); this._status = BettererContextStatus.ready; } @@ -56,20 +59,20 @@ export class BettererContextΩ implements BettererContext { public async runnerStart(files: BettererFilePaths = []): Promise { assert.equal(this._status, BettererContextStatus.ready); this._stats = new BettererStatsΩ(); - const expectedRaw = await this._initExpected(); - const runs = Object.keys(this._tests) - .filter((name) => { - const test = this._tests[name]; - // Only run BettererFileTests when a list of files is given: - return !files.length || isBettererFileTest(test); - }) - .map((name) => { - let expected: BettererExpectedResult | null = null; - if (Object.hasOwnProperty.call(expectedRaw, name)) { - expected = expectedRaw[name]; - } - return new BettererRunΩ(this, name, this._tests[name], expected || NO_PREVIOUS_RESULT, files); - }); + await this._initObsolete(); + const runs = await Promise.all( + Object.keys(this._tests) + .filter((name) => { + const test = this._tests[name]; + // Only run BettererFileTests when a list of files is given: + return !files.length || isBettererFileTest(test); + }) + .map(async (name) => { + const test = this._tests[name]; + const expected = await this._results.getResult(name, test); + return new BettererRunΩ(this, name, test, expected, files); + }) + ); this._reporter?.runsStart?.(runs, files); this._status = BettererContextStatus.running; this._running = new Promise((resolve) => { @@ -124,14 +127,16 @@ export class BettererContextΩ implements BettererContext { this._stats.skipped.push(run.name); } - public runUpdate(run: BettererRunΩ): void { + public runUpdate(run: BettererRunΩ): BettererDiff { assert(this._stats); this._stats.updated.push(run.name); + return this._results.getDiff(run); } - public runWorse(run: BettererRunΩ): void { + public runWorse(run: BettererRunΩ): BettererDiff { assert(this._stats); this._stats.worse.push(run.name); + return this._results.getDiff(run); } public runEnd(run: BettererRunΩ): void { @@ -145,18 +150,18 @@ export class BettererContextΩ implements BettererContext { public async process(runs: BettererRunsΩ): Promise { assert.equal(this._status, BettererContextStatus.end); assert(this._stats); - const printed: Array = await Promise.all(runs.filter((run) => run.shouldPrint).map((run) => print(run))); + const printed = await this._results.print(runs); try { - await write(printed, this.config.resultsPath); + await this._results.write(printed); } catch (error) { this._reporter?.contextError?.(this, error, printed); } return this._stats; } - private _initTests(configPaths: BettererConfigPaths): BettererTestMap { + private _initTests(): BettererTestMap { let tests: BettererTestMap = {}; - configPaths.map((configPath) => { + this.config.configPaths.map((configPath) => { const more = this._getTests(configPath); tests = { ...tests, ...more }; }); @@ -181,7 +186,7 @@ export class BettererContextΩ implements BettererContext { if (isBettererTest(maybeTest)) { test = maybeTest; } else { - test = new BettererTest(testOptions[name] as BettererTestOptions); + test = new BettererTest(testOptions[name] as BettererTestOptions); } assert(test); tests[name] = test; @@ -192,17 +197,17 @@ export class BettererContextΩ implements BettererContext { } } - private async _initExpected(): Promise { + private async _initObsolete(): Promise { assert(this._stats); - const expectedRaw = await read(this.config.resultsPath); - const obsolete = Object.keys(expectedRaw).filter( + const resultNames = await this._results.getResultNames(); + const obsolete = resultNames.filter( (expectedName) => !Object.keys(this._tests).find((name) => name === expectedName) ); this._stats.obsolete.push(...obsolete); - return expectedRaw; } - private _initFilters(filters: BettererConfigFilters): void { + private _initFilters(): void { + const { filters } = this.config; if (filters.length) { Object.keys(this._tests).forEach((name) => { if (!filters.some((filter) => filter.test(name))) { diff --git a/packages/betterer/src/context/run.ts b/packages/betterer/src/context/run.ts index 7b3fe465e..783d93018 100644 --- a/packages/betterer/src/context/run.ts +++ b/packages/betterer/src/context/run.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; -import { BettererExpectedResult, NO_PREVIOUS_RESULT, deserialise, diff } from '../results'; +import { BettererResultΩ, BettererDiff } from '../results'; import { BettererTest } from '../test'; import { BettererFilePaths } from '../watcher'; import { BettererContextΩ } from './context'; @@ -18,41 +18,32 @@ enum BettererRunStatus { } export class BettererRunΩ implements BettererRun { - private _result: unknown; - private _toPrint: unknown; + private _diff: BettererDiff | null = null; + private _result: BettererResultΩ | null = null; private _status: BettererRunStatus = BettererRunStatus.pending; - - private _expected: unknown; private _timestamp: number | null = null; private _isComplete = false; private _isExpired = false; - private _isNew = true; - private _hasResult = false; constructor( private readonly _context: BettererContextΩ, private readonly _name: string, private readonly _test: BettererTest, - expected: BettererExpectedResult | typeof NO_PREVIOUS_RESULT, + private readonly _expected: BettererResultΩ, private readonly _files: BettererFilePaths - ) { - if (expected === NO_PREVIOUS_RESULT) { - this._expected = NO_PREVIOUS_RESULT; - return; - } else { - this._isNew = false; - this._expected = deserialise(this, expected.value); - this._toPrint = this._expected; - this._hasResult = true; - } + ) {} + + public get diff(): BettererDiff { + assert(this._diff); + return this._diff; } public get name(): string { return this._name; } - public get expected(): unknown | typeof NO_PREVIOUS_RESULT { + public get expected(): BettererResultΩ { return this._expected; } @@ -60,14 +51,6 @@ export class BettererRunΩ implements BettererRun { return this._files; } - public get shouldPrint(): boolean { - return !this.isComplete && this._hasResult; - } - - public get toPrint(): unknown { - return this._toPrint; - } - public get timestamp(): number { assert.notStrictEqual(this._status, BettererRunStatus.pending); assert.notStrictEqual(this._timestamp, null); @@ -91,7 +74,7 @@ export class BettererRunΩ implements BettererRun { } public get isNew(): boolean { - return this._isNew; + return this._expected.isNew; } public get isSame(): boolean { @@ -110,7 +93,8 @@ export class BettererRunΩ implements BettererRun { return this._status === BettererRunStatus.worse; } - public get result(): unknown { + public get result(): BettererResultΩ { + assert(this._result); return this._result; } @@ -118,13 +102,11 @@ export class BettererRunΩ implements BettererRun { return this._test; } - public better(result: unknown, isComplete: boolean): void { + public better(result: BettererResultΩ, isComplete: boolean): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.better; this._isComplete = isComplete; this._result = result; - this._toPrint = result; - this._hasResult = true; this._context.runBetter(this); } @@ -138,13 +120,11 @@ export class BettererRunΩ implements BettererRun { this._context.runFailed(this); } - public neww(result: unknown, isComplete: boolean): void { + public neww(result: BettererResultΩ, isComplete: boolean): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.neww; this._isComplete = isComplete; this._result = result; - this._toPrint = result; - this._hasResult = true; this._context.runNew(this); } @@ -159,46 +139,31 @@ export class BettererRunΩ implements BettererRun { this._timestamp = startTime; } - public same(result: unknown): void { + public same(result: BettererResultΩ): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.same; this._result = result; - this._toPrint = result; - this._hasResult = true; this._context.runSame(this); } public skipped(): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.skipped; - if (this._expected !== NO_PREVIOUS_RESULT) { - this._result = this._expected; - this._toPrint = this._expected; - this._hasResult = true; - } this._context.runSkipped(this); } - public update(result: unknown): void { + public update(result: BettererResultΩ): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.update; this._result = result; - this._toPrint = result; - this._hasResult = true; - this._context.runUpdate(this); + this._diff = this._context.runUpdate(this); } - public worse(result: unknown): void { + public worse(result: BettererResultΩ): void { assert.equal(this._status, BettererRunStatus.pending); this._status = BettererRunStatus.worse; this._result = result; - this._toPrint = this._expected; - this._hasResult = true; - this._context.runWorse(this); - } - - public diff(): void { - diff(this); + this._diff = this._context.runWorse(this); } } diff --git a/packages/betterer/src/context/types.ts b/packages/betterer/src/context/types.ts index 9b25bbea9..ad3c7f6f6 100644 --- a/packages/betterer/src/context/types.ts +++ b/packages/betterer/src/context/types.ts @@ -1,6 +1,6 @@ import { BettererFilePaths } from '../watcher'; import { BettererConfig } from '../config'; -import { NO_PREVIOUS_RESULT } from '../results'; +import { BettererDiff, BettererResult } from '../results'; import { BettererTest } from '../test'; export type BettererRuns = ReadonlyArray; @@ -13,11 +13,11 @@ export type BettererContext = { }; export type BettererRun = { - readonly expected: unknown | typeof NO_PREVIOUS_RESULT; + readonly diff: BettererDiff; + readonly expected: BettererResult; readonly files: BettererFilePaths; readonly name: string; - readonly result: unknown; - readonly shouldPrint: boolean; + readonly result: BettererResult; readonly test: BettererTest; readonly timestamp: number; @@ -30,8 +30,6 @@ export type BettererRun = { readonly isSkipped: boolean; readonly isUpdated: boolean; readonly isWorse: boolean; - - diff(): void; }; export type BettererStats = { diff --git a/packages/betterer/src/index.ts b/packages/betterer/src/index.ts index bf13b03fb..a92273dd5 100644 --- a/packages/betterer/src/index.ts +++ b/packages/betterer/src/index.ts @@ -8,6 +8,7 @@ export { BettererConfigPaths } from './config/public'; export { BettererContext, BettererRun, BettererRuns, BettererStats, BettererTestNames } from './context/public'; +export { BettererDiff, BettererResult } from './results/public'; export { BettererReporter, BettererReporterNames } from './reporters/public'; export { BettererDeserialise, @@ -27,6 +28,7 @@ export { BettererFileTestDiff, BettererFileTestFunction, BettererFiles, + BettererFilesDiff, BettererPrinter, BettererSerialise, BettererSerialiser, diff --git a/packages/betterer/src/results/deserialiser.ts b/packages/betterer/src/results/deserialiser.ts deleted file mode 100644 index 42f8e9534..000000000 --- a/packages/betterer/src/results/deserialiser.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BettererRunΩ } from '../context'; - -export function deserialise(run: BettererRunΩ, serialised: string): unknown { - const { test } = run; - const parsed = JSON.parse(serialised) as unknown; - const deserialiser = test.serialiser?.deserialise || defaultDeserialiser; - return deserialiser(parsed); -} - -function defaultDeserialiser(serialised: unknown): unknown { - return serialised; -} diff --git a/packages/betterer/src/results/diff.ts b/packages/betterer/src/results/diff.ts deleted file mode 100644 index dd0afe8fa..000000000 --- a/packages/betterer/src/results/diff.ts +++ /dev/null @@ -1,24 +0,0 @@ -import logDiff from 'jest-diff'; - -import { BettererRun } from '../context'; - -export function diff(run: BettererRun): unknown { - const { test } = run; - const differ = test.differ || defaultDiff; - return differ(run); -} - -const DIFF_OPTIONS = { - aAnnotation: 'Result', - bAnnotation: 'Expected' -}; -const NEW_LINE = '\n'; - -function defaultDiff(run: BettererRun): void { - const { expected, result } = run; - const diffStr = logDiff(result, expected, DIFF_OPTIONS) || ''; - const lines = diffStr.split(NEW_LINE); - lines.forEach((line) => { - process.stdout.write(` ${line}`); - }); -} diff --git a/packages/betterer/src/results/differ.ts b/packages/betterer/src/results/differ.ts new file mode 100644 index 000000000..27d7cb966 --- /dev/null +++ b/packages/betterer/src/results/differ.ts @@ -0,0 +1,25 @@ +import logDiff from 'jest-diff'; + +import { BettererDiff } from './types'; + +const DIFF_OPTIONS = { + aAnnotation: 'Result', + bAnnotation: 'Expected' +}; + +const NEW_LINE = '\n'; + +export function defaultDiffer(expected: unknown, result: unknown): BettererDiff { + return { + expected, + result, + diff: null, + log() { + const diffStr = logDiff(result, expected, DIFF_OPTIONS) || ''; + const lines = diffStr.split(NEW_LINE); + lines.forEach((line) => { + process.stdout.write(` ${line}`); + }); + } + }; +} diff --git a/packages/betterer/src/results/index.ts b/packages/betterer/src/results/index.ts index 3f6a2c3ca..246823f13 100644 --- a/packages/betterer/src/results/index.ts +++ b/packages/betterer/src/results/index.ts @@ -1,7 +1,3 @@ -export { diff } from './diff'; -export { deserialise } from './deserialiser'; -export { print } from './printer'; -export { read } from './reader'; -export { serialise } from './serialiser'; -export { write } from './writer'; -export { BettererExpectedResult, BettererExpectedResults, NO_PREVIOUS_RESULT } from './types'; +export { BettererResultΩ } from './result'; +export { BettererResults } from './results'; +export { BettererExpectedResult, BettererExpectedResults, BettererDiff, BettererResult } from './types'; diff --git a/packages/betterer/src/results/printer.ts b/packages/betterer/src/results/printer.ts index 3255cd131..19bbefabd 100644 --- a/packages/betterer/src/results/printer.ts +++ b/packages/betterer/src/results/printer.ts @@ -1,20 +1,15 @@ import { escape } from 'safe-string-literal'; import { isString } from '../utils'; -import { BettererRunΩ } from '../context'; -import { serialise } from './serialiser'; // Characters that we avoid escaping to make snapshots easier to visually diff const UNESCAPED = '"\n'; -export async function print(run: BettererRunΩ): Promise { - const { test, name } = run; - const printer = test.printer || defaultPrinter; - const printedValue = await printer(run, serialise(run)); +export function print(name: string, printedValue: string): string { const escaped = escape(printedValue, UNESCAPED); return `\nexports[\`${name}\`] = {\n value: \`${escaped}\`\n};\n`; } -function defaultPrinter(_: BettererRunΩ, value: unknown): string { - return isString(value) ? value : JSON.stringify(value); +export function defaultPrinter(serialised: unknown): string { + return isString(serialised) ? serialised : JSON.stringify(serialised); } diff --git a/packages/betterer/src/results/public.ts b/packages/betterer/src/results/public.ts new file mode 100644 index 000000000..c4d0a94c7 --- /dev/null +++ b/packages/betterer/src/results/public.ts @@ -0,0 +1 @@ +export { BettererDiff, BettererResult } from './types'; diff --git a/packages/betterer/src/results/result.ts b/packages/betterer/src/results/result.ts new file mode 100644 index 000000000..07a62ed25 --- /dev/null +++ b/packages/betterer/src/results/result.ts @@ -0,0 +1,23 @@ +import { BettererResult } from './types'; + +const NO_PREVIOUS_RESULT = Symbol('No Previous Result'); + +export class BettererResultΩ implements BettererResult { + private readonly _isNew: boolean; + private readonly _value: unknown; + + constructor(value: unknown | typeof NO_PREVIOUS_RESULT = NO_PREVIOUS_RESULT) { + this._isNew = value === NO_PREVIOUS_RESULT; + if (!this._isNew) { + this._value = value; + } + } + + public get isNew(): boolean { + return this._isNew; + } + + public get value(): unknown { + return this._value; + } +} diff --git a/packages/betterer/src/results/results.ts b/packages/betterer/src/results/results.ts new file mode 100644 index 000000000..256f53c40 --- /dev/null +++ b/packages/betterer/src/results/results.ts @@ -0,0 +1,57 @@ +import * as assert from 'assert'; + +import { BettererConfig } from '../config'; +import { BettererTest } from '../test'; +import { read } from './reader'; +import { BettererRunΩ, BettererRunsΩ } from '../context'; +import { print, defaultPrinter } from './printer'; +import { write } from './writer'; +import { BettererResultΩ } from './result'; +import { BettererDiff } from './types'; +import { defaultDiffer } from './differ'; + +export class BettererResults { + constructor(private _config: BettererConfig) {} + + public async getResultNames(): Promise> { + const results = await read(this._config.resultsPath); + return Object.keys(results); + } + + public async getResult(name: string, test: BettererTest): Promise { + const results = await read(this._config.resultsPath); + if (Object.hasOwnProperty.call(results, name)) { + assert(results[name]); + const { value } = results[name]; + const parsed = JSON.parse(value) as unknown; + return new BettererResultΩ(test.serialiser?.deserialise?.(parsed) || parsed); + } + return new BettererResultΩ(); + } + + public getDiff(run: BettererRunΩ): BettererDiff { + const differ = run.test.differ || defaultDiffer; + return differ(run.expected.value, run.result.value) as BettererDiff; + } + + public async print(runs: BettererRunsΩ): Promise> { + const toPrint = runs.filter((run) => { + const { isComplete, isNew, isSkipped, isFailed } = run; + return !(isComplete || (isNew && (isSkipped || isFailed))); + }); + return await Promise.all( + toPrint.map(async (run) => { + const { name, test, isFailed, isSkipped, isWorse } = run; + const toPrint = isFailed || isSkipped || isWorse ? run.expected : run.result; + const serialised = test.serialiser?.serialise?.(toPrint.value) || toPrint.value; + const printer = test.printer || defaultPrinter; + const printedValue = await printer(serialised); + return print(name, printedValue); + }) + ); + } + + public async write(printed: Array): Promise { + await write(printed, this._config.resultsPath); + } +} diff --git a/packages/betterer/src/results/serialiser.ts b/packages/betterer/src/results/serialiser.ts deleted file mode 100644 index 03313f41d..000000000 --- a/packages/betterer/src/results/serialiser.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BettererRunΩ } from '../context'; - -export function serialise(run: BettererRunΩ): unknown { - const { test, toPrint } = run; - const serialiser = test.serialiser?.serialise || defaultSerialiser; - return serialiser(toPrint); -} - -function defaultSerialiser(result: unknown): unknown { - return result; -} diff --git a/packages/betterer/src/results/types.ts b/packages/betterer/src/results/types.ts index cba509a9d..5cf9b22b6 100644 --- a/packages/betterer/src/results/types.ts +++ b/packages/betterer/src/results/types.ts @@ -1,6 +1,16 @@ -export const NO_PREVIOUS_RESULT = Symbol('No Previous Result'); - export type BettererExpectedResult = { value: string; }; export type BettererExpectedResults = Record; + +export type BettererDiff = { + expected: DeserialisedType; + result: DeserialisedType; + diff: DiffType; + log: () => void; +}; + +export type BettererResult = { + isNew: boolean; + value: unknown; +}; diff --git a/packages/betterer/src/runner/runner.ts b/packages/betterer/src/runner/runner.ts index c491b113c..5cdbd7106 100644 --- a/packages/betterer/src/runner/runner.ts +++ b/packages/betterer/src/runner/runner.ts @@ -3,6 +3,7 @@ import { logErrorΔ } from '@betterer/errors'; import { BettererContextΩ, BettererRunΩ, BettererRunsΩ } from '../context'; import { BettererFilePaths } from '../watcher'; +import { BettererResultΩ } from '../results'; export async function parallel(context: BettererContextΩ, files: BettererFilePaths): Promise { const runs = await context.runnerStart(files); @@ -36,9 +37,9 @@ async function runTest(run: BettererRunΩ, update: boolean): Promise { } run.start(); - let result: unknown; + let result: BettererResultΩ; try { - result = await test.test(run); + result = new BettererResultΩ(await test.test(run)); } catch (e) { run.failed(); logErrorΔ(e); @@ -46,14 +47,14 @@ async function runTest(run: BettererRunΩ, update: boolean): Promise { } run.ran(); - const goalComplete = await test.goal(result); + const goalComplete = await test.goal(result.value); if (run.isNew) { run.neww(result, goalComplete); return; } - const comparison = await test.constraint(result, run.expected); + const comparison = await test.constraint(result.value, run.expected.value); if (comparison === BettererConstraintResult.same) { run.same(result); diff --git a/packages/betterer/src/test/file-test/constraint.ts b/packages/betterer/src/test/file-test/constraint.ts index 12f4d5ceb..ea0dc3566 100644 --- a/packages/betterer/src/test/file-test/constraint.ts +++ b/packages/betterer/src/test/file-test/constraint.ts @@ -1,174 +1,28 @@ import { BettererConstraintResult } from '@betterer/constraints'; -import * as assert from 'assert'; -import { BettererFile } from './file'; -import { ensureDeserialised } from './serialiser'; -import { BettererFiles, BettererFileTestDiff, BettererFileIssueDeserialised } from './types'; +import { differ } from './differ'; +import { BettererFiles } from './types'; -type BettererFileTestConstraintResult = { - constraintResult: BettererConstraintResult; - diff: BettererFileTestDiff; -}; - -export function constraint(result: BettererFiles, expected: BettererFiles): BettererFileTestConstraintResult { - const diff = getDiff(result, expected); +export function constraint(result: BettererFiles, expected: BettererFiles): BettererConstraintResult { + const { diff } = differ(expected, result); const filePaths = Object.keys(diff); if (filePaths.length === 0) { - return { constraintResult: BettererConstraintResult.same, diff }; + return BettererConstraintResult.same; } const hasNew = filePaths.filter((filePath) => !!diff[filePath].neww?.length); if (hasNew.length) { - return { constraintResult: BettererConstraintResult.worse, diff }; + return BettererConstraintResult.worse; } const hasFixed = filePaths.filter((filePath) => !!diff[filePath].fixed?.length); if (hasFixed.length) { - return { constraintResult: BettererConstraintResult.better, diff }; + return BettererConstraintResult.better; } - return { constraintResult: BettererConstraintResult.same, diff }; -} - -function getDiff(result: BettererFiles, expected: BettererFiles): BettererFileTestDiff { - const diff = {} as BettererFileTestDiff; - - const unchangedResultFiles = result.filesΔ.filter((r) => - expected.filesΔ.find((e) => e.absolutePath === r.absolutePath && e.hash === r.hash) - ); - - const changedResultFiles = result.filesΔ.filter((r) => - expected.filesΔ.find((e) => e.absolutePath === r.absolutePath && e.hash !== r.hash) - ); - - const newOrMovedFiles = result.filesΔ.filter((r) => !expected.filesΔ.find((e) => e.absolutePath === r.absolutePath)); - - const fixedOrMovedFiles = expected.filesΔ.filter( - (e) => !result.filesΔ.find((r) => r.absolutePath === e.absolutePath) - ); - - const movedFiles = new Map(); - fixedOrMovedFiles.forEach((fixedOrMovedFile, index) => { - // A file may have been moved it has the same hash in both result and expected - const possibilities = newOrMovedFiles.filter((newOrMovedFile) => newOrMovedFile.hash === fixedOrMovedFile.hash); - if (!possibilities.length) { - return; - } - - // Multiple possibilities means that the same content has been moved into multiple new files. - // So just count the first one as a move, the rest will be new files: - const [moved] = possibilities; - movedFiles.set(moved, fixedOrMovedFile); - - // Remove the moved file from the fixedOrMovedFiles array: - fixedOrMovedFiles.splice(index, 1); - // And from the newOrMovedFiles array: - newOrMovedFiles.splice(newOrMovedFiles.indexOf(moved), 1); - }); - - // All the moved files have been removed from fixedOrMovedFiles and newOrMovedFiles: - const fixedFiles = fixedOrMovedFiles; - const newFiles = newOrMovedFiles; - - fixedFiles.forEach((file) => { - diff[file.relativePath] = { - fixed: file.issuesDeserialised - }; - }); - - newFiles.forEach((file) => { - diff[file.relativePath] = { - neww: file.issuesRaw - }; - }); - - const existingFiles = [...unchangedResultFiles, ...changedResultFiles, ...Array.from(movedFiles.keys())]; - existingFiles.forEach((resultFile) => { - const expectedFile = expected.getFileΔ(resultFile.absolutePath) || movedFiles.get(resultFile); - - assert(resultFile); - assert(expectedFile); - - // Convert all issues to their deserialised form for easier diffing: - const resultIssues = [...ensureDeserialised(resultFile)]; - const expectedIssues = ensureDeserialised(expectedFile); - - // Find all issues that exist in both result and expected: - const unchangedExpectedIssues = expectedIssues.filter((r) => - resultIssues.find((e) => { - return e.line === r.line && e.column === r.column && e.length === r.length && e.hash === r.hash; - }) - ); - const unchangedResultIssues = resultIssues.filter((r) => - expectedIssues.find((e) => { - return e.line === r.line && e.column === r.column && e.length === r.length && e.hash === r.hash; - }) - ); - - // Any result issues that aren't in expected are either new or have been moved: - const newOrMovedIssues = resultIssues.filter((r) => !unchangedResultIssues.includes(r)); - // Any expected issues that aren't in result are either fixed or have been moved: - const fixedOrMovedIssues = expectedIssues.filter((e) => !unchangedExpectedIssues.includes(e)); - - // We can find the moved issues by matching the issue hashes: - const movedIssues: Array = []; - const fixedIssues: Array = []; - fixedOrMovedIssues.forEach((fixedOrMovedIssue) => { - const { hash, line, column } = fixedOrMovedIssue; - // An issue may have been moved it has the same hash in both result and expected - const possibilities = newOrMovedIssues.filter((newOrMovedIssue) => newOrMovedIssue.hash === hash); - if (!possibilities.length) { - // If there is no matching has the issue must have been fixed: - fixedIssues.push(fixedOrMovedIssue); - return; - } - // Start by marking the first possibility as best: - let best = possibilities.shift(); - - // And then search through all the possibilities to find the closest issue: - possibilities.forEach((possibility) => { - assert(best); - if (Math.abs(line - possibility.line) >= Math.abs(line - best.line)) { - return; - } - if (Math.abs(line - possibility.line) < Math.abs(line - best.line)) { - best = possibility; - } - if (Math.abs(column - possibility.column) >= Math.abs(column - best.column)) { - return; - } - if (Math.abs(column - possibility.column) < Math.abs(column - best.column)) { - best = possibility; - } - }); - - assert(best); - - // Remove the moved issue from the newOrMovedIssues array: - newOrMovedIssues.splice(newOrMovedIssues.indexOf(best), 1); - - movedIssues.push(best); - }); - - // Find the raw issue data so that diffs can be logged: - const newIssues = newOrMovedIssues.map((newIssue) => resultFile.issuesRaw[resultIssues.indexOf(newIssue)]); - - // If there's no change, move on: - if (!newIssues.length && !fixedIssues.length) { - return; - } - - // Otherwise construct the diff: - diff[resultFile.relativePath] = { - existing: [...unchangedExpectedIssues, ...movedIssues], - fixed: fixedIssues, - neww: newIssues - }; - }); - - return diff; + return BettererConstraintResult.same; } diff --git a/packages/betterer/src/test/file-test/differ.ts b/packages/betterer/src/test/file-test/differ.ts index a639b3167..3e91fcb18 100644 --- a/packages/betterer/src/test/file-test/differ.ts +++ b/packages/betterer/src/test/file-test/differ.ts @@ -1,27 +1,174 @@ import { codeΔ, errorΔ, successΔ, warnΔ } from '@betterer/logger'; import * as assert from 'assert'; -import { BettererRun } from '../../context'; -import { BettererFileTest } from './file-test'; - -export function differ(run: BettererRun): void { - const { diff } = run.test as BettererFileTest; - assert(diff); - Object.keys(diff).forEach((file) => { - const issues = diff[file]; - const { existing, fixed } = issues; - if (fixed?.length) { - successΔ(`${fixed.length} fixed ${getIssues(fixed.length)} in "${file}".`); - } - if (existing?.length) { - warnΔ(`${existing.length} existing ${getIssues(existing.length)} in "${file}".`); +import { ensureDeserialised } from './serialiser'; +import { + BettererFile, + BettererFiles, + BettererFileTestDiff, + BettererFileIssueDeserialised, + BettererFilesDiff +} from './types'; + +export function differ(expected: BettererFiles, result: BettererFiles): BettererFileTestDiff { + const diff = {} as BettererFilesDiff; + + const unchangedResultFiles = result.filesΔ.filter((r) => + expected.filesΔ.find((e) => e.absolutePath === r.absolutePath && e.hash === r.hash) + ); + + const changedResultFiles = result.filesΔ.filter((r) => + expected.filesΔ.find((e) => e.absolutePath === r.absolutePath && e.hash !== r.hash) + ); + + const newOrMovedFiles = result.filesΔ.filter((r) => !expected.filesΔ.find((e) => e.absolutePath === r.absolutePath)); + + const fixedOrMovedFiles = expected.filesΔ.filter( + (e) => !result.filesΔ.find((r) => r.absolutePath === e.absolutePath) + ); + + const movedFiles = new Map(); + fixedOrMovedFiles.forEach((fixedOrMovedFile, index) => { + // A file may have been moved it has the same hash in both result and expected + const possibilities = newOrMovedFiles.filter((newOrMovedFile) => newOrMovedFile.hash === fixedOrMovedFile.hash); + if (!possibilities.length) { + return; } - if (issues.neww?.length) { - const { length } = issues.neww; - errorΔ(`${length} new ${getIssues(length)} in "${file}":`); - issues.neww.forEach((info) => codeΔ(info)); + + // Multiple possibilities means that the same content has been moved into multiple new files. + // So just count the first one as a move, the rest will be new files: + const [moved] = possibilities; + movedFiles.set(moved, fixedOrMovedFile); + + // Remove the moved file from the fixedOrMovedFiles array: + fixedOrMovedFiles.splice(index, 1); + // And from the newOrMovedFiles array: + newOrMovedFiles.splice(newOrMovedFiles.indexOf(moved), 1); + }); + + // All the moved files have been removed from fixedOrMovedFiles and newOrMovedFiles: + const fixedFiles = fixedOrMovedFiles; + const newFiles = newOrMovedFiles; + + fixedFiles.forEach((file) => { + diff[file.relativePath] = { + fixed: file.issuesDeserialised + }; + }); + + newFiles.forEach((file) => { + diff[file.relativePath] = { + neww: file.issuesRaw + }; + }); + + const existingFiles = [...unchangedResultFiles, ...changedResultFiles, ...Array.from(movedFiles.keys())]; + existingFiles.forEach((resultFile) => { + const expectedFile = expected.getFileΔ(resultFile.absolutePath) || movedFiles.get(resultFile); + + assert(resultFile); + assert(expectedFile); + + // Convert all issues to their deserialised form for easier diffing: + const resultIssues = [...ensureDeserialised(resultFile)]; + const expectedIssues = ensureDeserialised(expectedFile); + + // Find all issues that exist in both result and expected: + const unchangedExpectedIssues = expectedIssues.filter((r) => + resultIssues.find((e) => { + return e.line === r.line && e.column === r.column && e.length === r.length && e.hash === r.hash; + }) + ); + const unchangedResultIssues = resultIssues.filter((r) => + expectedIssues.find((e) => { + return e.line === r.line && e.column === r.column && e.length === r.length && e.hash === r.hash; + }) + ); + + // Any result issues that aren't in expected are either new or have been moved: + const newOrMovedIssues = resultIssues.filter((r) => !unchangedResultIssues.includes(r)); + // Any expected issues that aren't in result are either fixed or have been moved: + const fixedOrMovedIssues = expectedIssues.filter((e) => !unchangedExpectedIssues.includes(e)); + + // We can find the moved issues by matching the issue hashes: + const movedIssues: Array = []; + const fixedIssues: Array = []; + fixedOrMovedIssues.forEach((fixedOrMovedIssue) => { + const { hash, line, column } = fixedOrMovedIssue; + // An issue may have been moved it has the same hash in both result and expected + const possibilities = newOrMovedIssues.filter((newOrMovedIssue) => newOrMovedIssue.hash === hash); + if (!possibilities.length) { + // If there is no matching has the issue must have been fixed: + fixedIssues.push(fixedOrMovedIssue); + return; + } + // Start by marking the first possibility as best: + let best = possibilities.shift(); + + // And then search through all the possibilities to find the closest issue: + possibilities.forEach((possibility) => { + assert(best); + if (Math.abs(line - possibility.line) >= Math.abs(line - best.line)) { + return; + } + if (Math.abs(line - possibility.line) < Math.abs(line - best.line)) { + best = possibility; + } + if (Math.abs(column - possibility.column) >= Math.abs(column - best.column)) { + return; + } + if (Math.abs(column - possibility.column) < Math.abs(column - best.column)) { + best = possibility; + } + }); + + assert(best); + + // Remove the moved issue from the newOrMovedIssues array: + newOrMovedIssues.splice(newOrMovedIssues.indexOf(best), 1); + + movedIssues.push(best); + }); + + // Find the raw issue data so that diffs can be logged: + const newIssues = newOrMovedIssues.map((newIssue) => resultFile.issuesRaw[resultIssues.indexOf(newIssue)]); + + // If there's no change, move on: + if (!newIssues.length && !fixedIssues.length) { + return; } + + // Otherwise construct the diff: + diff[resultFile.relativePath] = { + existing: [...unchangedExpectedIssues, ...movedIssues], + fixed: fixedIssues, + neww: newIssues + }; }); + + return { + expected, + result, + diff, + log() { + assert(diff); + Object.keys(diff).forEach((file) => { + const issues = diff[file]; + const { existing, fixed } = issues; + if (fixed?.length) { + successΔ(`${fixed.length} fixed ${getIssues(fixed.length)} in "${file}".`); + } + if (existing?.length) { + warnΔ(`${existing.length} existing ${getIssues(existing.length)} in "${file}".`); + } + if (issues.neww?.length) { + const { length } = issues.neww; + errorΔ(`${length} new ${getIssues(length)} in "${file}":`); + issues.neww.forEach((info) => codeΔ(info)); + } + }); + } + }; } function getIssues(count: number): string { diff --git a/packages/betterer/src/test/file-test/file-test.ts b/packages/betterer/src/test/file-test/file-test.ts index 08ef16783..cec66baa6 100644 --- a/packages/betterer/src/test/file-test/file-test.ts +++ b/packages/betterer/src/test/file-test/file-test.ts @@ -1,15 +1,13 @@ -import { BettererConstraintResult } from '@betterer/constraints'; - import { getConfig } from '../../config'; import { BettererRun } from '../../context'; import { createHash } from '../../hasher'; -import { NO_PREVIOUS_RESULT } from '../../results'; import { BettererFilePaths } from '../../watcher'; +import { getRelativePath } from '../../utils'; import { BettererTest } from '../test'; -import { BettererTestFunction, BettererTestConstraint } from '../types'; +import { BettererTestFunction } from '../types'; import { constraint } from './constraint'; import { differ } from './differ'; -import { BettererFile } from './file'; +import { BettererFileΩ } from './file'; import { BettererFileResolver } from './file-resolver'; import { BettererFilesΩ } from './files'; import { goal } from './goal'; @@ -21,17 +19,19 @@ import { BettererFileGlobs, BettererFileIssuesMapSerialised, BettererFileTestFunction, - BettererFileTestDiff + BettererFileTestDiff, + BettererFile } from './types'; -import { getRelativePath } from '../../utils'; const IS_BETTERER_FILE_TEST = 'isBettererTest'; -export class BettererFileTest extends BettererTest { +export class BettererFileTest extends BettererTest< + BettererFiles, + BettererFileIssuesMapSerialised, + BettererFileTestDiff +> { public isBettererFileTest = IS_BETTERER_FILE_TEST; - private _diff: BettererFileTestDiff | null = null; - private _constraint: BettererTestConstraint | null = null; private _test: BettererTestFunction | null = null; constructor(private _resolver: BettererFileResolver, fileTest: BettererFileTestFunction) { @@ -40,10 +40,7 @@ export class BettererFileTest extends BettererTest => { - this._constraint = this._constraint || this._createConstraint(); - return await this._constraint(result, expected); - }, + constraint, goal, serialiser: { @@ -55,10 +52,6 @@ export class BettererFileTest extends BettererTest => { const { files } = run; - const expected = run.expected as BettererFiles | typeof NO_PREVIOUS_RESULT; + const expected = run.expected.value as BettererFiles; const result = await fileTest(await this._resolver.filesΔ(files)); let absolutePaths: BettererFilePaths = Object.keys(result); - if (files.length && expected !== NO_PREVIOUS_RESULT) { + if (files.length && !run.isNew) { const expectedAbsolutePaths = expected.filesΔ.map((file) => file.absolutePath); absolutePaths = Array.from(new Set([...absolutePaths, ...expectedAbsolutePaths])); } @@ -89,27 +82,19 @@ export class BettererFileTest extends BettererTest ); }; } - - private _createConstraint() { - return (result: BettererFiles, expected: BettererFiles): BettererConstraintResult => { - const { diff, constraintResult } = constraint(result, expected); - this._diff = diff; - return constraintResult; - }; - } } export function isBettererFileTest(test: unknown): test is BettererFileTest { diff --git a/packages/betterer/src/test/file-test/file.ts b/packages/betterer/src/test/file-test/file.ts index 5e5d3ca8d..e63fcae63 100644 --- a/packages/betterer/src/test/file-test/file.ts +++ b/packages/betterer/src/test/file-test/file.ts @@ -7,7 +7,7 @@ import { BettererFileIssuesDeserialised } from './types'; -export class BettererFile { +export class BettererFileΩ { public readonly key: string; private _issuesDeserialised: BettererFileIssuesDeserialised | null = null; diff --git a/packages/betterer/src/test/file-test/files.ts b/packages/betterer/src/test/file-test/files.ts index 6f285530e..ed7cdd927 100644 --- a/packages/betterer/src/test/file-test/files.ts +++ b/packages/betterer/src/test/file-test/files.ts @@ -1,5 +1,4 @@ -import { BettererFile } from './file'; -import { BettererFiles } from './types'; +import { BettererFile, BettererFiles } from './types'; export class BettererFilesΩ implements BettererFiles { private _fileMap: Record = {}; diff --git a/packages/betterer/src/test/file-test/index.ts b/packages/betterer/src/test/file-test/index.ts index 4016bd014..2926a53ee 100644 --- a/packages/betterer/src/test/file-test/index.ts +++ b/packages/betterer/src/test/file-test/index.ts @@ -1,5 +1,4 @@ export { BettererFileTest, isBettererFileTest } from './file-test'; -export { BettererFile } from './file'; export { BettererFileResolver } from './file-resolver'; export { BettererFiles, @@ -16,5 +15,7 @@ export { BettererFileIssuesSerialised, BettererFilePatterns, BettererFileTestDiff, - BettererFileTestFunction + BettererFileTestFunction, + BettererFile, + BettererFilesDiff } from './types'; diff --git a/packages/betterer/src/test/file-test/printer.ts b/packages/betterer/src/test/file-test/printer.ts index 548e62ba2..aee1d2a96 100644 --- a/packages/betterer/src/test/file-test/printer.ts +++ b/packages/betterer/src/test/file-test/printer.ts @@ -1,7 +1,6 @@ -import { BettererRun } from '../../context'; import { BettererFileIssuesMapSerialised } from './types'; -export function printer(_: BettererRun, serialised: BettererFileIssuesMapSerialised): string { +export function printer(serialised: BettererFileIssuesMapSerialised): string { let printed = '{\n'; Object.keys(serialised) .sort() diff --git a/packages/betterer/src/test/file-test/public.ts b/packages/betterer/src/test/file-test/public.ts index bd270bbb9..e11dcbc08 100644 --- a/packages/betterer/src/test/file-test/public.ts +++ b/packages/betterer/src/test/file-test/public.ts @@ -1,8 +1,8 @@ export { BettererFileTest } from './file-test'; -export { BettererFile } from './file'; export { BettererFileResolver } from './file-resolver'; export { BettererFiles, + BettererFilesDiff, BettererFileGlobs, BettererFileIssueDeserialised, BettererFileIssueRaw, diff --git a/packages/betterer/src/test/file-test/serialiser.ts b/packages/betterer/src/test/file-test/serialiser.ts index a78855d1e..5e4b43776 100644 --- a/packages/betterer/src/test/file-test/serialiser.ts +++ b/packages/betterer/src/test/file-test/serialiser.ts @@ -2,9 +2,10 @@ import LinesAndColumns from 'lines-and-columns'; import { getConfig } from '../../config'; import { createHash } from '../../hasher'; import { getAbsolutePath } from '../../utils'; -import { BettererFile } from './file'; +import { BettererFileΩ } from './file'; import { BettererFilesΩ } from './files'; import { + BettererFile, BettererFiles, BettererFileIssuesMapSerialised, BettererFileIssuesRaw, @@ -27,7 +28,7 @@ export function deserialise(serialised: BettererFileIssuesMapSerialised): Better }); const { resultsPath } = getConfig(); const absolutePath = getAbsolutePath(resultsPath, relativePath); - return new BettererFile(relativePath, absolutePath, hash, issues); + return new BettererFileΩ(relativePath, absolutePath, hash, issues); }) ); } diff --git a/packages/betterer/src/test/file-test/types.ts b/packages/betterer/src/test/file-test/types.ts index 32e892ba3..36ff86248 100644 --- a/packages/betterer/src/test/file-test/types.ts +++ b/packages/betterer/src/test/file-test/types.ts @@ -1,8 +1,8 @@ import { BettererLoggerCodeInfo } from '@betterer/logger'; +import { BettererDiff } from '../../results'; import { MaybeAsync } from '../../types'; import { BettererFilePaths } from '../../watcher'; -import { BettererFile } from './file'; export type BettererFileIssueRaw = BettererLoggerCodeInfo & { hash?: string; @@ -26,7 +26,7 @@ export type BettererFileIssuesMapSerialised = Record | ReadonlyArray; -export type BettererFileTestDiff = Record< +export type BettererFilesDiff = Record< string, { fixed?: ReadonlyArray; @@ -34,12 +34,22 @@ export type BettererFileTestDiff = Record< neww?: ReadonlyArray; } >; +export type BettererFileTestDiff = BettererDiff; export type BettererFileTestFunction = (files: BettererFilePaths) => MaybeAsync; export type BettererFileGlobs = ReadonlyArray>; export type BettererFilePatterns = ReadonlyArray>; +export type BettererFile = { + readonly key: string; + readonly relativePath: string; + readonly absolutePath: string; + readonly hash: string; + readonly issuesRaw: BettererFileIssuesRaw; + readonly issuesDeserialised: BettererFileIssuesDeserialised; +}; + export type BettererFiles = { readonly filesΔ: ReadonlyArray; getFileΔ(absolutePath: string): BettererFile | void; diff --git a/packages/betterer/src/test/index.ts b/packages/betterer/src/test/index.ts index 9b12669af..4e7bfd259 100644 --- a/packages/betterer/src/test/index.ts +++ b/packages/betterer/src/test/index.ts @@ -1,6 +1,7 @@ -export { BettererFileTest, BettererFile, BettererFiles, isBettererFileTest } from './file-test'; -export { BettererTest, isBettererTest } from './test'; export { + BettererFileTest, + BettererFiles, + isBettererFileTest, BettererFileGlobs, BettererFileIssueDeserialised, BettererFileIssueRaw, @@ -14,8 +15,11 @@ export { BettererFileIssuesSerialised, BettererFilePatterns, BettererFileTestDiff, - BettererFileTestFunction -} from './file-test/types'; + BettererFileTestFunction, + BettererFile, + BettererFilesDiff +} from './file-test'; +export { BettererTest, isBettererTest } from './test'; export { BettererDeserialise, BettererDiffer, @@ -27,5 +31,6 @@ export { BettererTestGoal, BettererTestMap, BettererTestOptionsMap, - BettererTestOptions + BettererTestOptions, + BettererTestStateOptions } from './types'; diff --git a/packages/betterer/src/test/public.ts b/packages/betterer/src/test/public.ts index 2a102638b..a44a5c663 100644 --- a/packages/betterer/src/test/public.ts +++ b/packages/betterer/src/test/public.ts @@ -13,7 +13,8 @@ export { BettererFileTest, BettererFileTestDiff, BettererFileTestFunction, - BettererFiles + BettererFiles, + BettererFilesDiff } from './file-test/public'; export { BettererTest } from './test'; export { diff --git a/packages/betterer/src/test/test.ts b/packages/betterer/src/test/test.ts index 7ce748609..5f4c2ce26 100644 --- a/packages/betterer/src/test/test.ts +++ b/packages/betterer/src/test/test.ts @@ -13,19 +13,23 @@ import { BettererTestState } from './test-state'; const IS_BETTERER_TEST = 'isBettererTest'; -export class BettererTest extends BettererTestState { +export class BettererTest< + DeserialisedType = unknown, + SerialisedType = DeserialisedType, + DiffType = null +> extends BettererTestState { public readonly constraint: BettererTestConstraint; public readonly goal: BettererTestGoal; public readonly test: BettererTestFunction; public readonly deadline: number; - public differ?: BettererDiffer; - public printer?: BettererPrinter; - public serialiser?: BettererSerialiser; + public readonly differ?: BettererDiffer; + public readonly printer?: BettererPrinter; + public readonly serialiser?: BettererSerialiser; - public isBettererTest = IS_BETTERER_TEST; + public readonly isBettererTest = IS_BETTERER_TEST; - constructor(options: BettererTestOptions) { + constructor(options: BettererTestOptions) { super(options.isSkipped, options.isOnly); if (options.constraint == null) { @@ -45,7 +49,7 @@ export class BettererTest): number { + private _createDeadline(options: BettererTestOptions): number { const { deadline } = options; if (deadline == null) { return Infinity; @@ -55,7 +59,7 @@ export class BettererTest + options: BettererTestOptions ): BettererTestGoal { const hasGoal = Object.hasOwnProperty.call(options, 'goal'); if (!hasGoal) { diff --git a/packages/betterer/src/test/types.ts b/packages/betterer/src/test/types.ts index b70cd16bc..9068fca75 100644 --- a/packages/betterer/src/test/types.ts +++ b/packages/betterer/src/test/types.ts @@ -13,9 +13,12 @@ export type BettererTestConstraint = ( export type BettererTestGoal = (result: DeserialisedType) => MaybeAsync; -export type BettererDiffer = (run: BettererRun) => void; +export type BettererDiffer = ( + expected: DeserialisedType, + result: DeserialisedType +) => DiffType; -export type BettererPrinter = (run: BettererRun, serialised: SerialisedType) => MaybeAsync; +export type BettererPrinter = (serialised: SerialisedType) => MaybeAsync; export type BettererSerialise = ( result: DeserialisedType @@ -35,12 +38,12 @@ export type BettererTestStateOptions = { isSkipped?: boolean; }; -export type BettererTestOptions = { +export type BettererTestOptions = { constraint: BettererTestConstraint; deadline?: Date | string; goal?: DeserialisedType | BettererTestGoal; test: BettererTestFunction; - differ?: BettererDiffer; + differ?: BettererDiffer; printer?: BettererPrinter; serialiser?: BettererSerialiser; } & BettererTestStateOptions; diff --git a/packages/extension/src/server/validator.ts b/packages/extension/src/server/validator.ts index b7b4f43de..d6aa48719 100644 --- a/packages/extension/src/server/validator.ts +++ b/packages/extension/src/server/validator.ts @@ -1,10 +1,10 @@ import { - BettererFileTest, BettererFiles, BettererFileIssuesRaw, BettererFileIssueRaw, BettererFileIssueDeserialised, - BettererFileIssues + BettererFileIssues, + BettererFileTestDiff } from '@betterer/betterer'; import * as assert from 'assert'; import { IConnection, TextDocuments, Diagnostic, DiagnosticSeverity, Position } from 'vscode-languageserver'; @@ -65,14 +65,12 @@ export class BettererValidator { runs .filter((run) => !run.isFailed) - .filter((run) => (run.result as BettererFiles).getFileΔ(filePath)) + .filter((run) => (run.result.value as BettererFiles).getFileΔ(filePath)) .map((run) => { - const test = run.test as BettererFileTest; - const files = run.result as BettererFiles; - const file = files.getFileΔ(filePath); + const file = (run.result.value as BettererFiles).getFileΔ(filePath); assert(file); - const fileDiff = test?.diff?.[file.relativePath]; + const fileDiff = (run.diff as BettererFileTestDiff).diff[file.relativePath]; let existingIssues: BettererFileIssues = []; let newIssues: BettererFileIssuesRaw = []; @@ -85,10 +83,10 @@ export class BettererValidator { existingIssues = file.issuesRaw; } existingIssues.forEach((issue: BettererFileIssueRaw | BettererFileIssueDeserialised) => { - diagnostics.push(createWarning(test.name, 'existing issue', issue, document)); + diagnostics.push(createWarning(run.name, 'existing issue', issue, document)); }); newIssues.forEach((issue) => { - diagnostics.push(createError(test.name, 'new issue', issue, document)); + diagnostics.push(createError(run.name, 'new issue', issue, document)); }); }); this._connection.sendDiagnostics({ uri, diagnostics }); diff --git a/packages/reporter/src/reporter.ts b/packages/reporter/src/reporter.ts index ebcf4f6be..b5ce480a9 100644 --- a/packages/reporter/src/reporter.ts +++ b/packages/reporter/src/reporter.ts @@ -105,14 +105,14 @@ export const defaultReporter: BettererReporter = { if (run.isUpdated) { infoΔ(testUpdatedΔ(name)); brΔ(); - run.diff(); + run.diff.log(); brΔ(); return; } if (run.isWorse) { errorΔ(testWorseΔ(name)); brΔ(); - run.diff(); + run.diff.log(); brΔ(); } }