Skip to content

Commit 89f3170

Browse files
boneskullclaude
andauthored
fix(reporters): use timestamped filenames for JSON/CSV output (#200)
Co-authored-by: Claude <[email protected]>
1 parent 6f17059 commit 89f3170

File tree

7 files changed

+212
-57
lines changed

7 files changed

+212
-57
lines changed

src/cli/commands/run.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import type {
1616
import type { CliContext } from '../index.js';
1717

1818
import { ErrorCodes, ExitCodes } from '../../constants.js';
19-
import { resolveOutputPath } from '../../core/output-path-resolver.js';
19+
import {
20+
generateTimestampedFilename,
21+
resolveOutputPath,
22+
} from '../../core/output-path-resolver.js';
2023
import {
2124
type BudgetExceededError,
2225
InvalidArgumentError,
@@ -430,7 +433,7 @@ const setupReporters = async (
430433
const outputPath = resolveOutputPath(
431434
outputDir,
432435
explicitOutputFile,
433-
'results.csv',
436+
generateTimestampedFilename('csv'),
434437
);
435438
reporter = new CsvReporter({
436439
includeHeaders: true,
@@ -455,7 +458,7 @@ const setupReporters = async (
455458
const outputPath = resolveOutputPath(
456459
outputDir,
457460
explicitOutputFile,
458-
'results.json',
461+
generateTimestampedFilename('json'),
459462
);
460463
// Precedence: CLI flag > config file > default (false)
461464
const prettyPrint =

src/core/output-path-resolver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { extname, isAbsolute, join, resolve } from 'node:path';
22

3+
/**
4+
* Generates a timestamped filename for benchmark output files.
5+
*
6+
* @param extension - File extension without the dot (e.g., 'json', 'csv')
7+
* @returns Filename in format `benchmarks-YYYY-MM-DD-HH-MM-SS.{extension}` (UTC
8+
* time)
9+
*/
10+
export const generateTimestampedFilename = (extension: string): string => {
11+
const now = new Date();
12+
const pad = (n: number) => n.toString().padStart(2, '0');
13+
const timestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}-${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}`;
14+
return `benchmarks-${timestamp}.${extension}`;
15+
};
16+
317
/**
418
* Resolves the final output path for a reporter
519
*

test/integration/quiet-mode.test.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
1212
import { join } from 'node:path';
1313
import { afterEach, beforeEach, describe, it } from 'node:test';
1414

15-
import { runCommand } from '../util.js';
15+
import { findFileByPattern, runCommand } from '../util.js';
1616
import { fixtures } from './fixture-paths.js';
1717

1818
describe('Quiet Mode Integration Tests', () => {
@@ -93,8 +93,8 @@ describe('Quiet Mode Integration Tests', () => {
9393
'--quiet',
9494
'--reporter',
9595
'json',
96-
'--output',
97-
outputDir,
96+
'--output-file',
97+
outputFile,
9898
'--iterations',
9999
'5',
100100
]);
@@ -117,8 +117,8 @@ describe('Quiet Mode Integration Tests', () => {
117117
'--quiet',
118118
'--reporter',
119119
'csv',
120-
'--output',
121-
outputDir,
120+
'--output-file',
121+
outputFile,
122122
'--iterations',
123123
'5',
124124
]);
@@ -155,17 +155,21 @@ describe('Quiet Mode Integration Tests', () => {
155155
expect(result.stderr, 'to be empty');
156156
expect(result.exitCode, 'to equal', 0);
157157

158-
// Both JSON and CSV files should be written
159-
const jsonContent = await readFile(
160-
join(outputDir, 'results.json'),
161-
'utf-8',
158+
// Both JSON and CSV files should be written with timestamped names
159+
const jsonFile = await findFileByPattern(
160+
outputDir,
161+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
162162
);
163+
expect(jsonFile, 'to be truthy');
164+
const jsonContent = await readFile(jsonFile!, 'utf-8');
163165
expect(jsonContent, 'to match', /"meta":/);
164166

165-
const csvContent = await readFile(
166-
join(outputDir, 'results.csv'),
167-
'utf-8',
167+
const csvFile = await findFileByPattern(
168+
outputDir,
169+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
168170
);
171+
expect(csvFile, 'to be truthy');
172+
const csvContent = await readFile(csvFile!, 'utf-8');
169173
expect(csvContent, 'to contain', 'file');
170174
});
171175
});
@@ -220,9 +224,13 @@ describe('Quiet Mode Integration Tests', () => {
220224
expect(result.stderr, 'to be empty');
221225
expect(result.exitCode, 'to equal', 0);
222226

223-
// Verify file was written
224-
const outputFile = join(outputDir, 'results.json');
225-
const fileExists = existsSync(outputFile);
227+
// Verify file was written with timestamped name
228+
const outputFile = await findFileByPattern(
229+
outputDir,
230+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
231+
);
232+
expect(outputFile, 'to be truthy');
233+
const fileExists = existsSync(outputFile!);
226234
expect(fileExists, 'to be true');
227235
});
228236
});

test/integration/reporters.test.ts

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
44
import { join } from 'node:path';
55
import { afterEach, beforeEach, describe, it } from 'node:test';
66

7-
import { runCommand } from '../util.js';
7+
import { findFileByPattern, runCommand } from '../util.js';
88
import { fixtures } from './fixture-paths.js';
99

1010
/**
@@ -79,8 +79,8 @@ describe('Multiple reporter output formats', () => {
7979
fixtures.simple,
8080
'--reporter',
8181
'json',
82-
'--output',
83-
join(tempDir, 'results'),
82+
'--output-file',
83+
outputFile,
8484
]);
8585

8686
expect(result.exitCode, 'to equal', 0);
@@ -106,19 +106,19 @@ describe('Multiple reporter output formats', () => {
106106

107107
it('should include all benchmark metadata in JSON', async () => {
108108
const outputDir = join(tempDir, 'metadata-output');
109+
const jsonFile = join(outputDir, 'results.json');
109110
const result = await runCommand([
110111
'run',
111112
fixtures.withMetadataTags,
112113
'--reporter',
113114
'json',
114-
'--output',
115-
outputDir,
115+
'--output-file',
116+
jsonFile,
116117
]);
117118

118119
expect(result.exitCode, 'to equal', 0);
119120

120121
// Read JSON output file
121-
const jsonFile = join(outputDir, 'results.json');
122122
const jsonContent = await readFile(jsonFile, 'utf-8');
123123
const data = JSON.parse(jsonContent);
124124

@@ -150,8 +150,8 @@ describe('Multiple reporter output formats', () => {
150150
fixtures.csvTasks,
151151
'--reporter',
152152
'csv',
153-
'--output',
154-
join(tempDir, 'results'),
153+
'--output-file',
154+
outputFile,
155155
]);
156156

157157
expect(result.exitCode, 'to equal', 0);
@@ -176,19 +176,20 @@ describe('Multiple reporter output formats', () => {
176176
});
177177

178178
it('should include all required CSV columns', async () => {
179+
const outputFile = join(tempDir, 'results.csv');
179180
const result = await runCommand([
180181
'run',
181182
fixtures.simple,
182183
'--reporter',
183184
'csv',
184-
'--output',
185-
tempDir,
185+
'--output-file',
186+
outputFile,
186187
]);
187188

188189
expect(result.exitCode, 'to equal', 0);
189190

190191
// Read CSV from output file
191-
const csvContent = await readFile(join(tempDir, 'results.csv'), 'utf-8');
192+
const csvContent = await readFile(outputFile, 'utf-8');
192193
const lines = csvContent.trim().split('\n');
193194
expect(lines.length, 'to be greater than', 0);
194195

@@ -222,6 +223,7 @@ describe('Multiple reporter output formats', () => {
222223

223224
describe('multiple reporters simultaneously', () => {
224225
it('should output to multiple formats at once', async () => {
226+
const outputDir = join(tempDir, 'results');
225227
const result = await runCommand([
226228
'run',
227229
fixtures.simple,
@@ -232,20 +234,29 @@ describe('Multiple reporter output formats', () => {
232234
'--reporter',
233235
'csv',
234236
'--output',
235-
join(tempDir, 'results'),
237+
outputDir,
236238
]);
237239

238240
expect(result.exitCode, 'to equal', 0);
239241

240242
// Should have human output in stdout
241243
expect(result.stdout, 'to match', /Test Suite|ops/);
242244

243-
// Should create json and csv files
244-
const jsonFile = join(tempDir, 'results', 'results.json');
245-
const csvFile = join(tempDir, 'results', 'results.csv');
245+
// Should create json and csv files with timestamped names
246+
const jsonFile = await findFileByPattern(
247+
outputDir,
248+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
249+
);
250+
const csvFile = await findFileByPattern(
251+
outputDir,
252+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
253+
);
246254

247-
const jsonContent = await readFile(jsonFile, 'utf-8');
248-
const csvContent = await readFile(csvFile, 'utf-8');
255+
expect(jsonFile, 'to be truthy');
256+
expect(csvFile, 'to be truthy');
257+
258+
const jsonContent = await readFile(jsonFile!, 'utf-8');
259+
const csvContent = await readFile(csvFile!, 'utf-8');
249260

250261
expect(jsonContent.length, 'to be greater than', 0);
251262
expect(csvContent.length, 'to be greater than', 0);
@@ -291,30 +302,33 @@ describe('Multiple reporter output formats', () => {
291302

292303
expect(result.exitCode, 'to equal', 0);
293304

294-
// Should create nested directories
295-
const jsonContent = await readFile(
296-
join(outputDir, 'results.json'),
297-
'utf-8',
305+
// Should create nested directories and timestamped files
306+
const jsonFile = await findFileByPattern(
307+
outputDir,
308+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
298309
);
310+
expect(jsonFile, 'to be truthy');
311+
const jsonContent = await readFile(jsonFile!, 'utf-8');
299312
expect(jsonContent.length, 'to be greater than', 0);
300313
});
301314

302315
it('should handle file naming conflicts', async () => {
303-
// Create existing file
316+
// Create existing file with explicit name
304317
const existingFile = join(tempDir, 'results', 'results.json');
305318
await mkdir(join(tempDir, 'results'), { recursive: true });
306319
await writeFile(existingFile, '{"existing": true}');
307320

321+
// Use explicit --output-file to test overwrite behavior
308322
const result = await runCommand([
309323
'run',
310324
fixtures.simple,
311325
'--reporter',
312326
'json',
313-
'--output',
314-
join(tempDir, 'results'),
327+
'--output-file',
328+
existingFile,
315329
]);
316330

317-
// Should handle existing files (overwrite or append)
331+
// Should handle existing files (overwrite)
318332
expect(result.exitCode, 'to equal', 0);
319333
});
320334

test/integration/verbose-mode.test.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os';
1111
import { join } from 'node:path';
1212
import { afterEach, beforeEach, describe, it } from 'node:test';
1313

14-
import { runCommand } from '../util.js';
14+
import { findFileByPattern, runCommand } from '../util.js';
1515
import { fixtures } from './fixture-paths.js';
1616

1717
describe('Verbose Mode Integration Tests', () => {
@@ -248,11 +248,13 @@ describe('Verbose Mode Integration Tests', () => {
248248
expect(result.stderr, 'to contain', 'Setting up reporters...');
249249
expect(result.exitCode, 'to equal', 0);
250250

251-
// JSON data should be written to file
252-
const jsonContent = await readFile(
253-
join(outputDir, 'results.json'),
254-
'utf-8',
251+
// JSON data should be written to file with timestamped name
252+
const jsonFile = await findFileByPattern(
253+
outputDir,
254+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
255255
);
256+
expect(jsonFile, 'to be truthy');
257+
const jsonContent = await readFile(jsonFile!, 'utf-8');
256258
expect(jsonContent, 'to contain', '"meta":');
257259
});
258260

@@ -272,11 +274,13 @@ describe('Verbose Mode Integration Tests', () => {
272274
expect(result.stderr, 'to be empty');
273275
expect(result.exitCode, 'to equal', 0);
274276

275-
// JSON data should be written to file
276-
const jsonContent = await readFile(
277-
join(outputDir, 'results.json'),
278-
'utf-8',
277+
// JSON data should be written to file with timestamped name
278+
const jsonFile = await findFileByPattern(
279+
outputDir,
280+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
279281
);
282+
expect(jsonFile, 'to be truthy');
283+
const jsonContent = await readFile(jsonFile!, 'utf-8');
280284
expect(jsonContent, 'to contain', '"meta":');
281285
});
282286
});
@@ -300,11 +304,13 @@ describe('Verbose Mode Integration Tests', () => {
300304
expect(result.stderr, 'to contain', 'Setting up reporters...');
301305
expect(result.exitCode, 'to equal', 0);
302306

303-
// CSV data should be written to file
304-
const csvContent = await readFile(
305-
join(outputDir, 'results.csv'),
306-
'utf-8',
307+
// CSV data should be written to file with timestamped name
308+
const csvFile = await findFileByPattern(
309+
outputDir,
310+
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
307311
);
312+
expect(csvFile, 'to be truthy');
313+
const csvContent = await readFile(csvFile!, 'utf-8');
308314
expect(csvContent, 'to contain', 'file');
309315
});
310316
});

0 commit comments

Comments
 (0)