@curl-runner/cli 1.16.0 → 1.16.2
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 +2 -2
- package/src/ci-exit.test.ts +0 -216
- package/src/cli.ts +0 -1351
- package/src/commands/upgrade.ts +0 -262
- package/src/diff/baseline-manager.test.ts +0 -181
- package/src/diff/baseline-manager.ts +0 -266
- package/src/diff/diff-formatter.ts +0 -316
- package/src/diff/index.ts +0 -3
- package/src/diff/response-differ.test.ts +0 -330
- package/src/diff/response-differ.ts +0 -489
- package/src/executor/max-concurrency.test.ts +0 -139
- package/src/executor/profile-executor.test.ts +0 -132
- package/src/executor/profile-executor.ts +0 -167
- package/src/executor/request-executor.ts +0 -663
- package/src/parser/yaml.test.ts +0 -480
- package/src/parser/yaml.ts +0 -271
- package/src/snapshot/index.ts +0 -3
- package/src/snapshot/snapshot-differ.test.ts +0 -358
- package/src/snapshot/snapshot-differ.ts +0 -296
- package/src/snapshot/snapshot-formatter.ts +0 -170
- package/src/snapshot/snapshot-manager.test.ts +0 -204
- package/src/snapshot/snapshot-manager.ts +0 -342
- package/src/types/bun-yaml.d.ts +0 -11
- package/src/types/config.ts +0 -638
- package/src/utils/colors.ts +0 -30
- package/src/utils/condition-evaluator.test.ts +0 -415
- package/src/utils/condition-evaluator.ts +0 -327
- package/src/utils/curl-builder.test.ts +0 -165
- package/src/utils/curl-builder.ts +0 -209
- package/src/utils/installation-detector.test.ts +0 -52
- package/src/utils/installation-detector.ts +0 -123
- package/src/utils/logger.ts +0 -856
- package/src/utils/response-store.test.ts +0 -213
- package/src/utils/response-store.ts +0 -108
- package/src/utils/stats.test.ts +0 -161
- package/src/utils/stats.ts +0 -151
- package/src/utils/version-checker.ts +0 -158
- package/src/version.ts +0 -43
- package/src/watcher/file-watcher.test.ts +0 -186
- package/src/watcher/file-watcher.ts +0 -140
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import type { ProfileConfig } from '../types/config';
|
|
3
|
-
import { calculateProfileStats } from '../utils/stats';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Test the profile stats calculation logic used by ProfileExecutor.
|
|
7
|
-
* Actual HTTP execution is tested via integration tests.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
describe('Profile Stats Integration', () => {
|
|
11
|
-
test('stats calculation matches expected ProfileResult structure', () => {
|
|
12
|
-
const timings = [50, 55, 60, 65, 70, 75, 80, 85, 90, 95];
|
|
13
|
-
const warmup = 2;
|
|
14
|
-
const failures = 0;
|
|
15
|
-
|
|
16
|
-
const stats = calculateProfileStats(timings, warmup, failures);
|
|
17
|
-
|
|
18
|
-
// Verify all ProfileStats fields are present
|
|
19
|
-
expect(stats).toHaveProperty('iterations');
|
|
20
|
-
expect(stats).toHaveProperty('warmup');
|
|
21
|
-
expect(stats).toHaveProperty('min');
|
|
22
|
-
expect(stats).toHaveProperty('max');
|
|
23
|
-
expect(stats).toHaveProperty('mean');
|
|
24
|
-
expect(stats).toHaveProperty('median');
|
|
25
|
-
expect(stats).toHaveProperty('p50');
|
|
26
|
-
expect(stats).toHaveProperty('p95');
|
|
27
|
-
expect(stats).toHaveProperty('p99');
|
|
28
|
-
expect(stats).toHaveProperty('stdDev');
|
|
29
|
-
expect(stats).toHaveProperty('failures');
|
|
30
|
-
expect(stats).toHaveProperty('failureRate');
|
|
31
|
-
expect(stats).toHaveProperty('timings');
|
|
32
|
-
|
|
33
|
-
// Verify warmup exclusion
|
|
34
|
-
expect(stats.iterations).toBe(8); // 10 - 2 warmup
|
|
35
|
-
expect(stats.warmup).toBe(2);
|
|
36
|
-
expect(stats.timings.length).toBe(8);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('profile config defaults are applied correctly', () => {
|
|
40
|
-
const defaultConfig: ProfileConfig = {
|
|
41
|
-
iterations: 10,
|
|
42
|
-
warmup: 1,
|
|
43
|
-
concurrency: 1,
|
|
44
|
-
histogram: false,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
expect(defaultConfig.iterations).toBe(10);
|
|
48
|
-
expect(defaultConfig.warmup).toBe(1);
|
|
49
|
-
expect(defaultConfig.concurrency).toBe(1);
|
|
50
|
-
expect(defaultConfig.histogram).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('concurrent config changes iterations behavior', () => {
|
|
54
|
-
const _sequentialConfig: ProfileConfig = {
|
|
55
|
-
iterations: 100,
|
|
56
|
-
concurrency: 1,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const concurrentConfig: ProfileConfig = {
|
|
60
|
-
iterations: 100,
|
|
61
|
-
concurrency: 10,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// With concurrency 10, iterations should be chunked into 10 parallel batches
|
|
65
|
-
const expectedChunks = Math.ceil(concurrentConfig.iterations / concurrentConfig.concurrency!);
|
|
66
|
-
expect(expectedChunks).toBe(10);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('failure tracking affects failureRate calculation', () => {
|
|
70
|
-
const timings = [10, 20, 30, 40, 50];
|
|
71
|
-
const failures = 2;
|
|
72
|
-
|
|
73
|
-
const stats = calculateProfileStats(timings, 0, failures);
|
|
74
|
-
|
|
75
|
-
// 2 failures out of 5 total (timings) + 2 failures = 7 total iterations
|
|
76
|
-
// But failures are tracked separately, so failureRate = 2/5 = 40% based on timings length
|
|
77
|
-
expect(stats.failures).toBe(2);
|
|
78
|
-
expect(stats.failureRate).toBeGreaterThan(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('warmup iterations are excluded from percentile calculations', () => {
|
|
82
|
-
// First 2 values are outlier warmup times
|
|
83
|
-
const timings = [500, 400, 100, 100, 100, 100, 100, 100, 100, 100];
|
|
84
|
-
const warmup = 2;
|
|
85
|
-
|
|
86
|
-
const stats = calculateProfileStats(timings, warmup, 0);
|
|
87
|
-
|
|
88
|
-
// After excluding warmup, all values are 100
|
|
89
|
-
expect(stats.min).toBe(100);
|
|
90
|
-
expect(stats.max).toBe(100);
|
|
91
|
-
expect(stats.mean).toBe(100);
|
|
92
|
-
expect(stats.p50).toBe(100);
|
|
93
|
-
expect(stats.p95).toBe(100);
|
|
94
|
-
expect(stats.p99).toBe(100);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('export file extension determines format', () => {
|
|
98
|
-
const jsonFile = 'results.json';
|
|
99
|
-
const csvFile = 'results.csv';
|
|
100
|
-
|
|
101
|
-
expect(jsonFile.endsWith('.json')).toBe(true);
|
|
102
|
-
expect(csvFile.endsWith('.csv')).toBe(true);
|
|
103
|
-
expect(jsonFile.endsWith('.csv')).toBe(false);
|
|
104
|
-
expect(csvFile.endsWith('.json')).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('ProfileConfig Validation', () => {
|
|
109
|
-
test('iterations must be positive', () => {
|
|
110
|
-
const validConfig: ProfileConfig = { iterations: 10 };
|
|
111
|
-
const invalidIterations = 0;
|
|
112
|
-
|
|
113
|
-
expect(validConfig.iterations).toBeGreaterThan(0);
|
|
114
|
-
expect(invalidIterations).toBeLessThanOrEqual(0);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('warmup should not exceed iterations', () => {
|
|
118
|
-
const config: ProfileConfig = {
|
|
119
|
-
iterations: 10,
|
|
120
|
-
warmup: 5,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
expect(config.warmup).toBeLessThanOrEqual(config.iterations);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('concurrency defaults to 1 (sequential)', () => {
|
|
127
|
-
const config: ProfileConfig = { iterations: 10 };
|
|
128
|
-
const concurrency = config.concurrency ?? 1;
|
|
129
|
-
|
|
130
|
-
expect(concurrency).toBe(1);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExecutionResult,
|
|
3
|
-
GlobalConfig,
|
|
4
|
-
ProfileConfig,
|
|
5
|
-
ProfileResult,
|
|
6
|
-
RequestConfig,
|
|
7
|
-
} from '../types/config';
|
|
8
|
-
import { CurlBuilder } from '../utils/curl-builder';
|
|
9
|
-
import { Logger } from '../utils/logger';
|
|
10
|
-
import { calculateProfileStats } from '../utils/stats';
|
|
11
|
-
|
|
12
|
-
export class ProfileExecutor {
|
|
13
|
-
private logger: Logger;
|
|
14
|
-
private profileConfig: ProfileConfig;
|
|
15
|
-
|
|
16
|
-
constructor(globalConfig: GlobalConfig, profileConfig: ProfileConfig) {
|
|
17
|
-
this.profileConfig = profileConfig;
|
|
18
|
-
this.logger = new Logger(globalConfig.output);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Execute a single iteration of the request (minimal overhead version).
|
|
23
|
-
* Skips logging and validation for accurate timing.
|
|
24
|
-
*/
|
|
25
|
-
private async executeSingleIteration(config: RequestConfig): Promise<ExecutionResult> {
|
|
26
|
-
const startTime = performance.now();
|
|
27
|
-
const command = CurlBuilder.buildCommand(config);
|
|
28
|
-
const result = await CurlBuilder.executeCurl(command);
|
|
29
|
-
|
|
30
|
-
if (result.success) {
|
|
31
|
-
let body = result.body;
|
|
32
|
-
try {
|
|
33
|
-
if (
|
|
34
|
-
result.headers?.['content-type']?.includes('application/json') ||
|
|
35
|
-
(body && (body.trim().startsWith('{') || body.trim().startsWith('[')))
|
|
36
|
-
) {
|
|
37
|
-
body = JSON.parse(body);
|
|
38
|
-
}
|
|
39
|
-
} catch (_e) {
|
|
40
|
-
// Keep raw body
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
request: config,
|
|
45
|
-
success: true,
|
|
46
|
-
status: result.status,
|
|
47
|
-
headers: result.headers,
|
|
48
|
-
body,
|
|
49
|
-
metrics: {
|
|
50
|
-
...result.metrics,
|
|
51
|
-
duration: performance.now() - startTime,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
request: config,
|
|
58
|
-
success: false,
|
|
59
|
-
error: result.error,
|
|
60
|
-
metrics: {
|
|
61
|
-
duration: performance.now() - startTime,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Execute iterations in chunks for controlled concurrency.
|
|
68
|
-
*/
|
|
69
|
-
private async executeWithConcurrency(
|
|
70
|
-
config: RequestConfig,
|
|
71
|
-
iterations: number,
|
|
72
|
-
concurrency: number,
|
|
73
|
-
): Promise<ExecutionResult[]> {
|
|
74
|
-
const results: ExecutionResult[] = [];
|
|
75
|
-
|
|
76
|
-
for (let i = 0; i < iterations; i += concurrency) {
|
|
77
|
-
const chunkSize = Math.min(concurrency, iterations - i);
|
|
78
|
-
const chunk = await Promise.all(
|
|
79
|
-
Array.from({ length: chunkSize }, () => this.executeSingleIteration(config)),
|
|
80
|
-
);
|
|
81
|
-
results.push(...chunk);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return results;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Profile a single request by running it multiple times.
|
|
89
|
-
*/
|
|
90
|
-
async profileRequest(config: RequestConfig, index = 0): Promise<ProfileResult> {
|
|
91
|
-
const iterations = this.profileConfig.iterations;
|
|
92
|
-
const warmup = this.profileConfig.warmup ?? 1;
|
|
93
|
-
const concurrency = this.profileConfig.concurrency ?? 1;
|
|
94
|
-
const requestName = config.name || `Request ${index + 1}`;
|
|
95
|
-
|
|
96
|
-
this.logger.logProfileStart(requestName, iterations, warmup, concurrency);
|
|
97
|
-
|
|
98
|
-
const results =
|
|
99
|
-
concurrency === 1
|
|
100
|
-
? await this.executeSequentially(config, iterations)
|
|
101
|
-
: await this.executeWithConcurrency(config, iterations, concurrency);
|
|
102
|
-
|
|
103
|
-
// Collect timings and count failures
|
|
104
|
-
const timings: number[] = [];
|
|
105
|
-
let failures = 0;
|
|
106
|
-
|
|
107
|
-
for (const result of results) {
|
|
108
|
-
if (result.success && result.metrics?.duration !== undefined) {
|
|
109
|
-
timings.push(result.metrics.duration);
|
|
110
|
-
} else {
|
|
111
|
-
failures++;
|
|
112
|
-
// Use 0 as placeholder for failed requests (excluded from stats)
|
|
113
|
-
timings.push(0);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Filter out failed timings (0s) for stats calculation
|
|
118
|
-
const successfulTimings = timings
|
|
119
|
-
.map((t, i) => (results[i].success ? t : -1))
|
|
120
|
-
.filter((t) => t >= 0);
|
|
121
|
-
|
|
122
|
-
// Recalculate stats with only successful timings
|
|
123
|
-
const stats = calculateProfileStats(
|
|
124
|
-
successfulTimings,
|
|
125
|
-
Math.min(warmup, successfulTimings.length),
|
|
126
|
-
failures,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
request: config,
|
|
131
|
-
stats,
|
|
132
|
-
iterations: results,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Execute iterations sequentially (default behavior).
|
|
138
|
-
*/
|
|
139
|
-
private async executeSequentially(
|
|
140
|
-
config: RequestConfig,
|
|
141
|
-
iterations: number,
|
|
142
|
-
): Promise<ExecutionResult[]> {
|
|
143
|
-
const results: ExecutionResult[] = [];
|
|
144
|
-
|
|
145
|
-
for (let i = 0; i < iterations; i++) {
|
|
146
|
-
const result = await this.executeSingleIteration(config);
|
|
147
|
-
results.push(result);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return results;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Profile multiple requests.
|
|
155
|
-
*/
|
|
156
|
-
async profileRequests(requests: RequestConfig[]): Promise<ProfileResult[]> {
|
|
157
|
-
const results: ProfileResult[] = [];
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i < requests.length; i++) {
|
|
160
|
-
const result = await this.profileRequest(requests[i], i);
|
|
161
|
-
results.push(result);
|
|
162
|
-
this.logger.logProfileResult(result, this.profileConfig.histogram ?? false);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return results;
|
|
166
|
-
}
|
|
167
|
-
}
|