Skip to content

Commit

Permalink
refactor(betterer 🔧): try to make rendering more predictable
Browse files Browse the repository at this point in the history
  • Loading branch information
phenomnomnominal committed Sep 15, 2024
1 parent d26c395 commit 37ed294
Show file tree
Hide file tree
Showing 86 changed files with 749 additions and 830 deletions.
9 changes: 4 additions & 5 deletions goldens/api/betterer.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface BettererConfigWatcher {
export interface BettererContext {
readonly config: BettererConfig;
options(optionsOverride: BettererOptionsOverride): Promise<void>;
stop(): Promise<BettererSuiteSummary>;
stop(): Promise<BettererContextSummary>;
}

// @public
Expand Down Expand Up @@ -426,11 +426,10 @@ export interface BettererRun {
}

// @public
export interface BettererRunner {
options(optionsOverride: BettererOptionsOverride): Promise<void>;
export interface BettererRunner extends BettererContext {
queue(filePaths?: string | BettererFilePaths): Promise<void>;
stop(): Promise<BettererSuiteSummary>;
stop(force: true): Promise<BettererSuiteSummary | null>;
stop(): Promise<BettererContextSummary>;
stop(force?: true): Promise<BettererContextSummary | null>;
}

// @public
Expand Down
4 changes: 3 additions & 1 deletion goldens/api/render.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { memo } from 'react';
import { process as process_2 } from 'process';
import { PropsWithChildren } from 'react';
import R from 'react';
import { default as React_3 } from 'react';
import { render } from 'ink';
import { RenderOptions } from 'ink';
import { Text as Text_2 } from 'ink';
import { TextProps } from 'ink';
import TI from 'ink-text-input';
import { useApp } from 'ink';
Expand Down Expand Up @@ -62,6 +62,8 @@ export { render }

export { RenderOptions }

// @internal
const Text_2: React_3.FC<TextProps>;
export { Text_2 as Text }

// @internal
Expand Down
3 changes: 2 additions & 1 deletion packages/betterer/src/api/betterer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { watch } from './watch.js';
*/
export const betterer = async function betterer(options: BettererOptions = {}): Promise<BettererSuiteSummary> {
const runner = await BettererRunnerΩ.create(options);
return await runner.run();
const contextSummary = await runner.run();
return contextSummary.lastSuite;
};

betterer.merge = merge;
Expand Down
4 changes: 3 additions & 1 deletion packages/betterer/src/context/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateStringRegExpArray,
validateWorkers
} from '../config/index.js';
import { getGlobals } from '../globals.js';

export async function createContextConfig(options: BettererOptionsContext): Promise<BettererConfigContext> {
const ci = options.ci ?? false;
Expand Down Expand Up @@ -41,8 +42,9 @@ export async function createContextConfig(options: BettererOptionsContext): Prom
};
}

