@curl-runner/cli 1.13.0 → 1.15.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/package.json +1 -1
- package/src/cli.ts +423 -1
- package/src/diff/baseline-manager.test.ts +181 -0
- package/src/diff/baseline-manager.ts +266 -0
- package/src/diff/diff-formatter.ts +316 -0
- package/src/diff/index.ts +3 -0
- package/src/diff/response-differ.test.ts +330 -0
- package/src/diff/response-differ.ts +489 -0
- package/src/executor/profile-executor.test.ts +132 -0
- package/src/executor/profile-executor.ts +167 -0
- package/src/types/config.ts +166 -0
- package/src/utils/logger.ts +121 -0
- package/src/utils/stats.test.ts +161 -0
- package/src/utils/stats.ts +151 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Glob } from 'bun';
|
|
4
|
+
import { BaselineManager, DiffFormatter, DiffOrchestrator } from './diff';
|
|
5
|
+
import { ProfileExecutor } from './executor/profile-executor';
|
|
4
6
|
import { RequestExecutor } from './executor/request-executor';
|
|
5
7
|
import { YamlParser } from './parser/yaml';
|
|
6
8
|
import type {
|
|
9
|
+
DiffConfig,
|
|
7
10
|
ExecutionResult,
|
|
8
11
|
ExecutionSummary,
|
|
9
12
|
GlobalConfig,
|
|
13
|
+
GlobalDiffConfig,
|
|
14
|
+
ProfileConfig,
|
|
10
15
|
RequestConfig,
|
|
11
16
|
WatchConfig,
|
|
12
17
|
} from './types/config';
|
|
13
18
|
import { Logger } from './utils/logger';
|
|
19
|
+
import { exportToCSV, exportToJSON } from './utils/stats';
|
|
14
20
|
import { VersionChecker } from './utils/version-checker';
|
|
15
21
|
import { getVersion } from './version';
|
|
16
22
|
import { FileWatcher } from './watcher/file-watcher';
|
|
@@ -166,6 +172,49 @@ class CurlRunnerCLI {
|
|
|
166
172
|
};
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
// Profile mode configuration
|
|
176
|
+
if (process.env.CURL_RUNNER_PROFILE) {
|
|
177
|
+
const iterations = Number.parseInt(process.env.CURL_RUNNER_PROFILE, 10);
|
|
178
|
+
if (iterations > 0) {
|
|
179
|
+
envConfig.profile = {
|
|
180
|
+
...envConfig.profile,
|
|
181
|
+
iterations,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (process.env.CURL_RUNNER_PROFILE_WARMUP) {
|
|
187
|
+
envConfig.profile = {
|
|
188
|
+
...envConfig.profile,
|
|
189
|
+
iterations: envConfig.profile?.iterations ?? 10,
|
|
190
|
+
warmup: Number.parseInt(process.env.CURL_RUNNER_PROFILE_WARMUP, 10),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (process.env.CURL_RUNNER_PROFILE_CONCURRENCY) {
|
|
195
|
+
envConfig.profile = {
|
|
196
|
+
...envConfig.profile,
|
|
197
|
+
iterations: envConfig.profile?.iterations ?? 10,
|
|
198
|
+
concurrency: Number.parseInt(process.env.CURL_RUNNER_PROFILE_CONCURRENCY, 10),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.env.CURL_RUNNER_PROFILE_HISTOGRAM) {
|
|
203
|
+
envConfig.profile = {
|
|
204
|
+
...envConfig.profile,
|
|
205
|
+
iterations: envConfig.profile?.iterations ?? 10,
|
|
206
|
+
histogram: process.env.CURL_RUNNER_PROFILE_HISTOGRAM.toLowerCase() === 'true',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (process.env.CURL_RUNNER_PROFILE_EXPORT) {
|
|
211
|
+
envConfig.profile = {
|
|
212
|
+
...envConfig.profile,
|
|
213
|
+
iterations: envConfig.profile?.iterations ?? 10,
|
|
214
|
+
exportFile: process.env.CURL_RUNNER_PROFILE_EXPORT,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
169
218
|
// Snapshot configuration
|
|
170
219
|
if (process.env.CURL_RUNNER_SNAPSHOT) {
|
|
171
220
|
envConfig.snapshot = {
|
|
@@ -198,6 +247,52 @@ class CurlRunnerCLI {
|
|
|
198
247
|
};
|
|
199
248
|
}
|
|
200
249
|
|
|
250
|
+
// Diff configuration
|
|
251
|
+
if (process.env.CURL_RUNNER_DIFF) {
|
|
252
|
+
envConfig.diff = {
|
|
253
|
+
...envConfig.diff,
|
|
254
|
+
enabled: process.env.CURL_RUNNER_DIFF.toLowerCase() === 'true',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (process.env.CURL_RUNNER_DIFF_SAVE) {
|
|
259
|
+
envConfig.diff = {
|
|
260
|
+
...envConfig.diff,
|
|
261
|
+
save: process.env.CURL_RUNNER_DIFF_SAVE.toLowerCase() === 'true',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (process.env.CURL_RUNNER_DIFF_LABEL) {
|
|
266
|
+
envConfig.diff = {
|
|
267
|
+
...envConfig.diff,
|
|
268
|
+
label: process.env.CURL_RUNNER_DIFF_LABEL,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (process.env.CURL_RUNNER_DIFF_COMPARE) {
|
|
273
|
+
envConfig.diff = {
|
|
274
|
+
...envConfig.diff,
|
|
275
|
+
compareWith: process.env.CURL_RUNNER_DIFF_COMPARE,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (process.env.CURL_RUNNER_DIFF_DIR) {
|
|
280
|
+
envConfig.diff = {
|
|
281
|
+
...envConfig.diff,
|
|
282
|
+
dir: process.env.CURL_RUNNER_DIFF_DIR,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (process.env.CURL_RUNNER_DIFF_OUTPUT) {
|
|
287
|
+
const format = process.env.CURL_RUNNER_DIFF_OUTPUT.toLowerCase();
|
|
288
|
+
if (['terminal', 'json', 'markdown'].includes(format)) {
|
|
289
|
+
envConfig.diff = {
|
|
290
|
+
...envConfig.diff,
|
|
291
|
+
outputFormat: format as 'terminal' | 'json' | 'markdown',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
201
296
|
return envConfig;
|
|
202
297
|
}
|
|
203
298
|
|
|
@@ -223,6 +318,12 @@ class CurlRunnerCLI {
|
|
|
223
318
|
return;
|
|
224
319
|
}
|
|
225
320
|
|
|
321
|
+
// Handle diff subcommand: curl-runner diff <label1> <label2> [file]
|
|
322
|
+
if (args[0] === 'diff' && args.length >= 3) {
|
|
323
|
+
await this.executeDiffSubcommand(args.slice(1), options);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
226
327
|
// Load configuration from environment variables, config file, then CLI options
|
|
227
328
|
const envConfig = this.loadEnvironmentVariables();
|
|
228
329
|
const configFile = await this.loadConfigFile();
|
|
@@ -370,6 +471,46 @@ class CurlRunnerCLI {
|
|
|
370
471
|
};
|
|
371
472
|
}
|
|
372
473
|
|
|
474
|
+
// Apply diff options
|
|
475
|
+
if (options.diff !== undefined) {
|
|
476
|
+
globalConfig.diff = {
|
|
477
|
+
...globalConfig.diff,
|
|
478
|
+
enabled: options.diff as boolean,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (options.diffSave !== undefined) {
|
|
482
|
+
globalConfig.diff = {
|
|
483
|
+
...globalConfig.diff,
|
|
484
|
+
enabled: true,
|
|
485
|
+
save: options.diffSave as boolean,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (options.diffLabel !== undefined) {
|
|
489
|
+
globalConfig.diff = {
|
|
490
|
+
...globalConfig.diff,
|
|
491
|
+
label: options.diffLabel as string,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (options.diffCompare !== undefined) {
|
|
495
|
+
globalConfig.diff = {
|
|
496
|
+
...globalConfig.diff,
|
|
497
|
+
enabled: true,
|
|
498
|
+
compareWith: options.diffCompare as string,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (options.diffDir !== undefined) {
|
|
502
|
+
globalConfig.diff = {
|
|
503
|
+
...globalConfig.diff,
|
|
504
|
+
dir: options.diffDir as string,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (options.diffOutput !== undefined) {
|
|
508
|
+
globalConfig.diff = {
|
|
509
|
+
...globalConfig.diff,
|
|
510
|
+
outputFormat: options.diffOutput as 'terminal' | 'json' | 'markdown',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
373
514
|
if (allRequests.length === 0) {
|
|
374
515
|
this.logger.logError('No requests found in YAML files');
|
|
375
516
|
process.exit(1);
|
|
@@ -378,7 +519,36 @@ class CurlRunnerCLI {
|
|
|
378
519
|
// Check if watch mode is enabled
|
|
379
520
|
const watchEnabled = options.watch || globalConfig.watch?.enabled;
|
|
380
521
|
|
|
381
|
-
if (
|
|
522
|
+
// Check if profile mode is enabled (mutually exclusive with watch mode)
|
|
523
|
+
const profileIterations =
|
|
524
|
+
(options.profile as number | undefined) ?? globalConfig.profile?.iterations;
|
|
525
|
+
const profileEnabled = profileIterations && profileIterations > 0;
|
|
526
|
+
|
|
527
|
+
if (watchEnabled && profileEnabled) {
|
|
528
|
+
this.logger.logError('Profile mode and watch mode cannot be used together');
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (profileEnabled) {
|
|
533
|
+
// Profile mode - run requests multiple times for latency stats
|
|
534
|
+
const profileConfig: ProfileConfig = {
|
|
535
|
+
iterations: profileIterations,
|
|
536
|
+
warmup:
|
|
537
|
+
(options.profileWarmup as number | undefined) ?? globalConfig.profile?.warmup ?? 1,
|
|
538
|
+
concurrency:
|
|
539
|
+
(options.profileConcurrency as number | undefined) ??
|
|
540
|
+
globalConfig.profile?.concurrency ??
|
|
541
|
+
1,
|
|
542
|
+
histogram:
|
|
543
|
+
(options.profileHistogram as boolean | undefined) ??
|
|
544
|
+
globalConfig.profile?.histogram ??
|
|
545
|
+
false,
|
|
546
|
+
exportFile:
|
|
547
|
+
(options.profileExport as string | undefined) ?? globalConfig.profile?.exportFile,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
await this.executeProfileMode(allRequests, globalConfig, profileConfig);
|
|
551
|
+
} else if (watchEnabled) {
|
|
382
552
|
// Build watch config from options and global config
|
|
383
553
|
const watchConfig: WatchConfig = {
|
|
384
554
|
enabled: true,
|
|
@@ -409,6 +579,49 @@ class CurlRunnerCLI {
|
|
|
409
579
|
}
|
|
410
580
|
}
|
|
411
581
|
|
|
582
|
+
private async executeProfileMode(
|
|
583
|
+
requests: RequestConfig[],
|
|
584
|
+
globalConfig: GlobalConfig,
|
|
585
|
+
profileConfig: ProfileConfig,
|
|
586
|
+
): Promise<void> {
|
|
587
|
+
const profileExecutor = new ProfileExecutor(globalConfig, profileConfig);
|
|
588
|
+
const results = await profileExecutor.profileRequests(requests);
|
|
589
|
+
|
|
590
|
+
this.logger.logProfileSummary(results);
|
|
591
|
+
|
|
592
|
+
// Export results if requested
|
|
593
|
+
if (profileConfig.exportFile) {
|
|
594
|
+
const exportData: string[] = [];
|
|
595
|
+
const isCSV = profileConfig.exportFile.endsWith('.csv');
|
|
596
|
+
|
|
597
|
+
for (const result of results) {
|
|
598
|
+
const name = result.request.name || result.request.url;
|
|
599
|
+
if (isCSV) {
|
|
600
|
+
exportData.push(exportToCSV(result.stats, name));
|
|
601
|
+
} else {
|
|
602
|
+
exportData.push(exportToJSON(result.stats, name));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const content = isCSV ? exportData.join('\n\n') : `[${exportData.join(',\n')}]`;
|
|
607
|
+
await Bun.write(profileConfig.exportFile, content);
|
|
608
|
+
this.logger.logInfo(`Profile results exported to ${profileConfig.exportFile}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Exit with code 1 if failure rate is high
|
|
612
|
+
const totalFailures = results.reduce((sum, r) => sum + r.stats.failures, 0);
|
|
613
|
+
const totalIterations = results.reduce(
|
|
614
|
+
(sum, r) => sum + r.stats.iterations + r.stats.warmup,
|
|
615
|
+
0,
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
if (totalFailures > 0 && totalFailures / totalIterations > 0.5) {
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
|
|
412
625
|
private async executeRequests(
|
|
413
626
|
yamlFiles: string[],
|
|
414
627
|
globalConfig: GlobalConfig,
|
|
@@ -475,9 +688,138 @@ class CurlRunnerCLI {
|
|
|
475
688
|
summary = await executor.execute(allRequests);
|
|
476
689
|
}
|
|
477
690
|
|
|
691
|
+
// Handle diff mode
|
|
692
|
+
if (globalConfig.diff?.enabled || globalConfig.diff?.save || globalConfig.diff?.compareWith) {
|
|
693
|
+
await this.handleDiffMode(yamlFiles[0], summary.results, globalConfig.diff);
|
|
694
|
+
}
|
|
695
|
+
|
|
478
696
|
return summary;
|
|
479
697
|
}
|
|
480
698
|
|
|
699
|
+
private async handleDiffMode(
|
|
700
|
+
yamlPath: string,
|
|
701
|
+
results: ExecutionResult[],
|
|
702
|
+
diffConfig: GlobalDiffConfig,
|
|
703
|
+
): Promise<void> {
|
|
704
|
+
const orchestrator = new DiffOrchestrator(diffConfig);
|
|
705
|
+
const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
|
|
706
|
+
const config: DiffConfig = BaselineManager.mergeConfig(diffConfig, true) || {};
|
|
707
|
+
|
|
708
|
+
const currentLabel = diffConfig.label || 'current';
|
|
709
|
+
const compareLabel = diffConfig.compareWith;
|
|
710
|
+
|
|
711
|
+
// Save baseline if requested
|
|
712
|
+
if (diffConfig.save) {
|
|
713
|
+
await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
|
|
714
|
+
this.logger.logInfo(`Baseline saved as '${currentLabel}'`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Compare with baseline if requested
|
|
718
|
+
if (compareLabel) {
|
|
719
|
+
const diffSummary = await orchestrator.compareWithBaseline(
|
|
720
|
+
yamlPath,
|
|
721
|
+
results,
|
|
722
|
+
currentLabel,
|
|
723
|
+
compareLabel,
|
|
724
|
+
config,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
// Check if baseline exists
|
|
728
|
+
if (diffSummary.newBaselines === diffSummary.totalRequests) {
|
|
729
|
+
this.logger.logWarning(
|
|
730
|
+
`No baseline '${compareLabel}' found. Saving current run as baseline.`,
|
|
731
|
+
);
|
|
732
|
+
await orchestrator.saveBaseline(yamlPath, compareLabel, results, config);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const output = formatter.formatSummary(diffSummary, compareLabel, currentLabel);
|
|
737
|
+
console.log(output);
|
|
738
|
+
|
|
739
|
+
// Save current as baseline if configured
|
|
740
|
+
if (diffConfig.save) {
|
|
741
|
+
await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
|
|
742
|
+
}
|
|
743
|
+
} else if (diffConfig.enabled && !diffConfig.save) {
|
|
744
|
+
// Auto-detect: list available baselines or save first baseline
|
|
745
|
+
const labels = await orchestrator.listLabels(yamlPath);
|
|
746
|
+
|
|
747
|
+
if (labels.length === 0) {
|
|
748
|
+
// No baselines exist - save current as default baseline
|
|
749
|
+
await orchestrator.saveBaseline(yamlPath, 'baseline', results, config);
|
|
750
|
+
this.logger.logInfo(`No baselines found. Saved current run as 'baseline'.`);
|
|
751
|
+
} else if (labels.length === 1) {
|
|
752
|
+
// One baseline exists - compare against it
|
|
753
|
+
const diffSummary = await orchestrator.compareWithBaseline(
|
|
754
|
+
yamlPath,
|
|
755
|
+
results,
|
|
756
|
+
currentLabel,
|
|
757
|
+
labels[0],
|
|
758
|
+
config,
|
|
759
|
+
);
|
|
760
|
+
const output = formatter.formatSummary(diffSummary, labels[0], currentLabel);
|
|
761
|
+
console.log(output);
|
|
762
|
+
} else {
|
|
763
|
+
// Multiple baselines - list them
|
|
764
|
+
this.logger.logInfo(`Available baselines: ${labels.join(', ')}`);
|
|
765
|
+
this.logger.logInfo(`Use --diff-compare <label> to compare against a specific baseline.`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Executes the diff subcommand to compare two stored baselines.
|
|
772
|
+
* Usage: curl-runner diff <label1> <label2> [file.yaml]
|
|
773
|
+
*/
|
|
774
|
+
private async executeDiffSubcommand(
|
|
775
|
+
args: string[],
|
|
776
|
+
options: Record<string, unknown>,
|
|
777
|
+
): Promise<void> {
|
|
778
|
+
const label1 = args[0];
|
|
779
|
+
const label2 = args[1];
|
|
780
|
+
let yamlFile = args[2];
|
|
781
|
+
|
|
782
|
+
// Find YAML file if not specified
|
|
783
|
+
if (!yamlFile) {
|
|
784
|
+
const yamlFiles = await this.findYamlFiles([], options);
|
|
785
|
+
if (yamlFiles.length === 0) {
|
|
786
|
+
this.logger.logError(
|
|
787
|
+
'No YAML files found. Specify a file: curl-runner diff <label1> <label2> <file.yaml>',
|
|
788
|
+
);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
if (yamlFiles.length > 1) {
|
|
792
|
+
this.logger.logError('Multiple YAML files found. Specify which file to use.');
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
yamlFile = yamlFiles[0];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const diffConfig: GlobalDiffConfig = {
|
|
799
|
+
dir: (options.diffDir as string) || '__baselines__',
|
|
800
|
+
outputFormat: (options.diffOutput as 'terminal' | 'json' | 'markdown') || 'terminal',
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const orchestrator = new DiffOrchestrator(diffConfig);
|
|
804
|
+
const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
|
|
805
|
+
const config: DiffConfig = { exclude: [], match: {} };
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const diffSummary = await orchestrator.compareTwoBaselines(yamlFile, label1, label2, config);
|
|
809
|
+
const output = formatter.formatSummary(diffSummary, label1, label2);
|
|
810
|
+
console.log(output);
|
|
811
|
+
|
|
812
|
+
// Exit with code 1 if differences found
|
|
813
|
+
if (diffSummary.changed > 0) {
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
process.exit(0);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
this.logger.logError(error instanceof Error ? error.message : String(error));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
481
823
|
private parseArguments(args: string[]): { files: string[]; options: Record<string, unknown> } {
|
|
482
824
|
const options: Record<string, unknown> = {};
|
|
483
825
|
const files: string[] = [];
|
|
@@ -509,6 +851,8 @@ class CurlRunnerCLI {
|
|
|
509
851
|
options.watchClear = true;
|
|
510
852
|
} else if (key === 'no-watch-clear') {
|
|
511
853
|
options.watchClear = false;
|
|
854
|
+
} else if (key === 'profile-histogram') {
|
|
855
|
+
options.profileHistogram = true;
|
|
512
856
|
} else if (key === 'snapshot') {
|
|
513
857
|
options.snapshot = true;
|
|
514
858
|
} else if (key === 'update-snapshots') {
|
|
@@ -517,6 +861,10 @@ class CurlRunnerCLI {
|
|
|
517
861
|
options.snapshotUpdate = 'failing';
|
|
518
862
|
} else if (key === 'ci-snapshot') {
|
|
519
863
|
options.snapshotCi = true;
|
|
864
|
+
} else if (key === 'diff') {
|
|
865
|
+
options.diff = true;
|
|
866
|
+
} else if (key === 'diff-save') {
|
|
867
|
+
options.diffSave = true;
|
|
520
868
|
} else if (nextArg && !nextArg.startsWith('--')) {
|
|
521
869
|
if (key === 'continue-on-error') {
|
|
522
870
|
options.continueOnError = nextArg === 'true';
|
|
@@ -550,8 +898,26 @@ class CurlRunnerCLI {
|
|
|
550
898
|
}
|
|
551
899
|
} else if (key === 'watch-debounce') {
|
|
552
900
|
options.watchDebounce = Number.parseInt(nextArg, 10);
|
|
901
|
+
} else if (key === 'profile') {
|
|
902
|
+
options.profile = Number.parseInt(nextArg, 10);
|
|
903
|
+
} else if (key === 'profile-warmup') {
|
|
904
|
+
options.profileWarmup = Number.parseInt(nextArg, 10);
|
|
905
|
+
} else if (key === 'profile-concurrency') {
|
|
906
|
+
options.profileConcurrency = Number.parseInt(nextArg, 10);
|
|
907
|
+
} else if (key === 'profile-export') {
|
|
908
|
+
options.profileExport = nextArg;
|
|
553
909
|
} else if (key === 'snapshot-dir') {
|
|
554
910
|
options.snapshotDir = nextArg;
|
|
911
|
+
} else if (key === 'diff-label') {
|
|
912
|
+
options.diffLabel = nextArg;
|
|
913
|
+
} else if (key === 'diff-compare') {
|
|
914
|
+
options.diffCompare = nextArg;
|
|
915
|
+
} else if (key === 'diff-dir') {
|
|
916
|
+
options.diffDir = nextArg;
|
|
917
|
+
} else if (key === 'diff-output') {
|
|
918
|
+
if (['terminal', 'json', 'markdown'].includes(nextArg)) {
|
|
919
|
+
options.diffOutput = nextArg;
|
|
920
|
+
}
|
|
555
921
|
} else {
|
|
556
922
|
options[key] = nextArg;
|
|
557
923
|
}
|
|
@@ -587,6 +953,9 @@ class CurlRunnerCLI {
|
|
|
587
953
|
case 'u':
|
|
588
954
|
options.snapshotUpdate = 'all';
|
|
589
955
|
break;
|
|
956
|
+
case 'd':
|
|
957
|
+
options.diff = true;
|
|
958
|
+
break;
|
|
590
959
|
case 'o': {
|
|
591
960
|
// Handle -o flag for output file
|
|
592
961
|
const outputArg = args[i + 1];
|
|
@@ -596,6 +965,15 @@ class CurlRunnerCLI {
|
|
|
596
965
|
}
|
|
597
966
|
break;
|
|
598
967
|
}
|
|
968
|
+
case 'P': {
|
|
969
|
+
// Handle -P flag for profile mode
|
|
970
|
+
const profileArg = args[i + 1];
|
|
971
|
+
if (profileArg && !profileArg.startsWith('-')) {
|
|
972
|
+
options.profile = Number.parseInt(profileArg, 10);
|
|
973
|
+
i++;
|
|
974
|
+
}
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
599
977
|
}
|
|
600
978
|
}
|
|
601
979
|
} else {
|
|
@@ -717,6 +1095,7 @@ class CurlRunnerCLI {
|
|
|
717
1095
|
ci: { ...base.ci, ...override.ci },
|
|
718
1096
|
watch: { ...base.watch, ...override.watch },
|
|
719
1097
|
snapshot: { ...base.snapshot, ...override.snapshot },
|
|
1098
|
+
diff: { ...base.diff, ...override.diff },
|
|
720
1099
|
};
|
|
721
1100
|
}
|
|
722
1101
|
|
|
@@ -804,6 +1183,13 @@ ${this.logger.color('WATCH MODE:', 'yellow')}
|
|
|
804
1183
|
--watch-debounce <ms> Debounce delay for watch mode (default: 300)
|
|
805
1184
|
--no-watch-clear Don't clear screen between watch runs
|
|
806
1185
|
|
|
1186
|
+
${this.logger.color('PROFILE MODE:', 'yellow')}
|
|
1187
|
+
-P, --profile <n> Run each request N times for latency stats
|
|
1188
|
+
--profile-warmup <n> Warmup iterations to exclude from stats (default: 1)
|
|
1189
|
+
--profile-concurrency <n> Concurrent iterations (default: 1 = sequential)
|
|
1190
|
+
--profile-histogram Show ASCII histogram of latency distribution
|
|
1191
|
+
--profile-export <file> Export raw timings to file (.json or .csv)
|
|
1192
|
+
|
|
807
1193
|
${this.logger.color('CI/CD OPTIONS:', 'yellow')}
|
|
808
1194
|
--strict-exit Exit with code 1 if any validation fails (for CI/CD)
|
|
809
1195
|
--fail-on <count> Exit with code 1 if failures exceed this count
|
|
@@ -816,6 +1202,18 @@ ${this.logger.color('SNAPSHOT OPTIONS:', 'yellow')}
|
|
|
816
1202
|
--snapshot-dir <dir> Custom snapshot directory (default: __snapshots__)
|
|
817
1203
|
--ci-snapshot Fail if snapshot is missing (CI mode)
|
|
818
1204
|
|
|
1205
|
+
${this.logger.color('DIFF OPTIONS:', 'yellow')}
|
|
1206
|
+
-d, --diff Enable response diffing (compare with baseline)
|
|
1207
|
+
--diff-save Save current run as baseline
|
|
1208
|
+
--diff-label <name> Label for current run (e.g., 'staging', 'v1.0')
|
|
1209
|
+
--diff-compare <label> Compare against this baseline label
|
|
1210
|
+
--diff-dir <dir> Baseline storage directory (default: __baselines__)
|
|
1211
|
+
--diff-output <format> Output format (terminal|json|markdown)
|
|
1212
|
+
|
|
1213
|
+
${this.logger.color('DIFF SUBCOMMAND:', 'yellow')}
|
|
1214
|
+
curl-runner diff <label1> <label2> [file.yaml]
|
|
1215
|
+
Compare two stored baselines without making requests
|
|
1216
|
+
|
|
819
1217
|
${this.logger.color('EXAMPLES:', 'yellow')}
|
|
820
1218
|
# Run all YAML files in current directory
|
|
821
1219
|
curl-runner
|
|
@@ -865,6 +1263,15 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
|
|
|
865
1263
|
# Watch with custom debounce
|
|
866
1264
|
curl-runner tests/ -w --watch-debounce 500
|
|
867
1265
|
|
|
1266
|
+
# Profile mode - run request 100 times for latency stats
|
|
1267
|
+
curl-runner api.yaml -P 100
|
|
1268
|
+
|
|
1269
|
+
# Profile with 5 warmup iterations and histogram
|
|
1270
|
+
curl-runner api.yaml --profile 50 --profile-warmup 5 --profile-histogram
|
|
1271
|
+
|
|
1272
|
+
# Profile with concurrent iterations and export
|
|
1273
|
+
curl-runner api.yaml -P 100 --profile-concurrency 10 --profile-export results.json
|
|
1274
|
+
|
|
868
1275
|
# Snapshot testing - save and compare responses
|
|
869
1276
|
curl-runner api.yaml --snapshot
|
|
870
1277
|
|
|
@@ -874,6 +1281,21 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
|
|
|
874
1281
|
# CI mode - fail if snapshot missing
|
|
875
1282
|
curl-runner api.yaml --snapshot --ci-snapshot
|
|
876
1283
|
|
|
1284
|
+
# Response diffing - save baseline for staging
|
|
1285
|
+
curl-runner api.yaml --diff-save --diff-label staging
|
|
1286
|
+
|
|
1287
|
+
# Compare current run against staging baseline
|
|
1288
|
+
curl-runner api.yaml --diff --diff-compare staging
|
|
1289
|
+
|
|
1290
|
+
# Compare staging vs production baselines (offline)
|
|
1291
|
+
curl-runner diff staging production api.yaml
|
|
1292
|
+
|
|
1293
|
+
# Auto-diff: creates baseline on first run, compares on subsequent runs
|
|
1294
|
+
curl-runner api.yaml --diff
|
|
1295
|
+
|
|
1296
|
+
# Diff with JSON output for CI
|
|
1297
|
+
curl-runner api.yaml --diff --diff-compare staging --diff-output json
|
|
1298
|
+
|
|
877
1299
|
${this.logger.color('YAML STRUCTURE:', 'yellow')}
|
|
878
1300
|
Single request:
|
|
879
1301
|
request:
|