Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/cli/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import type {
import type { CliContext } from '../index.js';

import { ErrorCodes, ExitCodes } from '../../constants.js';
import { resolveOutputPath } from '../../core/output-path-resolver.js';
import {
generateTimestampedFilename,
resolveOutputPath,
} from '../../core/output-path-resolver.js';
import {
type BudgetExceededError,
InvalidArgumentError,
Expand Down Expand Up @@ -430,7 +433,7 @@ const setupReporters = async (
const outputPath = resolveOutputPath(
outputDir,
explicitOutputFile,
'results.csv',
generateTimestampedFilename('csv'),
);
reporter = new CsvReporter({
includeHeaders: true,
Expand All @@ -455,7 +458,7 @@ const setupReporters = async (
const outputPath = resolveOutputPath(
outputDir,
explicitOutputFile,
'results.json',
generateTimestampedFilename('json'),
);
// Precedence: CLI flag > config file > default (false)
const prettyPrint =
Expand Down
14 changes: 14 additions & 0 deletions src/core/output-path-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { extname, isAbsolute, join, resolve } from 'node:path';

/**
* Generates a timestamped filename for benchmark output files.
*
* @param extension - File extension without the dot (e.g., 'json', 'csv')
* @returns Filename in format `benchmarks-YYYY-MM-DD-HH-MM-SS.{extension}` (UTC
* time)
*/
export const generateTimestampedFilename = (extension: string): string => {
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
const timestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}-${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}`;
return `benchmarks-${timestamp}.${extension}`;
};

/**
* Resolves the final output path for a reporter
*
Expand Down
38 changes: 23 additions & 15 deletions test/integration/quiet-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';

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

describe('Quiet Mode Integration Tests', () => {
Expand Down Expand Up @@ -93,8 +93,8 @@ describe('Quiet Mode Integration Tests', () => {
'--quiet',
'--reporter',
'json',
'--output',
outputDir,
'--output-file',
outputFile,
'--iterations',
'5',
]);
Expand All @@ -117,8 +117,8 @@ describe('Quiet Mode Integration Tests', () => {
'--quiet',
'--reporter',
'csv',
'--output',
outputDir,
'--output-file',
outputFile,
'--iterations',
'5',
]);
Expand Down Expand Up @@ -155,17 +155,21 @@ describe('Quiet Mode Integration Tests', () => {
expect(result.stderr, 'to be empty');
expect(result.exitCode, 'to equal', 0);

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

const csvContent = await readFile(
join(outputDir, 'results.csv'),
'utf-8',
const csvFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
);
expect(csvFile, 'to be truthy');
const csvContent = await readFile(csvFile!, 'utf-8');
expect(csvContent, 'to contain', 'file');
});
});
Expand Down Expand Up @@ -220,9 +224,13 @@ describe('Quiet Mode Integration Tests', () => {
expect(result.stderr, 'to be empty');
expect(result.exitCode, 'to equal', 0);

// Verify file was written
const outputFile = join(outputDir, 'results.json');
const fileExists = existsSync(outputFile);
// Verify file was written with timestamped name
const outputFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
);
expect(outputFile, 'to be truthy');
const fileExists = existsSync(outputFile!);
expect(fileExists, 'to be true');
});
});
Expand Down
64 changes: 39 additions & 25 deletions test/integration/reporters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';

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

/**
Expand Down Expand Up @@ -79,8 +79,8 @@ describe('Multiple reporter output formats', () => {
fixtures.simple,
'--reporter',
'json',
'--output',
join(tempDir, 'results'),
'--output-file',
outputFile,
]);

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

it('should include all benchmark metadata in JSON', async () => {
const outputDir = join(tempDir, 'metadata-output');
const jsonFile = join(outputDir, 'results.json');
const result = await runCommand([
'run',
fixtures.withMetadataTags,
'--reporter',
'json',
'--output',
outputDir,
'--output-file',
jsonFile,
]);

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

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

Expand Down Expand Up @@ -150,8 +150,8 @@ describe('Multiple reporter output formats', () => {
fixtures.csvTasks,
'--reporter',
'csv',
'--output',
join(tempDir, 'results'),
'--output-file',
outputFile,
]);

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

it('should include all required CSV columns', async () => {
const outputFile = join(tempDir, 'results.csv');
const result = await runCommand([
'run',
fixtures.simple,
'--reporter',
'csv',
'--output',
tempDir,
'--output-file',
outputFile,
]);

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

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

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

describe('multiple reporters simultaneously', () => {
it('should output to multiple formats at once', async () => {
const outputDir = join(tempDir, 'results');
const result = await runCommand([
'run',
fixtures.simple,
Expand All @@ -232,20 +234,29 @@ describe('Multiple reporter output formats', () => {
'--reporter',
'csv',
'--output',
join(tempDir, 'results'),
outputDir,
]);

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

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

// Should create json and csv files
const jsonFile = join(tempDir, 'results', 'results.json');
const csvFile = join(tempDir, 'results', 'results.csv');
// Should create json and csv files with timestamped names
const jsonFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
);
const csvFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
);

const jsonContent = await readFile(jsonFile, 'utf-8');
const csvContent = await readFile(csvFile, 'utf-8');
expect(jsonFile, 'to be truthy');
expect(csvFile, 'to be truthy');

const jsonContent = await readFile(jsonFile!, 'utf-8');
const csvContent = await readFile(csvFile!, 'utf-8');

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

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

// Should create nested directories
const jsonContent = await readFile(
join(outputDir, 'results.json'),
'utf-8',
// Should create nested directories and timestamped files
const jsonFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
);
expect(jsonFile, 'to be truthy');
const jsonContent = await readFile(jsonFile!, 'utf-8');
expect(jsonContent.length, 'to be greater than', 0);
});

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

// Use explicit --output-file to test overwrite behavior
const result = await runCommand([
'run',
fixtures.simple,
'--reporter',
'json',
'--output',
join(tempDir, 'results'),
'--output-file',
existingFile,
]);

// Should handle existing files (overwrite or append)
// Should handle existing files (overwrite)
expect(result.exitCode, 'to equal', 0);
});

Expand Down
32 changes: 19 additions & 13 deletions test/integration/verbose-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';

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

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

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

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

// JSON data should be written to file
const jsonContent = await readFile(
join(outputDir, 'results.json'),
'utf-8',
// JSON data should be written to file with timestamped name
const jsonFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.json$/,
);
expect(jsonFile, 'to be truthy');
const jsonContent = await readFile(jsonFile!, 'utf-8');
expect(jsonContent, 'to contain', '"meta":');
});
});
Expand All @@ -300,11 +304,13 @@ describe('Verbose Mode Integration Tests', () => {
expect(result.stderr, 'to contain', 'Setting up reporters...');
expect(result.exitCode, 'to equal', 0);

// CSV data should be written to file
const csvContent = await readFile(
join(outputDir, 'results.csv'),
'utf-8',
// CSV data should be written to file with timestamped name
const csvFile = await findFileByPattern(
outputDir,
/^benchmarks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.csv$/,
);
expect(csvFile, 'to be truthy');
const csvContent = await readFile(csvFile!, 'utf-8');
expect(csvContent, 'to contain', 'file');
});
});
Expand Down
Loading