export function overrideContextConfig(config: BettererConfig, optionsOverride: BettererOptionsContextOverride): void {
export function overrideContextConfig(optionsOverride: BettererOptionsContextOverride): void {
if (optionsOverride.filters) {
const { config } = getGlobals();
validateStringRegExpArray({ filters: optionsOverride.filters });
config.filters = toRegExps(toArray<string | RegExp>(optionsOverride.filters));
}
Expand Down
15 changes: 10 additions & 5 deletions packages/betterer/src/context/context-summary.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { BettererError } from '@betterer/errors';
import type { BettererConfig } from '../config/index.js';
import type { BettererSuiteSummaries, BettererSuiteSummary } from '../suite/index.js';
import type { BettererContextSummary } from './types.js';

import { BettererError } from '@betterer/errors';

import { getGlobals } from '../globals.js';

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

constructor(public readonly suites: BettererSuiteSummaries) {
const { config } = getGlobals();
this.config = config;
}

public get lastSuite(): BettererSuiteSummary {
const suite = this.suites[this.suites.length - 1];
Expand Down
218 changes: 93 additions & 125 deletions packages/betterer/src/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,167 +1,135 @@
import type { BettererError } from '@betterer/errors';
import { BettererError } from '@betterer/errors';

import type { BettererConfig, BettererOptionsOverride } from '../config/index.js';
import type { BettererFilePaths } from '../fs/index.js';
import type { BettererReporterΩ } from '../reporters/index.js';
import type { BettererSuiteSummaries, BettererSuiteSummary, BettererSuiteSummaryΩ } from '../suite/index.js';
import type { BettererContext, BettererContextStarted, BettererContextSummary } from './types.js';
import type {
BettererSuite,
BettererSuites,
BettererSuiteSummaries,
BettererSuiteSummary,
BettererSuiteSummaryΩ
} from '../suite/index.js';
import type { BettererContext, BettererContextSummary } from './types.js';

import { overrideContextConfig } from '../context/index.js';
import { BettererFileResolverΩ } from '../fs/index.js';
import type { BettererFileResolverΩ } from '../fs/index.js';
import { overrideReporterConfig } from '../reporters/index.js';
import { overrideWatchConfig } from '../runner/index.js';
import { BettererSuiteΩ } from '../suite/index.js';
import { defer } from '../utils.js';
import { BettererContextSummaryΩ } from './context-summary.js';
import { getGlobals } from '../globals.js';
import { BettererContextSummaryΩ } from './context-summary.js';

export class BettererContextΩ implements BettererContext {
public readonly config: BettererConfig;

private _started: BettererContextStarted;
private _suites: BettererSuites = [];
private _suiteSummaries: BettererSuiteSummaries = [];

constructor() {
const { config } = getGlobals();
this.config = config;
this._started = this._start();
}

public get lastSuite(): BettererSuite {
const suite = this._suites[this._suites.length - 1];
if (!suite) {
throw new BettererError(`Context has not started a suite run yet! ❌`);
}
return suite;
}

public async options(optionsOverride: BettererOptionsOverride): Promise<void> {
// Wait for any pending run to finish, and any existing reporter to render:
await this._started.end();

const { config } = getGlobals();
let lastSuiteΩ: BettererSuiteΩ | null = null;
try {
const lastSuite = this.lastSuite;
lastSuiteΩ = lastSuite as BettererSuiteΩ;
await lastSuiteΩ.lifecycle.promise;
} catch {
// It's okay if there's not a pending suite!
}

// Override the config:
overrideContextConfig(config, optionsOverride);
await overrideReporterConfig(config, optionsOverride);
overrideWatchConfig(config, optionsOverride);

// Start everything again, and trigger a new reporter:
this._started = this._start();

// eslint-disable-next-line @typescript-eslint/no-misused-promises -- SIGTERM doesn't care about Promises
process.on('SIGTERM', () => this.stop());
}
overrideContextConfig(optionsOverride);
await overrideReporterConfig(optionsOverride);
overrideWatchConfig(optionsOverride);

public async runOnce(): Promise<void> {
await this.run([], true);
if (lastSuiteΩ) {
// Run the tests again, with all the new options:
void this.run(lastSuiteΩ.filePaths, false);
}
}

public async run(specifiedFilePaths: BettererFilePaths, isRunOnce = false): Promise<void> {
try {
const { config, results, versionControl } = getGlobals();

await versionControl.api.sync();

const { cwd, ci, includes, excludes, reporter } = config;
const reporterΩ = reporter as BettererReporterΩ;

const resolver = new BettererFileResolverΩ(cwd, versionControl);
resolver.include(...includes);
resolver.exclude(...excludes);

const hasSpecifiedFiles = specifiedFilePaths.length > 0;
const hasGlobalIncludesExcludes = includes.length || excludes.length;

let filePaths: BettererFilePaths;
if (hasSpecifiedFiles && hasGlobalIncludesExcludes) {
// Validate specified files based on global `includes`/`excludes and gitignore rules:
filePaths = await resolver.validate(specifiedFilePaths);
} else if (hasSpecifiedFiles) {
// Validate specified files based on gitignore rules:
filePaths = await resolver.validate(specifiedFilePaths);
} else if (hasGlobalIncludesExcludes) {
// Resolve files based on global `includes`/`excludes and gitignore rules:
filePaths = await resolver.files();
} else {
// When `filePaths` is `[]` the test will use its specific resolver:
filePaths = [];
}

const suite = await BettererSuiteΩ.create(filePaths);
const suiteLifecycle = defer<BettererSuiteSummary>();
public async run(specifiedFilePaths: BettererFilePaths, isRunOnce = false): Promise<BettererSuiteSummary> {
const { config, reporter, resolvers, results } = getGlobals();
const resolver = resolvers.cwd as BettererFileResolverΩ;

const { includes, excludes } = config;
resolver.include(...includes);
resolver.exclude(...excludes);

const hasSpecifiedFiles = specifiedFilePaths.length > 0;
const hasGlobalIncludesExcludes = includes.length || excludes.length;

let filePaths: BettererFilePaths;
if (hasSpecifiedFiles && hasGlobalIncludesExcludes) {
// Validate specified files based on global `includes`/`excludes and gitignore rules:
filePaths = await resolver.validate(specifiedFilePaths);
} else if (hasSpecifiedFiles) {
// Validate specified files based on gitignore rules:
filePaths = await resolver.validate(specifiedFilePaths);
} else if (hasGlobalIncludesExcludes) {
// Resolve files based on global `includes`/`excludes and gitignore rules:
filePaths = await resolver.files();
} else {
// When `filePaths` is `[]` the test will use its specific resolver:
filePaths = [];
}

// Don't await here! A custom reporter could be awaiting
// the lifecycle promise which is unresolved right now!
const reportSuiteStart = reporterΩ.suiteStart(suite, suiteLifecycle.promise);
try {
const suiteSummary = await suite.run();
const suiteΩ = await BettererSuiteΩ.create(filePaths);
this._suites.push(suiteΩ);

if (!isRunOnce && !ci) {
const suiteSummaryΩ = suiteSummary as BettererSuiteSummaryΩ;
await results.api.write(suiteSummaryΩ.result);
}
const reporterΩ = reporter as BettererReporterΩ;

this._suiteSummaries = [...this._suiteSummaries, suiteSummary];
// Don't await here! A custom reporter could be awaiting
// the lifecycle promise which is unresolved right now!
const reportSuiteStart = reporterΩ.suiteStart(suiteΩ, suiteΩ.lifecycle.promise);
try {
const suiteSummary = await suiteΩ.run();
this._suiteSummaries = [...this._suiteSummaries, suiteSummary];

// Lifecycle promise is resolved, so it's safe to finally await
// the result of `reporter.suiteStart`:
suiteLifecycle.resolve(suiteSummary);
await reportSuiteStart;
if (!isRunOnce && !config.ci) {
const suiteSummaryΩ = suiteSummary as BettererSuiteSummaryΩ;
await results.api.write(suiteSummaryΩ.result);
}

await reporterΩ.suiteEnd(suiteSummary);
} catch (error) {
// Lifecycle promise is rejected, so it's safe to finally await
// the result of `reporter.suiteStart`:
suiteLifecycle.reject(error as BettererError);
await reportSuiteStart;
// Lifecycle promise is resolved, so it's safe to await
// the result of `reporter.suiteStart`:
suiteΩ.lifecycle.resolve(suiteSummary);
await reportSuiteStart;

await reporterΩ.suiteError(suite, error as BettererError);
}
await reporterΩ.suiteEnd(suiteSummary);
return suiteSummary;
} catch (error) {
await this._started.error(error as BettererError);
// Lifecycle promise is rejected, so it's safe to await
// the result of `reporter.suiteStart`:
suiteΩ.lifecycle.reject(error as BettererError);
await reportSuiteStart;

await reporterΩ.suiteError(suiteΩ, error as BettererError);
throw error;
}
}

public async stop(): Promise<BettererSuiteSummary> {
const { lastSuite } = await this._started.end();
return lastSuite;
}

private _start(): BettererContextStarted {
const { config, results, versionControl } = getGlobals();

// Update `reporterΩ` here because `this.options()` may have been called:
const reporterΩ = config.reporter as BettererReporterΩ;

const contextLifecycle = defer<BettererContextSummary>();
public async stop(): Promise<BettererContextSummary> {
try {
const lastSuiteΩ = this.lastSuite as BettererSuiteΩ;
await lastSuiteΩ.lifecycle.promise;
} catch {
//
}

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

// Lifecycle promise is resolved, so it's safe to finally await
// the result of `reporter.contextStart`:
contextLifecycle.resolve(contextSummary);
await reportContextStart;

await reporterΩ.contextEnd(contextSummary);

const suiteSummaryΩ = contextSummary.lastSuite as BettererSuiteSummaryΩ;
if (!config.ci) {
const didWrite = await results.api.write(suiteSummaryΩ.result);
if (didWrite && config.precommit) {
await versionControl.api.add(config.resultsPath);
}
}
await versionControl.api.writeCache();

return contextSummary;
},
error: async (error: BettererError): Promise<void> => {
// Lifecycle promise is rejected, so it's safe to finally await
// the result of `reporter.contextStart`:
contextLifecycle.reject(error);
await reportContextStart;

await reporterΩ.contextError(this, error);
}
};
return new BettererContextSummaryΩ(this._suiteSummaries);
}
}
8 changes: 5 additions & 3 deletions packages/betterer/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export { createContextConfig, enableMode, overrideContextConfig } from './config.js';
export { BettererContextΩ } from './context.js';
export {
export type {
BettererConfigContext,
BettererConfigExcludes,
BettererConfigFilters,
Expand All @@ -20,3 +18,7 @@ export {
BettererOptionsModeUpdate,
BettererOptionsModeWatch
} from './types.js';

export { createContextConfig, enableMode, overrideContextConfig } from './config.js';
export { BettererContextSummaryΩ } from './context-summary.js';
export { BettererContextΩ } from './context.js';
Loading

0 comments on commit 37ed294

Please sign in to comment.