modestbench 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +6 -2
- package/dist/adapters/jest-adapter.cjs +496 -0
- package/dist/adapters/jest-adapter.cjs.map +1 -0
- package/dist/adapters/jest-adapter.d.cts +42 -0
- package/dist/adapters/jest-adapter.d.cts.map +1 -0
- package/dist/adapters/jest-adapter.d.ts +42 -0
- package/dist/adapters/jest-adapter.d.ts.map +1 -0
- package/dist/adapters/jest-adapter.js +459 -0
- package/dist/adapters/jest-adapter.js.map +1 -0
- package/dist/adapters/jest-hooks.cjs +83 -0
- package/dist/adapters/jest-hooks.cjs.map +1 -0
- package/dist/adapters/jest-hooks.d.cts +24 -0
- package/dist/adapters/jest-hooks.d.cts.map +1 -0
- package/dist/adapters/jest-hooks.d.ts +24 -0
- package/dist/adapters/jest-hooks.d.ts.map +1 -0
- package/dist/adapters/jest-hooks.js +78 -0
- package/dist/adapters/jest-hooks.js.map +1 -0
- package/dist/adapters/jest-register.cjs +17 -0
- package/dist/adapters/jest-register.cjs.map +1 -0
- package/dist/adapters/jest-register.d.cts +12 -0
- package/dist/adapters/jest-register.d.cts.map +1 -0
- package/dist/adapters/jest-register.d.ts +12 -0
- package/dist/adapters/jest-register.d.ts.map +1 -0
- package/dist/adapters/jest-register.js +15 -0
- package/dist/adapters/jest-register.js.map +1 -0
- package/dist/adapters/types.cjs.map +1 -1
- package/dist/adapters/types.d.cts +5 -1
- package/dist/adapters/types.d.cts.map +1 -1
- package/dist/adapters/types.d.ts +5 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/cli/commands/run.cjs +17 -11
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.js +9 -3
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/test.cjs +17 -15
- package/dist/cli/commands/test.cjs.map +1 -1
- package/dist/cli/commands/test.d.cts.map +1 -1
- package/dist/cli/commands/test.d.ts.map +1 -1
- package/dist/cli/commands/test.js +5 -3
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/index.cjs +5 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/constants.cjs +1 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +1 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reporters/index.cjs +3 -1
- package/dist/reporters/index.cjs.map +1 -1
- package/dist/reporters/index.d.cts +1 -0
- package/dist/reporters/index.d.cts.map +1 -1
- package/dist/reporters/index.d.ts +1 -0
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +1 -0
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/nyan.cjs +318 -0
- package/dist/reporters/nyan.cjs.map +1 -0
- package/dist/reporters/nyan.d.cts +118 -0
- package/dist/reporters/nyan.d.cts.map +1 -0
- package/dist/reporters/nyan.d.ts +118 -0
- package/dist/reporters/nyan.d.ts.map +1 -0
- package/dist/reporters/nyan.js +314 -0
- package/dist/reporters/nyan.js.map +1 -0
- package/dist/types/core.cjs.map +1 -1
- package/dist/types/core.d.cts +13 -12
- package/dist/types/core.d.cts.map +1 -1
- package/dist/types/core.d.ts +13 -12
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js.map +1 -1
- package/dist/types/index.cjs +0 -2
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +0 -1
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/types/index.js.map +1 -1
- package/package.json +28 -8
- package/src/adapters/jest-adapter.ts +563 -0
- package/src/adapters/jest-hooks.ts +82 -0
- package/src/adapters/jest-register.ts +16 -0
- package/src/adapters/types.ts +5 -1
- package/src/cli/commands/run.ts +10 -3
- package/src/cli/commands/test.ts +5 -3
- package/src/cli/index.ts +10 -2
- package/src/constants.ts +2 -1
- package/src/index.ts +3 -0
- package/src/reporters/index.ts +1 -0
- package/src/reporters/nyan.ts +409 -0
- package/src/types/core.ts +16 -14
- package/src/types/index.ts +0 -3
- package/dist/types/cli.cjs +0 -12
- package/dist/types/cli.cjs.map +0 -1
- package/dist/types/cli.d.cts +0 -75
- package/dist/types/cli.d.cts.map +0 -1
- package/dist/types/cli.d.ts +0 -75
- package/dist/types/cli.d.ts.map +0 -1
- package/dist/types/cli.js +0 -9
- package/dist/types/cli.js.map +0 -1
- package/src/types/cli.ts +0 -82
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Jest Loader Hooks
|
|
3
|
+
*
|
|
4
|
+
* ES module loader hooks that intercept `@jest/globals` imports and return our
|
|
5
|
+
* capturing mock from globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node --import modestbench/jest test-file.js
|
|
8
|
+
*
|
|
9
|
+
* This loader exports async `resolve` and `load` hooks that get registered via
|
|
10
|
+
* module.register() when imported through jest-register.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { LoadHook, ResolveHook } from 'node:module';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the mock module source code
|
|
17
|
+
*
|
|
18
|
+
* Uses top-level await to conditionally get mock or real module. Note: Uses
|
|
19
|
+
* '@jest/globals?passthrough' to bypass our hook when falling back.
|
|
20
|
+
*
|
|
21
|
+
* Security: The globalThis mock is only installed by our own adapter code, so
|
|
22
|
+
* the generated source is safe. No user input is interpolated into this
|
|
23
|
+
* template.
|
|
24
|
+
*/
|
|
25
|
+
const generateMockSource = (): string => `
|
|
26
|
+
const mock = globalThis.__MODESTBENCH_JEST_MOCK__;
|
|
27
|
+
|
|
28
|
+
// If no mock installed, fall through to real @jest/globals
|
|
29
|
+
// The '?passthrough' query tells our hook to not intercept this import
|
|
30
|
+
const source = mock ?? await import('@jest/globals?passthrough');
|
|
31
|
+
|
|
32
|
+
export const describe = source.describe;
|
|
33
|
+
export const fdescribe = source.fdescribe ?? source.describe?.only;
|
|
34
|
+
export const xdescribe = source.xdescribe ?? source.describe?.skip;
|
|
35
|
+
export const test = source.test;
|
|
36
|
+
export const it = source.it ?? source.test;
|
|
37
|
+
export const fit = source.fit ?? source.test?.only;
|
|
38
|
+
export const xit = source.xit ?? source.test?.skip;
|
|
39
|
+
export const xtest = source.xtest ?? source.test?.skip;
|
|
40
|
+
export const expect = source.expect;
|
|
41
|
+
export const jest = source.jest;
|
|
42
|
+
export const beforeAll = source.beforeAll;
|
|
43
|
+
export const afterAll = source.afterAll;
|
|
44
|
+
export const beforeEach = source.beforeEach;
|
|
45
|
+
export const afterEach = source.afterEach;
|
|
46
|
+
export default source;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve hook - intercepts @jest/globals specifier
|
|
51
|
+
*
|
|
52
|
+
* Uses query param '?passthrough' to prevent infinite recursion when falling
|
|
53
|
+
* back to real @jest/globals (when no mock is installed).
|
|
54
|
+
*/
|
|
55
|
+
export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
|
|
56
|
+
// Only intercept bare '@jest/globals', not '@jest/globals?passthrough'
|
|
57
|
+
if (specifier === '@jest/globals') {
|
|
58
|
+
return {
|
|
59
|
+
shortCircuit: true,
|
|
60
|
+
url: 'modestbench://capture/jest',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Strip passthrough query to resolve real @jest/globals
|
|
64
|
+
if (specifier === '@jest/globals?passthrough') {
|
|
65
|
+
return nextResolve('@jest/globals', context);
|
|
66
|
+
}
|
|
67
|
+
return nextResolve(specifier, context);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load hook - returns mock module for our custom URL
|
|
72
|
+
*/
|
|
73
|
+
export const load: LoadHook = async (url, context, nextLoad) => {
|
|
74
|
+
if (url === 'modestbench://capture/jest') {
|
|
75
|
+
return {
|
|
76
|
+
format: 'module',
|
|
77
|
+
shortCircuit: true,
|
|
78
|
+
source: generateMockSource(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return nextLoad(url, context);
|
|
82
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Jest Loader Registration
|
|
3
|
+
*
|
|
4
|
+
* Registers the Jest ESM loader hooks via module.register().
|
|
5
|
+
*
|
|
6
|
+
* Usage: node --import modestbench/jest your-test.js
|
|
7
|
+
*
|
|
8
|
+
* This file registers the hooks module which intercepts '@jest/globals'
|
|
9
|
+
* imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { register } from 'node:module';
|
|
13
|
+
|
|
14
|
+
register('./jest-hooks.js', {
|
|
15
|
+
parentURL: import.meta.url,
|
|
16
|
+
});
|
package/src/adapters/types.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface CapturedSuite {
|
|
|
20
20
|
readonly hooks: SuiteHooks;
|
|
21
21
|
/** Suite name */
|
|
22
22
|
readonly name: string;
|
|
23
|
+
/** Whether this suite is marked .only */
|
|
24
|
+
readonly only?: boolean;
|
|
25
|
+
/** Whether this suite is marked .skip */
|
|
26
|
+
readonly skip?: boolean;
|
|
23
27
|
/** Tests in this suite */
|
|
24
28
|
readonly tests: CapturedTest[];
|
|
25
29
|
}
|
|
@@ -79,7 +83,7 @@ export interface SuiteHooks {
|
|
|
79
83
|
/**
|
|
80
84
|
* Supported test frameworks
|
|
81
85
|
*/
|
|
82
|
-
export type TestFramework = 'ava' | 'mocha' | 'node-test';
|
|
86
|
+
export type TestFramework = 'ava' | 'jest' | 'mocha' | 'node-test';
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* Interface for test framework adapters
|
package/src/cli/commands/run.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
import type { BenchmarkRun, ModestBenchConfig } from '../../types/index.js';
|
|
11
11
|
import type { CliContext } from '../index.js';
|
|
12
12
|
|
|
13
|
-
import { ErrorCodes } from '../../constants.js';
|
|
13
|
+
import { ErrorCodes, ExitCodes } from '../../constants.js';
|
|
14
14
|
import { resolveOutputPath } from '../../core/output-path-resolver.js';
|
|
15
15
|
import {
|
|
16
16
|
type BudgetExceededError,
|
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
import { CsvReporter } from '../../reporters/csv.js';
|
|
21
21
|
import { HumanReporter } from '../../reporters/human.js';
|
|
22
22
|
import { JsonReporter } from '../../reporters/json.js';
|
|
23
|
+
import { NyanReporter } from '../../reporters/nyan.js';
|
|
23
24
|
import { SimpleReporter } from '../../reporters/simple.js';
|
|
24
|
-
import { ExitCodes } from '../../types/cli.js';
|
|
25
25
|
import { hasErrorCode, isError } from '../../utils/type-guards.js';
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -395,7 +395,7 @@ const setupReporters = (
|
|
|
395
395
|
: undefined;
|
|
396
396
|
|
|
397
397
|
// Built-in reporter names for error messages
|
|
398
|
-
const builtInReporters = ['human', 'json', 'csv', 'simple'];
|
|
398
|
+
const builtInReporters = ['human', 'json', 'csv', 'nyan', 'simple'];
|
|
399
399
|
|
|
400
400
|
for (const reporterName of requestedReporters) {
|
|
401
401
|
let reporter;
|
|
@@ -440,6 +440,13 @@ const setupReporters = (
|
|
|
440
440
|
break;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
case 'nyan':
|
|
444
|
+
reporter = new NyanReporter({
|
|
445
|
+
color: true,
|
|
446
|
+
quiet: explicitQuiet,
|
|
447
|
+
});
|
|
448
|
+
break;
|
|
449
|
+
|
|
443
450
|
case 'simple':
|
|
444
451
|
reporter = new SimpleReporter({
|
|
445
452
|
quiet: explicitQuiet,
|
package/src/cli/commands/test.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* executing them through a lightweight benchmark runner.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { hostname } from 'node:os';
|
|
9
|
-
import { cpus, freemem, totalmem } from 'node:os';
|
|
8
|
+
import { cpus, freemem, hostname, totalmem } from 'node:os';
|
|
10
9
|
import { resolve } from 'node:path';
|
|
11
10
|
import { performance } from 'node:perf_hooks';
|
|
12
11
|
|
|
@@ -21,6 +20,7 @@ import type {
|
|
|
21
20
|
import type { CliContext } from '../index.js';
|
|
22
21
|
|
|
23
22
|
import { AvaAdapter } from '../../adapters/ava-adapter.js';
|
|
23
|
+
import { JestAdapter } from '../../adapters/jest-adapter.js';
|
|
24
24
|
import { MochaAdapter } from '../../adapters/mocha-adapter.js';
|
|
25
25
|
import { NodeTestAdapter } from '../../adapters/node-test-adapter.js';
|
|
26
26
|
import {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type ConvertedBenchmarkSuite,
|
|
31
31
|
type TestFramework,
|
|
32
32
|
} from '../../adapters/types.js';
|
|
33
|
-
import { ExitCodes } from '../../
|
|
33
|
+
import { ExitCodes } from '../../constants.js';
|
|
34
34
|
import { createRunId } from '../../types/core.js';
|
|
35
35
|
import { isError } from '../../utils/type-guards.js';
|
|
36
36
|
|
|
@@ -512,6 +512,8 @@ const selectAdapter = (framework: TestFramework) => {
|
|
|
512
512
|
switch (framework) {
|
|
513
513
|
case 'ava':
|
|
514
514
|
return new AvaAdapter();
|
|
515
|
+
case 'jest':
|
|
516
|
+
return new JestAdapter();
|
|
515
517
|
case 'mocha':
|
|
516
518
|
return new MochaAdapter();
|
|
517
519
|
case 'node-test':
|
package/src/cli/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
CsvReporter,
|
|
40
40
|
HumanReporter,
|
|
41
41
|
JsonReporter,
|
|
42
|
+
NyanReporter,
|
|
42
43
|
SimpleReporter,
|
|
43
44
|
} from '../reporters/index.js';
|
|
44
45
|
// Import commands
|
|
@@ -993,11 +994,11 @@ export const main = async (
|
|
|
993
994
|
)
|
|
994
995
|
.command(
|
|
995
996
|
'test <framework> [files..]',
|
|
996
|
-
'Run test files as benchmarks (captures tests from Mocha, node:test, or AVA)',
|
|
997
|
+
'Run test files as benchmarks (captures tests from Jest, Mocha, node:test, or AVA)',
|
|
997
998
|
(yargs) => {
|
|
998
999
|
return yargs
|
|
999
1000
|
.positional('framework', {
|
|
1000
|
-
choices: ['mocha', 'node-test'
|
|
1001
|
+
choices: ['ava', 'jest', 'mocha', 'node-test'] as const,
|
|
1001
1002
|
demandOption: true,
|
|
1002
1003
|
describe: 'Test framework to use',
|
|
1003
1004
|
nargs: 1,
|
|
@@ -1151,6 +1152,13 @@ const createCliContext = async (
|
|
|
1151
1152
|
}),
|
|
1152
1153
|
);
|
|
1153
1154
|
|
|
1155
|
+
engine.registerReporter(
|
|
1156
|
+
'nyan',
|
|
1157
|
+
new NyanReporter({
|
|
1158
|
+
color: !options.noColor,
|
|
1159
|
+
}),
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1154
1162
|
return {
|
|
1155
1163
|
abortController,
|
|
1156
1164
|
configManager: engine.configManager,
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Engine } from './types/
|
|
1
|
+
import { type Engine } from './types/core.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Supported benchmark file extensions
|
|
@@ -60,6 +60,7 @@ export const Reporters = {
|
|
|
60
60
|
CSV: 'csv',
|
|
61
61
|
HUMAN: 'human',
|
|
62
62
|
JSON: 'json',
|
|
63
|
+
NYAN: 'nyan',
|
|
63
64
|
SIMPLE: 'simple',
|
|
64
65
|
} as const;
|
|
65
66
|
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
export { bootstrap as modestbench } from './bootstrap.js';
|
|
10
10
|
|
|
11
|
+
// Constants
|
|
12
|
+
export { ExitCodes } from './constants.js';
|
|
13
|
+
|
|
11
14
|
// Core engine
|
|
12
15
|
export { ModestBenchEngine } from './core/engine.js';
|
|
13
16
|
export { AccurateEngine, TinybenchEngine } from './core/engines/index.js';
|
package/src/reporters/index.ts
CHANGED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Nyan Cat Reporter
|
|
3
|
+
*
|
|
4
|
+
* Because benchmarking should be more colorful. Displays an animated nyan cat
|
|
5
|
+
* flying through a rainbow trail as benchmarks complete.
|
|
6
|
+
*
|
|
7
|
+
* Based on Mocha's legendary nyan reporter, adapted for the glory of
|
|
8
|
+
* performance measurement.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BenchmarkRun,
|
|
15
|
+
FileResult,
|
|
16
|
+
SuiteResult,
|
|
17
|
+
TaskResult,
|
|
18
|
+
} from '../types/index.js';
|
|
19
|
+
|
|
20
|
+
import { BaseReporter } from '../services/reporter-registry.js';
|
|
21
|
+
import { colors } from '../utils/ansi.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Nyan Cat reporter - because your benchmarks deserve rainbow power
|
|
25
|
+
*/
|
|
26
|
+
export class NyanReporter extends BaseReporter {
|
|
27
|
+
/** Index into rainbow colors for cycling */
|
|
28
|
+
private colorIndex = 0;
|
|
29
|
+
|
|
30
|
+
/** Current file being processed */
|
|
31
|
+
private currentFile = '';
|
|
32
|
+
|
|
33
|
+
/** Current suite being processed */
|
|
34
|
+
private currentSuite = '';
|
|
35
|
+
|
|
36
|
+
/** Total failed tasks */
|
|
37
|
+
private failed = 0;
|
|
38
|
+
|
|
39
|
+
/** Collected failures for summary */
|
|
40
|
+
private failures: Array<{
|
|
41
|
+
error: string;
|
|
42
|
+
file: string;
|
|
43
|
+
suite: string;
|
|
44
|
+
task: string;
|
|
45
|
+
}> = [];
|
|
46
|
+
|
|
47
|
+
/** Number of lines the cat occupies */
|
|
48
|
+
private readonly numberOfLines = 4;
|
|
49
|
+
|
|
50
|
+
/** Total passed tasks */
|
|
51
|
+
private passed = 0;
|
|
52
|
+
|
|
53
|
+
/** Whether the display is active */
|
|
54
|
+
private progressActive = false;
|
|
55
|
+
|
|
56
|
+
/** Quiet mode - suppress output */
|
|
57
|
+
private readonly quiet: boolean;
|
|
58
|
+
|
|
59
|
+
/** Generated rainbow color palette */
|
|
60
|
+
private rainbowColors: number[] = [];
|
|
61
|
+
|
|
62
|
+
/** Width of scoreboard */
|
|
63
|
+
private readonly scoreboardWidth = 5;
|
|
64
|
+
|
|
65
|
+
/** Start time for duration calculation */
|
|
66
|
+
private startTime = 0;
|
|
67
|
+
|
|
68
|
+
/** Animation tick (alternates between frames) */
|
|
69
|
+
private tick = false;
|
|
70
|
+
|
|
71
|
+
/** Rainbow trail storage - one trajectory per cat line */
|
|
72
|
+
private trajectories: string[][] = [[], [], [], []];
|
|
73
|
+
|
|
74
|
+
/** Maximum width of the rainbow trail */
|
|
75
|
+
private trajectoryWidthMax = 0;
|
|
76
|
+
|
|
77
|
+
/** Whether to use colors */
|
|
78
|
+
private readonly useColor: boolean;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
options: {
|
|
82
|
+
color?: boolean;
|
|
83
|
+
quiet?: boolean;
|
|
84
|
+
} = {},
|
|
85
|
+
) {
|
|
86
|
+
super('nyan', options);
|
|
87
|
+
|
|
88
|
+
this.quiet = options.quiet ?? false;
|
|
89
|
+
|
|
90
|
+
// Auto-detect color support if not explicitly set
|
|
91
|
+
this.useColor =
|
|
92
|
+
options.color ??
|
|
93
|
+
(process.stdout.isTTY &&
|
|
94
|
+
process.env.FORCE_COLOR !== '0' &&
|
|
95
|
+
process.env.NO_COLOR == null);
|
|
96
|
+
|
|
97
|
+
// Generate the rainbow colors on construction
|
|
98
|
+
this.rainbowColors = this.generateColors();
|
|
99
|
+
|
|
100
|
+
// Calculate trajectory width based on terminal width
|
|
101
|
+
// Leave room for scoreboard (5) + cat (11) + some padding
|
|
102
|
+
const termWidth = process.stdout.columns || 80;
|
|
103
|
+
const nyanCatWidth = 11;
|
|
104
|
+
this.trajectoryWidthMax = Math.floor(termWidth * 0.75) - nyanCatWidth;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onEnd(run: BenchmarkRun): void {
|
|
108
|
+
if (this.quiet) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Show cursor and move past the cat
|
|
113
|
+
this.showCursor();
|
|
114
|
+
for (let i = 0; i < this.numberOfLines; i++) {
|
|
115
|
+
process.stdout.write('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Print summary
|
|
119
|
+
this.printEpilogue(run);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onError(error: Error): void {
|
|
123
|
+
if (this.quiet) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Make sure cursor is visible
|
|
128
|
+
this.showCursor();
|
|
129
|
+
|
|
130
|
+
console.error(`\n${colors.red}Error: ${error.message}${colors.reset}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onFileEnd(_result: FileResult): void {
|
|
134
|
+
// Just keep flying
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onFileStart(file: string): void {
|
|
138
|
+
this.currentFile = file;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onStart(_run: BenchmarkRun): void {
|
|
142
|
+
this.startTime = Date.now();
|
|
143
|
+
this.passed = 0;
|
|
144
|
+
this.failed = 0;
|
|
145
|
+
this.failures = [];
|
|
146
|
+
this.colorIndex = 0;
|
|
147
|
+
this.tick = false;
|
|
148
|
+
this.trajectories = [[], [], [], []];
|
|
149
|
+
|
|
150
|
+
if (this.quiet) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Hide cursor for cleaner animation
|
|
155
|
+
this.hideCursor();
|
|
156
|
+
|
|
157
|
+
// Initial draw
|
|
158
|
+
this.draw();
|
|
159
|
+
this.progressActive = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onSuiteEnd(_result: SuiteResult): void {
|
|
163
|
+
// Keep flying
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onSuiteStart(suite: string): void {
|
|
167
|
+
this.currentSuite = suite;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onTaskResult(result: TaskResult): void {
|
|
171
|
+
if (result.error) {
|
|
172
|
+
this.failed++;
|
|
173
|
+
this.failures.push({
|
|
174
|
+
error: result.error.message || String(result.error),
|
|
175
|
+
file: this.currentFile,
|
|
176
|
+
suite: this.currentSuite,
|
|
177
|
+
task: result.name,
|
|
178
|
+
});
|
|
179
|
+
} else if (!result.aborted) {
|
|
180
|
+
this.passed++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.quiet) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.draw();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onTaskStart(_task: string): void {
|
|
191
|
+
// The cat flies on results, not starts
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Append a segment to the rainbow trail
|
|
196
|
+
*/
|
|
197
|
+
private appendRainbow(): void {
|
|
198
|
+
const segment = this.tick ? '_' : '-';
|
|
199
|
+
const rainbowified = this.rainbowify(segment);
|
|
200
|
+
|
|
201
|
+
for (let index = 0; index < this.numberOfLines; index++) {
|
|
202
|
+
const trajectory = this.trajectories[index]!;
|
|
203
|
+
if (trajectory.length >= this.trajectoryWidthMax) {
|
|
204
|
+
trajectory.shift();
|
|
205
|
+
}
|
|
206
|
+
trajectory.push(rainbowified);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Move cursor down n lines
|
|
212
|
+
*/
|
|
213
|
+
private cursorDown(n: number): void {
|
|
214
|
+
process.stdout.write(`\x1b[${n}B`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Move cursor up n lines
|
|
219
|
+
*/
|
|
220
|
+
private cursorUp(n: number): void {
|
|
221
|
+
process.stdout.write(`\x1b[${n}A`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Draw the complete nyan cat scene
|
|
226
|
+
*/
|
|
227
|
+
private draw(): void {
|
|
228
|
+
this.appendRainbow();
|
|
229
|
+
this.drawScoreboard();
|
|
230
|
+
this.drawRainbow();
|
|
231
|
+
this.drawNyanCat();
|
|
232
|
+
this.tick = !this.tick;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Draw the nyan cat ASCII art
|
|
237
|
+
*/
|
|
238
|
+
private drawNyanCat(): void {
|
|
239
|
+
const startWidth = this.scoreboardWidth + this.trajectories[0]!.length;
|
|
240
|
+
const dist = `\x1b[${startWidth}C`;
|
|
241
|
+
|
|
242
|
+
process.stdout.write(dist);
|
|
243
|
+
process.stdout.write('_,------,');
|
|
244
|
+
process.stdout.write('\n');
|
|
245
|
+
|
|
246
|
+
process.stdout.write(dist);
|
|
247
|
+
const padding1 = this.tick ? ' ' : ' ';
|
|
248
|
+
process.stdout.write(`_|${padding1}/\\_/\\ `);
|
|
249
|
+
process.stdout.write('\n');
|
|
250
|
+
|
|
251
|
+
process.stdout.write(dist);
|
|
252
|
+
const padding2 = this.tick ? '_' : '__';
|
|
253
|
+
const tail = this.tick ? '~' : '^';
|
|
254
|
+
process.stdout.write(`${tail}|${padding2}${this.face()} `);
|
|
255
|
+
process.stdout.write('\n');
|
|
256
|
+
|
|
257
|
+
process.stdout.write(dist);
|
|
258
|
+
const padding3 = this.tick ? ' ' : ' ';
|
|
259
|
+
process.stdout.write(`${padding3}"" "" `);
|
|
260
|
+
process.stdout.write('\n');
|
|
261
|
+
|
|
262
|
+
this.cursorUp(this.numberOfLines);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Draw the rainbow trail
|
|
267
|
+
*/
|
|
268
|
+
private drawRainbow(): void {
|
|
269
|
+
for (const line of this.trajectories) {
|
|
270
|
+
process.stdout.write(`\x1b[${this.scoreboardWidth}C`);
|
|
271
|
+
process.stdout.write(line.join(''));
|
|
272
|
+
process.stdout.write('\n');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.cursorUp(this.numberOfLines);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Draw the scoreboard showing pass/fail counts
|
|
280
|
+
*/
|
|
281
|
+
private drawScoreboard(): void {
|
|
282
|
+
const draw = (type: 'green' | 'red', n: number) => {
|
|
283
|
+
process.stdout.write(' ');
|
|
284
|
+
if (this.useColor) {
|
|
285
|
+
process.stdout.write(`${colors[type]}${n}${colors.reset}`);
|
|
286
|
+
} else {
|
|
287
|
+
process.stdout.write(String(n));
|
|
288
|
+
}
|
|
289
|
+
process.stdout.write('\n');
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
draw('green', this.passed);
|
|
293
|
+
draw('red', this.failed);
|
|
294
|
+
process.stdout.write('\n');
|
|
295
|
+
process.stdout.write('\n');
|
|
296
|
+
|
|
297
|
+
this.cursorUp(this.numberOfLines);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the nyan cat's face based on current state
|
|
302
|
+
*/
|
|
303
|
+
private face(): string {
|
|
304
|
+
if (this.failed > 0) {
|
|
305
|
+
return '( x .x)';
|
|
306
|
+
} else if (this.passed > 0) {
|
|
307
|
+
return '( ^ .^)';
|
|
308
|
+
}
|
|
309
|
+
return '( - .-)';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate rainbow colors using sine wave color cycling
|
|
314
|
+
*
|
|
315
|
+
* Uses 256-color palette (colors 16-231 are a 6x6x6 color cube)
|
|
316
|
+
*/
|
|
317
|
+
private generateColors(): number[] {
|
|
318
|
+
const colorList: number[] = [];
|
|
319
|
+
|
|
320
|
+
// Generate 42 colors (6 * 7) cycling through the spectrum
|
|
321
|
+
for (let i = 0; i < 6 * 7; i++) {
|
|
322
|
+
const pi3 = Math.floor(Math.PI / 3);
|
|
323
|
+
const n = i * (1.0 / 6);
|
|
324
|
+
const r = Math.floor(3 * Math.sin(n) + 3);
|
|
325
|
+
const g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3);
|
|
326
|
+
const b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3);
|
|
327
|
+
// Calculate 256-color code from RGB values (16 + 36*r + 6*g + b)
|
|
328
|
+
colorList.push(36 * r + 6 * g + b + 16);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return colorList;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Hide the cursor
|
|
336
|
+
*/
|
|
337
|
+
private hideCursor(): void {
|
|
338
|
+
if (process.stdout.isTTY) {
|
|
339
|
+
process.stdout.write('\x1b[?25l');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Print the epilogue summary after the run
|
|
345
|
+
*/
|
|
346
|
+
private printEpilogue(run: BenchmarkRun): void {
|
|
347
|
+
const duration = Date.now() - this.startTime;
|
|
348
|
+
const durationStr = BaseReporter.formatDuration(duration * 1000000);
|
|
349
|
+
|
|
350
|
+
console.log();
|
|
351
|
+
console.log(
|
|
352
|
+
` ${this.useColor ? colors.green : ''}${this.passed} passing${this.useColor ? colors.reset : ''} ${this.useColor ? colors.gray : ''}(${durationStr})${this.useColor ? colors.reset : ''}`,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (this.failed > 0) {
|
|
356
|
+
console.log(
|
|
357
|
+
` ${this.useColor ? colors.red : ''}${this.failed} failing${this.useColor ? colors.reset : ''}`,
|
|
358
|
+
);
|
|
359
|
+
console.log();
|
|
360
|
+
|
|
361
|
+
// Print failure details
|
|
362
|
+
for (let i = 0; i < this.failures.length; i++) {
|
|
363
|
+
const failure = this.failures[i]!;
|
|
364
|
+
console.log(
|
|
365
|
+
` ${i + 1}) ${failure.suite === 'default' ? '' : failure.suite + ' > '}${failure.task}`,
|
|
366
|
+
);
|
|
367
|
+
console.log(
|
|
368
|
+
` ${this.useColor ? colors.red : ''}${failure.error}${this.useColor ? colors.reset : ''}`,
|
|
369
|
+
);
|
|
370
|
+
console.log();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Show total files/suites for context
|
|
375
|
+
let totalSuites = 0;
|
|
376
|
+
for (const file of run.files) {
|
|
377
|
+
totalSuites += file.suites.length;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log();
|
|
381
|
+
console.log(
|
|
382
|
+
` ${this.useColor ? colors.gray : ''}Files: ${run.files.length} | Suites: ${totalSuites} | Tasks: ${this.passed + this.failed}${this.useColor ? colors.reset : ''}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Apply rainbow coloring to a string
|
|
388
|
+
*/
|
|
389
|
+
private rainbowify(str: string): string {
|
|
390
|
+
if (!this.useColor) {
|
|
391
|
+
return str;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const color =
|
|
395
|
+
this.rainbowColors[this.colorIndex % this.rainbowColors.length]!;
|
|
396
|
+
this.colorIndex += 1;
|
|
397
|
+
|
|
398
|
+
return `\x1b[38;5;${color}m${str}\x1b[0m`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Show the cursor
|
|
403
|
+
*/
|
|
404
|
+
private showCursor(): void {
|
|
405
|
+
if (process.stdout.isTTY) {
|
|
406
|
+
process.stdout.write('\x1b[?25h');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|