Skip to content

Commit 6287509

Browse files
authored
feat: make results directory output optional (#107)
Add -o, --output-dir option to both client and server commands. By default, no results directory is created - only console output. When -o is specified, results are saved to that directory with timestamped subdirectories. This eliminates the need to add 'results/' to .gitignore in every repo where conformance tests are run. Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 0 Claude-Permission-Prompts: 2 Claude-Escapes: 0
1 parent 49311c7 commit 6287509

5 files changed

Lines changed: 81 additions & 49 deletions

File tree

src/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ program
3939
.option('--scenario <scenario>', 'Scenario to test')
4040
.option('--suite <suite>', 'Run a suite of tests in parallel (e.g., "auth")')
4141
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
42+
.option('-o, --output-dir <path>', 'Save results to this directory')
4243
.option('--verbose', 'Show verbose output')
4344
.action(async (options) => {
4445
try {
4546
const timeout = parseInt(options.timeout, 10);
4647
const verbose = options.verbose ?? false;
48+
const outputDir = options.outputDir;
4749

4850
// Handle suite mode
4951
if (options.suite) {
@@ -78,7 +80,8 @@ program
7880
const result = await runConformanceTest(
7981
options.command,
8082
scenarioName,
81-
timeout
83+
timeout,
84+
outputDir
8285
);
8386
return {
8487
scenario: scenarioName,
@@ -163,15 +166,16 @@ program
163166

164167
// If no command provided, run in interactive mode
165168
if (!validated.command) {
166-
await runInteractiveMode(validated.scenario, verbose);
169+
await runInteractiveMode(validated.scenario, verbose, outputDir);
167170
process.exit(0);
168171
}
169172

170173
// Otherwise run conformance test
171174
const result = await runConformanceTest(
172175
validated.command,
173176
validated.scenario,
174-
timeout
177+
timeout,
178+
outputDir
175179
);
176180

177181
const { overallFailure } = printClientResults(
@@ -209,19 +213,22 @@ program
209213
'Suite to run: "active" (default, excludes pending), "all", or "pending"',
210214
'active'
211215
)
216+
.option('-o, --output-dir <path>', 'Save results to this directory')
212217
.option('--verbose', 'Show verbose output (JSON instead of pretty print)')
213218
.action(async (options) => {
214219
try {
215220
// Validate options with Zod
216221
const validated = ServerOptionsSchema.parse(options);
217222

218223
const verbose = options.verbose ?? false;
224+
const outputDir = options.outputDir;
219225

220226
// If a single scenario is specified, run just that one
221227
if (validated.scenario) {
222228
const result = await runServerConformanceTest(
223229
validated.url,
224-
validated.scenario
230+
validated.scenario,
231+
outputDir
225232
);
226233

227234
const { failed } = printServerResults(
@@ -259,7 +266,8 @@ program
259266
try {
260267
const result = await runServerConformanceTest(
261268
validated.url,
262-
scenarioName
269+
scenarioName,
270+
outputDir
263271
);
264272
allResults.push({ scenario: scenarioName, checks: result.checks });
265273
} catch (error) {

src/runner/client.ts

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { promises as fs } from 'fs';
33
import path from 'path';
44
import { ConformanceCheck } from '../types';
55
import { getScenario } from '../scenarios';
6-
import { ensureResultsDir, createResultDir, formatPrettyChecks } from './utils';
6+
import { createResultDir, formatPrettyChecks } from './utils';
77

88
export interface ClientExecutionResult {
99
exitCode: number;
@@ -91,15 +91,19 @@ async function executeClient(
9191
export async function runConformanceTest(
9292
clientCommand: string,
9393
scenarioName: string,
94-
timeout: number = 30000
94+
timeout: number = 30000,
95+
outputDir?: string
9596
): Promise<{
9697
checks: ConformanceCheck[];
9798
clientOutput: ClientExecutionResult;
98-
resultDir: string;
99+
resultDir?: string;
99100
}> {
100-
await ensureResultsDir();
101-
const resultDir = createResultDir(scenarioName);
102-
await fs.mkdir(resultDir, { recursive: true });
101+
let resultDir: string | undefined;
102+
103+
if (outputDir) {
104+
resultDir = createResultDir(outputDir, scenarioName);
105+
await fs.mkdir(resultDir, { recursive: true });
106+
}
103107

104108
// Scenario is guaranteed to exist by CLI validation
105109
const scenario = getScenario(scenarioName)!;
@@ -138,16 +142,24 @@ export async function runConformanceTest(
138142

139143
const checks = scenario.getChecks();
140144

141-
await fs.writeFile(
142-
path.join(resultDir, 'checks.json'),
143-
JSON.stringify(checks, null, 2)
144-
);
145+
if (resultDir) {
146+
await fs.writeFile(
147+
path.join(resultDir, 'checks.json'),
148+
JSON.stringify(checks, null, 2)
149+
);
145150

146-
await fs.writeFile(path.join(resultDir, 'stdout.txt'), clientOutput.stdout);
151+
await fs.writeFile(
152+
path.join(resultDir, 'stdout.txt'),
153+
clientOutput.stdout
154+
);
147155

148-
await fs.writeFile(path.join(resultDir, 'stderr.txt'), clientOutput.stderr);
156+
await fs.writeFile(
157+
path.join(resultDir, 'stderr.txt'),
158+
clientOutput.stderr
159+
);
149160

150-
console.error(`Results saved to ${resultDir}`);
161+
console.error(`Results saved to ${resultDir}`);
162+
}
151163

152164
return {
153165
checks,
@@ -244,11 +256,15 @@ export function printClientResults(
244256

245257
export async function runInteractiveMode(
246258
scenarioName: string,
247-
verbose: boolean = false
259+
verbose: boolean = false,
260+
outputDir?: string
248261
): Promise<void> {
249-
await ensureResultsDir();
250-
const resultDir = createResultDir(scenarioName);
251-
await fs.mkdir(resultDir, { recursive: true });
262+
let resultDir: string | undefined;
263+
264+
if (outputDir) {
265+
resultDir = createResultDir(outputDir, scenarioName);
266+
await fs.mkdir(resultDir, { recursive: true });
267+
}
252268

253269
// Scenario is guaranteed to exist by CLI validation
254270
const scenario = getScenario(scenarioName)!;
@@ -257,23 +273,29 @@ export async function runInteractiveMode(
257273
const urls = await scenario.start();
258274

259275
console.log(`Server URL: ${urls.serverUrl}`);
260-
console.log('Press Ctrl+C to stop and save checks...');
276+
console.log('Press Ctrl+C to stop...');
261277

262278
const handleShutdown = async () => {
263279
console.log('\nShutting down...');
264280

265281
const checks = scenario.getChecks();
266-
await fs.writeFile(
267-
path.join(resultDir, 'checks.json'),
268-
JSON.stringify(checks, null, 2)
269-
);
282+
283+
if (resultDir) {
284+
await fs.writeFile(
285+
path.join(resultDir, 'checks.json'),
286+
JSON.stringify(checks, null, 2)
287+
);
288+
}
270289

271290
if (verbose) {
272291
console.log(`\nChecks:\n${JSON.stringify(checks, null, 2)}`);
273292
} else {
274293
console.log(`\nChecks:\n${formatPrettyChecks(checks)}`);
275294
}
276-
console.log(`\nChecks saved to ${resultDir}/checks.json`);
295+
296+
if (resultDir) {
297+
console.log(`\nChecks saved to ${resultDir}/checks.json`);
298+
}
277299

278300
await scenario.stop();
279301
process.exit(0);

src/runner/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export {
1515

1616
// Export utilities
1717
export {
18-
ensureResultsDir,
1918
createResultDir,
2019
formatPrettyChecks,
2120
getStatusColor,

src/runner/server.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
22
import path from 'path';
33
import { ConformanceCheck } from '../types';
44
import { getClientScenario } from '../scenarios';
5-
import { ensureResultsDir, createResultDir, formatPrettyChecks } from './utils';
5+
import { createResultDir, formatPrettyChecks } from './utils';
66

77
/**
88
* Format markdown-style text for terminal output using ANSI codes
@@ -19,15 +19,19 @@ function formatMarkdown(text: string): string {
1919

2020
export async function runServerConformanceTest(
2121
serverUrl: string,
22-
scenarioName: string
22+
scenarioName: string,
23+
outputDir?: string
2324
): Promise<{
2425
checks: ConformanceCheck[];
25-
resultDir: string;
26+
resultDir?: string;
2627
scenarioDescription: string;
2728
}> {
28-
await ensureResultsDir();
29-
const resultDir = createResultDir(scenarioName, 'server');
30-
await fs.mkdir(resultDir, { recursive: true });
29+
let resultDir: string | undefined;
30+
31+
if (outputDir) {
32+
resultDir = createResultDir(outputDir, scenarioName, 'server');
33+
await fs.mkdir(resultDir, { recursive: true });
34+
}
3135

3236
// Scenario is guaranteed to exist by CLI validation
3337
const scenario = getClientScenario(scenarioName)!;
@@ -38,12 +42,14 @@ export async function runServerConformanceTest(
3842

3943
const checks = await scenario.run(serverUrl);
4044

41-
await fs.writeFile(
42-
path.join(resultDir, 'checks.json'),
43-
JSON.stringify(checks, null, 2)
44-
);
45+
if (resultDir) {
46+
await fs.writeFile(
47+
path.join(resultDir, 'checks.json'),
48+
JSON.stringify(checks, null, 2)
49+
);
4550

46-
console.log(`Results saved to ${resultDir}`);
51+
console.log(`Results saved to ${resultDir}`);
52+
}
4753

4854
return {
4955
checks,

src/runner/utils.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { promises as fs } from 'fs';
21
import path from 'path';
32
import { ConformanceCheck } from '../types';
43

@@ -51,14 +50,12 @@ export function formatPrettyChecks(checks: ConformanceCheck[]): string {
5150
.join('\n');
5251
}
5352

54-
export async function ensureResultsDir(): Promise<string> {
55-
const resultsDir = path.join(process.cwd(), 'results');
56-
await fs.mkdir(resultsDir, { recursive: true });
57-
return resultsDir;
58-
}
59-
60-
export function createResultDir(scenario: string, prefix = ''): string {
53+
export function createResultDir(
54+
baseDir: string,
55+
scenario: string,
56+
prefix = ''
57+
): string {
6158
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
6259
const dirName = prefix ? `${prefix}-${scenario}` : scenario;
63-
return path.join('results', `${dirName}-${timestamp}`);
60+
return path.join(baseDir, `${dirName}-${timestamp}`);
6461
}

0 commit comments

Comments
 (0)