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.
Files changed (117) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -2
  3. package/dist/adapters/jest-adapter.cjs +496 -0
  4. package/dist/adapters/jest-adapter.cjs.map +1 -0
  5. package/dist/adapters/jest-adapter.d.cts +42 -0
  6. package/dist/adapters/jest-adapter.d.cts.map +1 -0
  7. package/dist/adapters/jest-adapter.d.ts +42 -0
  8. package/dist/adapters/jest-adapter.d.ts.map +1 -0
  9. package/dist/adapters/jest-adapter.js +459 -0
  10. package/dist/adapters/jest-adapter.js.map +1 -0
  11. package/dist/adapters/jest-hooks.cjs +83 -0
  12. package/dist/adapters/jest-hooks.cjs.map +1 -0
  13. package/dist/adapters/jest-hooks.d.cts +24 -0
  14. package/dist/adapters/jest-hooks.d.cts.map +1 -0
  15. package/dist/adapters/jest-hooks.d.ts +24 -0
  16. package/dist/adapters/jest-hooks.d.ts.map +1 -0
  17. package/dist/adapters/jest-hooks.js +78 -0
  18. package/dist/adapters/jest-hooks.js.map +1 -0
  19. package/dist/adapters/jest-register.cjs +17 -0
  20. package/dist/adapters/jest-register.cjs.map +1 -0
  21. package/dist/adapters/jest-register.d.cts +12 -0
  22. package/dist/adapters/jest-register.d.cts.map +1 -0
  23. package/dist/adapters/jest-register.d.ts +12 -0
  24. package/dist/adapters/jest-register.d.ts.map +1 -0
  25. package/dist/adapters/jest-register.js +15 -0
  26. package/dist/adapters/jest-register.js.map +1 -0
  27. package/dist/adapters/types.cjs.map +1 -1
  28. package/dist/adapters/types.d.cts +5 -1
  29. package/dist/adapters/types.d.cts.map +1 -1
  30. package/dist/adapters/types.d.ts +5 -1
  31. package/dist/adapters/types.d.ts.map +1 -1
  32. package/dist/adapters/types.js.map +1 -1
  33. package/dist/cli/commands/run.cjs +17 -11
  34. package/dist/cli/commands/run.cjs.map +1 -1
  35. package/dist/cli/commands/run.js +9 -3
  36. package/dist/cli/commands/run.js.map +1 -1
  37. package/dist/cli/commands/test.cjs +17 -15
  38. package/dist/cli/commands/test.cjs.map +1 -1
  39. package/dist/cli/commands/test.d.cts.map +1 -1
  40. package/dist/cli/commands/test.d.ts.map +1 -1
  41. package/dist/cli/commands/test.js +5 -3
  42. package/dist/cli/commands/test.js.map +1 -1
  43. package/dist/cli/index.cjs +5 -2
  44. package/dist/cli/index.cjs.map +1 -1
  45. package/dist/cli/index.d.cts.map +1 -1
  46. package/dist/cli/index.d.ts.map +1 -1
  47. package/dist/cli/index.js +6 -3
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/constants.cjs +1 -0
  50. package/dist/constants.cjs.map +1 -1
  51. package/dist/constants.d.cts +1 -0
  52. package/dist/constants.d.cts.map +1 -1
  53. package/dist/constants.d.ts +1 -0
  54. package/dist/constants.d.ts.map +1 -1
  55. package/dist/constants.js +1 -0
  56. package/dist/constants.js.map +1 -1
  57. package/dist/index.cjs +4 -1
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +1 -0
  60. package/dist/index.d.cts.map +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/reporters/index.cjs +3 -1
  66. package/dist/reporters/index.cjs.map +1 -1
  67. package/dist/reporters/index.d.cts +1 -0
  68. package/dist/reporters/index.d.cts.map +1 -1
  69. package/dist/reporters/index.d.ts +1 -0
  70. package/dist/reporters/index.d.ts.map +1 -1
  71. package/dist/reporters/index.js +1 -0
  72. package/dist/reporters/index.js.map +1 -1
  73. package/dist/reporters/nyan.cjs +318 -0
  74. package/dist/reporters/nyan.cjs.map +1 -0
  75. package/dist/reporters/nyan.d.cts +118 -0
  76. package/dist/reporters/nyan.d.cts.map +1 -0
  77. package/dist/reporters/nyan.d.ts +118 -0
  78. package/dist/reporters/nyan.d.ts.map +1 -0
  79. package/dist/reporters/nyan.js +314 -0
  80. package/dist/reporters/nyan.js.map +1 -0
  81. package/dist/types/core.cjs.map +1 -1
  82. package/dist/types/core.d.cts +13 -12
  83. package/dist/types/core.d.cts.map +1 -1
  84. package/dist/types/core.d.ts +13 -12
  85. package/dist/types/core.d.ts.map +1 -1
  86. package/dist/types/core.js.map +1 -1
  87. package/dist/types/index.cjs +0 -2
  88. package/dist/types/index.cjs.map +1 -1
  89. package/dist/types/index.d.cts +0 -1
  90. package/dist/types/index.d.cts.map +1 -1
  91. package/dist/types/index.d.ts +0 -1
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +0 -2
  94. package/dist/types/index.js.map +1 -1
  95. package/package.json +28 -8
  96. package/src/adapters/jest-adapter.ts +563 -0
  97. package/src/adapters/jest-hooks.ts +82 -0
  98. package/src/adapters/jest-register.ts +16 -0
  99. package/src/adapters/types.ts +5 -1
  100. package/src/cli/commands/run.ts +10 -3
  101. package/src/cli/commands/test.ts +5 -3
  102. package/src/cli/index.ts +10 -2
  103. package/src/constants.ts +2 -1
  104. package/src/index.ts +3 -0
  105. package/src/reporters/index.ts +1 -0
  106. package/src/reporters/nyan.ts +409 -0
  107. package/src/types/core.ts +16 -14
  108. package/src/types/index.ts +0 -3
  109. package/dist/types/cli.cjs +0 -12
  110. package/dist/types/cli.cjs.map +0 -1
  111. package/dist/types/cli.d.cts +0 -75
  112. package/dist/types/cli.d.cts.map +0 -1
  113. package/dist/types/cli.d.ts +0 -75
  114. package/dist/types/cli.d.ts.map +0 -1
  115. package/dist/types/cli.js +0 -9
  116. package/dist/types/cli.js.map +0 -1
  117. 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
+ });
@@ -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
@@ -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,
@@ -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 '../../types/cli.js';
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', 'ava'] as const,
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/cli.js';
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';
@@ -7,4 +7,5 @@
7
7
  export { CsvReporter } from './csv.js';
8
8
  export { HumanReporter } from './human.js';
9
9
  export { JsonReporter } from './json.js';
10
+ export { NyanReporter } from './nyan.js';
10
11
  export { SimpleReporter } from './simple.js';
@@ -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
+ }