@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,489 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Baseline,
|
|
3
|
-
DiffCompareResult,
|
|
4
|
-
DiffConfig,
|
|
5
|
-
DiffSummary,
|
|
6
|
-
ExecutionResult,
|
|
7
|
-
GlobalDiffConfig,
|
|
8
|
-
JsonValue,
|
|
9
|
-
ResponseDiff,
|
|
10
|
-
} from '../types/config';
|
|
11
|
-
import { BaselineManager } from './baseline-manager';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Compares responses against baselines with support for exclusions and match rules.
|
|
15
|
-
*/
|
|
16
|
-
export class ResponseDiffer {
|
|
17
|
-
private excludePaths: Set<string>;
|
|
18
|
-
private matchRules: Map<string, string>;
|
|
19
|
-
private includeTimings: boolean;
|
|
20
|
-
|
|
21
|
-
constructor(config: DiffConfig) {
|
|
22
|
-
this.excludePaths = new Set(config.exclude || []);
|
|
23
|
-
this.matchRules = new Map(Object.entries(config.match || {}));
|
|
24
|
-
this.includeTimings = config.includeTimings || false;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compares current response against baseline.
|
|
29
|
-
*/
|
|
30
|
-
compare(
|
|
31
|
-
baseline: Baseline,
|
|
32
|
-
current: Baseline,
|
|
33
|
-
baselineLabel: string,
|
|
34
|
-
currentLabel: string,
|
|
35
|
-
requestName: string,
|
|
36
|
-
): DiffCompareResult {
|
|
37
|
-
const differences: ResponseDiff[] = [];
|
|
38
|
-
|
|
39
|
-
// Compare status
|
|
40
|
-
if (baseline.status !== undefined || current.status !== undefined) {
|
|
41
|
-
if (baseline.status !== current.status && !this.isExcluded('status')) {
|
|
42
|
-
differences.push({
|
|
43
|
-
path: 'status',
|
|
44
|
-
baseline: baseline.status,
|
|
45
|
-
current: current.status,
|
|
46
|
-
type: 'changed',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Compare headers
|
|
52
|
-
if (baseline.headers || current.headers) {
|
|
53
|
-
const headerDiffs = this.compareObjects(
|
|
54
|
-
baseline.headers || {},
|
|
55
|
-
current.headers || {},
|
|
56
|
-
'headers',
|
|
57
|
-
);
|
|
58
|
-
differences.push(...headerDiffs);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Compare body
|
|
62
|
-
if (baseline.body !== undefined || current.body !== undefined) {
|
|
63
|
-
const bodyDiffs = this.deepCompare(baseline.body, current.body, 'body');
|
|
64
|
-
differences.push(...bodyDiffs);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const result: DiffCompareResult = {
|
|
68
|
-
requestName,
|
|
69
|
-
hasDifferences: differences.length > 0,
|
|
70
|
-
isNewBaseline: false,
|
|
71
|
-
baselineLabel,
|
|
72
|
-
currentLabel,
|
|
73
|
-
differences,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Add timing diff if enabled
|
|
77
|
-
if (this.includeTimings && baseline.timing !== undefined && current.timing !== undefined) {
|
|
78
|
-
const changePercent = ((current.timing - baseline.timing) / baseline.timing) * 100;
|
|
79
|
-
result.timingDiff = {
|
|
80
|
-
baseline: baseline.timing,
|
|
81
|
-
current: current.timing,
|
|
82
|
-
changePercent,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Deep comparison of two values with path tracking.
|
|
91
|
-
*/
|
|
92
|
-
deepCompare(baseline: unknown, current: unknown, path: string): ResponseDiff[] {
|
|
93
|
-
if (this.isExcluded(path)) {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (this.matchesRule(path, current)) {
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (baseline === null && current === null) {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
if (baseline === undefined && current === undefined) {
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const baselineType = this.getType(baseline);
|
|
109
|
-
const currentType = this.getType(current);
|
|
110
|
-
|
|
111
|
-
if (baselineType !== currentType) {
|
|
112
|
-
return [
|
|
113
|
-
{
|
|
114
|
-
path,
|
|
115
|
-
baseline,
|
|
116
|
-
current,
|
|
117
|
-
type: 'type_mismatch',
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (baselineType !== 'object' && baselineType !== 'array') {
|
|
123
|
-
if (baseline !== current) {
|
|
124
|
-
return [
|
|
125
|
-
{
|
|
126
|
-
path,
|
|
127
|
-
baseline,
|
|
128
|
-
current,
|
|
129
|
-
type: 'changed',
|
|
130
|
-
},
|
|
131
|
-
];
|
|
132
|
-
}
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (baselineType === 'array') {
|
|
137
|
-
return this.compareArrays(baseline as JsonValue[], current as JsonValue[], path);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return this.compareObjects(
|
|
141
|
-
baseline as Record<string, unknown>,
|
|
142
|
-
current as Record<string, unknown>,
|
|
143
|
-
path,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Compares two arrays.
|
|
149
|
-
*/
|
|
150
|
-
private compareArrays(baseline: JsonValue[], current: JsonValue[], path: string): ResponseDiff[] {
|
|
151
|
-
const differences: ResponseDiff[] = [];
|
|
152
|
-
const maxLen = Math.max(baseline.length, current.length);
|
|
153
|
-
|
|
154
|
-
for (let i = 0; i < maxLen; i++) {
|
|
155
|
-
const itemPath = `${path}[${i}]`;
|
|
156
|
-
|
|
157
|
-
if (i >= baseline.length) {
|
|
158
|
-
if (!this.isExcluded(itemPath)) {
|
|
159
|
-
differences.push({
|
|
160
|
-
path: itemPath,
|
|
161
|
-
baseline: undefined,
|
|
162
|
-
current: current[i],
|
|
163
|
-
type: 'added',
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
} else if (i >= current.length) {
|
|
167
|
-
if (!this.isExcluded(itemPath)) {
|
|
168
|
-
differences.push({
|
|
169
|
-
path: itemPath,
|
|
170
|
-
baseline: baseline[i],
|
|
171
|
-
current: undefined,
|
|
172
|
-
type: 'removed',
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
const itemDiffs = this.deepCompare(baseline[i], current[i], itemPath);
|
|
177
|
-
differences.push(...itemDiffs);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return differences;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Compares two objects.
|
|
186
|
-
*/
|
|
187
|
-
private compareObjects(
|
|
188
|
-
baseline: Record<string, unknown>,
|
|
189
|
-
current: Record<string, unknown>,
|
|
190
|
-
path: string,
|
|
191
|
-
): ResponseDiff[] {
|
|
192
|
-
const differences: ResponseDiff[] = [];
|
|
193
|
-
const allKeys = new Set([...Object.keys(baseline), ...Object.keys(current)]);
|
|
194
|
-
|
|
195
|
-
for (const key of allKeys) {
|
|
196
|
-
const keyPath = path ? `${path}.${key}` : key;
|
|
197
|
-
const hasBaseline = key in baseline;
|
|
198
|
-
const hasCurrent = key in current;
|
|
199
|
-
|
|
200
|
-
if (!hasBaseline && hasCurrent) {
|
|
201
|
-
if (!this.isExcluded(keyPath)) {
|
|
202
|
-
differences.push({
|
|
203
|
-
path: keyPath,
|
|
204
|
-
baseline: undefined,
|
|
205
|
-
current: current[key],
|
|
206
|
-
type: 'added',
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
} else if (hasBaseline && !hasCurrent) {
|
|
210
|
-
if (!this.isExcluded(keyPath)) {
|
|
211
|
-
differences.push({
|
|
212
|
-
path: keyPath,
|
|
213
|
-
baseline: baseline[key],
|
|
214
|
-
current: undefined,
|
|
215
|
-
type: 'removed',
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
const keyDiffs = this.deepCompare(baseline[key], current[key], keyPath);
|
|
220
|
-
differences.push(...keyDiffs);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return differences;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Checks if a path should be excluded from comparison.
|
|
229
|
-
*/
|
|
230
|
-
isExcluded(path: string): boolean {
|
|
231
|
-
if (this.excludePaths.has(path)) {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (const pattern of this.excludePaths) {
|
|
236
|
-
if (pattern.startsWith('*.')) {
|
|
237
|
-
const suffix = pattern.slice(2);
|
|
238
|
-
if (path.endsWith(`.${suffix}`)) {
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
const lastPart = path.split('.').pop();
|
|
242
|
-
if (lastPart === suffix) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (pattern.includes('[*]')) {
|
|
248
|
-
const regex = new RegExp(
|
|
249
|
-
`^${pattern.replace(/\[\*\]/g, '\\[\\d+\\]').replace(/\./g, '\\.')}$`,
|
|
250
|
-
);
|
|
251
|
-
if (regex.test(path)) {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Checks if a value matches a custom rule for its path.
|
|
262
|
-
*/
|
|
263
|
-
matchesRule(path: string, value: unknown): boolean {
|
|
264
|
-
const rule = this.matchRules.get(path);
|
|
265
|
-
if (!rule) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (rule === '*') {
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (rule.startsWith('regex:')) {
|
|
274
|
-
const pattern = rule.slice(6);
|
|
275
|
-
try {
|
|
276
|
-
const regex = new RegExp(pattern);
|
|
277
|
-
return regex.test(String(value));
|
|
278
|
-
} catch {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Gets the type of a value for comparison.
|
|
288
|
-
*/
|
|
289
|
-
private getType(value: unknown): string {
|
|
290
|
-
if (value === null) {
|
|
291
|
-
return 'null';
|
|
292
|
-
}
|
|
293
|
-
if (value === undefined) {
|
|
294
|
-
return 'undefined';
|
|
295
|
-
}
|
|
296
|
-
if (Array.isArray(value)) {
|
|
297
|
-
return 'array';
|
|
298
|
-
}
|
|
299
|
-
return typeof value;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Orchestrates response diffing between runs.
|
|
305
|
-
*/
|
|
306
|
-
export class DiffOrchestrator {
|
|
307
|
-
private manager: BaselineManager;
|
|
308
|
-
|
|
309
|
-
constructor(globalConfig: GlobalDiffConfig = {}) {
|
|
310
|
-
this.manager = new BaselineManager(globalConfig);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Compares execution results against a baseline.
|
|
315
|
-
*/
|
|
316
|
-
async compareWithBaseline(
|
|
317
|
-
yamlPath: string,
|
|
318
|
-
results: ExecutionResult[],
|
|
319
|
-
currentLabel: string,
|
|
320
|
-
baselineLabel: string,
|
|
321
|
-
config: DiffConfig,
|
|
322
|
-
): Promise<DiffSummary> {
|
|
323
|
-
const baselineFile = await this.manager.load(yamlPath, baselineLabel);
|
|
324
|
-
const diffResults: DiffCompareResult[] = [];
|
|
325
|
-
|
|
326
|
-
for (const result of results) {
|
|
327
|
-
if (result.skipped || !result.success) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const requestName = result.request.name || result.request.url;
|
|
332
|
-
const currentBaseline = this.manager.createBaseline(result, config);
|
|
333
|
-
|
|
334
|
-
if (!baselineFile) {
|
|
335
|
-
// No baseline exists - mark as new
|
|
336
|
-
diffResults.push({
|
|
337
|
-
requestName,
|
|
338
|
-
hasDifferences: false,
|
|
339
|
-
isNewBaseline: true,
|
|
340
|
-
baselineLabel,
|
|
341
|
-
currentLabel,
|
|
342
|
-
differences: [],
|
|
343
|
-
});
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const storedBaseline = baselineFile.baselines[requestName];
|
|
348
|
-
|
|
349
|
-
if (!storedBaseline) {
|
|
350
|
-
// Request not in baseline - mark as new
|
|
351
|
-
diffResults.push({
|
|
352
|
-
requestName,
|
|
353
|
-
hasDifferences: false,
|
|
354
|
-
isNewBaseline: true,
|
|
355
|
-
baselineLabel,
|
|
356
|
-
currentLabel,
|
|
357
|
-
differences: [],
|
|
358
|
-
});
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const differ = new ResponseDiffer(config);
|
|
363
|
-
const compareResult = differ.compare(
|
|
364
|
-
storedBaseline,
|
|
365
|
-
currentBaseline,
|
|
366
|
-
baselineLabel,
|
|
367
|
-
currentLabel,
|
|
368
|
-
requestName,
|
|
369
|
-
);
|
|
370
|
-
diffResults.push(compareResult);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
totalRequests: diffResults.length,
|
|
375
|
-
unchanged: diffResults.filter((r) => !r.hasDifferences && !r.isNewBaseline).length,
|
|
376
|
-
changed: diffResults.filter((r) => r.hasDifferences).length,
|
|
377
|
-
newBaselines: diffResults.filter((r) => r.isNewBaseline).length,
|
|
378
|
-
results: diffResults,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Compares two stored baselines (offline comparison).
|
|
384
|
-
*/
|
|
385
|
-
async compareTwoBaselines(
|
|
386
|
-
yamlPath: string,
|
|
387
|
-
label1: string,
|
|
388
|
-
label2: string,
|
|
389
|
-
config: DiffConfig,
|
|
390
|
-
): Promise<DiffSummary> {
|
|
391
|
-
const file1 = await this.manager.load(yamlPath, label1);
|
|
392
|
-
const file2 = await this.manager.load(yamlPath, label2);
|
|
393
|
-
|
|
394
|
-
if (!file1) {
|
|
395
|
-
throw new Error(`Baseline '${label1}' not found`);
|
|
396
|
-
}
|
|
397
|
-
if (!file2) {
|
|
398
|
-
throw new Error(`Baseline '${label2}' not found`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const allRequestNames = new Set([
|
|
402
|
-
...Object.keys(file1.baselines),
|
|
403
|
-
...Object.keys(file2.baselines),
|
|
404
|
-
]);
|
|
405
|
-
|
|
406
|
-
const diffResults: DiffCompareResult[] = [];
|
|
407
|
-
const differ = new ResponseDiffer(config);
|
|
408
|
-
|
|
409
|
-
for (const requestName of allRequestNames) {
|
|
410
|
-
const baseline1 = file1.baselines[requestName];
|
|
411
|
-
const baseline2 = file2.baselines[requestName];
|
|
412
|
-
|
|
413
|
-
if (!baseline1) {
|
|
414
|
-
diffResults.push({
|
|
415
|
-
requestName,
|
|
416
|
-
hasDifferences: true,
|
|
417
|
-
isNewBaseline: false,
|
|
418
|
-
baselineLabel: label1,
|
|
419
|
-
currentLabel: label2,
|
|
420
|
-
differences: [
|
|
421
|
-
{
|
|
422
|
-
path: '',
|
|
423
|
-
baseline: undefined,
|
|
424
|
-
current: 'exists',
|
|
425
|
-
type: 'added',
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
});
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (!baseline2) {
|
|
433
|
-
diffResults.push({
|
|
434
|
-
requestName,
|
|
435
|
-
hasDifferences: true,
|
|
436
|
-
isNewBaseline: false,
|
|
437
|
-
baselineLabel: label1,
|
|
438
|
-
currentLabel: label2,
|
|
439
|
-
differences: [
|
|
440
|
-
{
|
|
441
|
-
path: '',
|
|
442
|
-
baseline: 'exists',
|
|
443
|
-
current: undefined,
|
|
444
|
-
type: 'removed',
|
|
445
|
-
},
|
|
446
|
-
],
|
|
447
|
-
});
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const compareResult = differ.compare(baseline1, baseline2, label1, label2, requestName);
|
|
452
|
-
diffResults.push(compareResult);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return {
|
|
456
|
-
totalRequests: diffResults.length,
|
|
457
|
-
unchanged: diffResults.filter((r) => !r.hasDifferences).length,
|
|
458
|
-
changed: diffResults.filter((r) => r.hasDifferences).length,
|
|
459
|
-
newBaselines: 0,
|
|
460
|
-
results: diffResults,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Saves current results as baseline.
|
|
466
|
-
*/
|
|
467
|
-
async saveBaseline(
|
|
468
|
-
yamlPath: string,
|
|
469
|
-
label: string,
|
|
470
|
-
results: ExecutionResult[],
|
|
471
|
-
config: DiffConfig,
|
|
472
|
-
): Promise<void> {
|
|
473
|
-
await this.manager.saveBaseline(yamlPath, label, results, config);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Lists available baseline labels.
|
|
478
|
-
*/
|
|
479
|
-
async listLabels(yamlPath: string): Promise<string[]> {
|
|
480
|
-
return this.manager.listLabels(yamlPath);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Gets the baseline manager instance.
|
|
485
|
-
*/
|
|
486
|
-
getManager(): BaselineManager {
|
|
487
|
-
return this.manager;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import type { GlobalConfig, RequestConfig } from '../types/config';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Simulates the chunked execution logic from executeParallel
|
|
6
|
-
* Returns the chunks that would be created based on maxConcurrency
|
|
7
|
-
*/
|
|
8
|
-
function getExecutionChunks(
|
|
9
|
-
requests: RequestConfig[],
|
|
10
|
-
maxConcurrency: number | undefined,
|
|
11
|
-
): RequestConfig[][] {
|
|
12
|
-
if (!maxConcurrency || maxConcurrency >= requests.length) {
|
|
13
|
-
return [requests]; // All requests in a single batch
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const chunks: RequestConfig[][] = [];
|
|
17
|
-
for (let i = 0; i < requests.length; i += maxConcurrency) {
|
|
18
|
-
chunks.push(requests.slice(i, i + maxConcurrency));
|
|
19
|
-
}
|
|
20
|
-
return chunks;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates mock requests for testing
|
|
25
|
-
*/
|
|
26
|
-
function createMockRequests(count: number): RequestConfig[] {
|
|
27
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
28
|
-
url: `https://api.example.com/request/${i + 1}`,
|
|
29
|
-
method: 'GET' as const,
|
|
30
|
-
name: `Request ${i + 1}`,
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('maxConcurrency parallel execution', () => {
|
|
35
|
-
describe('chunk creation', () => {
|
|
36
|
-
test('should execute all requests at once when maxConcurrency is not set', () => {
|
|
37
|
-
const requests = createMockRequests(10);
|
|
38
|
-
const chunks = getExecutionChunks(requests, undefined);
|
|
39
|
-
expect(chunks).toHaveLength(1);
|
|
40
|
-
expect(chunks[0]).toHaveLength(10);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('should execute all requests at once when maxConcurrency >= requests.length', () => {
|
|
44
|
-
const requests = createMockRequests(5);
|
|
45
|
-
const chunks = getExecutionChunks(requests, 10);
|
|
46
|
-
expect(chunks).toHaveLength(1);
|
|
47
|
-
expect(chunks[0]).toHaveLength(5);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('should create correct chunks when maxConcurrency is 1', () => {
|
|
51
|
-
const requests = createMockRequests(3);
|
|
52
|
-
const chunks = getExecutionChunks(requests, 1);
|
|
53
|
-
expect(chunks).toHaveLength(3);
|
|
54
|
-
expect(chunks[0]).toHaveLength(1);
|
|
55
|
-
expect(chunks[1]).toHaveLength(1);
|
|
56
|
-
expect(chunks[2]).toHaveLength(1);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('should create correct chunks when maxConcurrency is 2', () => {
|
|
60
|
-
const requests = createMockRequests(5);
|
|
61
|
-
const chunks = getExecutionChunks(requests, 2);
|
|
62
|
-
expect(chunks).toHaveLength(3);
|
|
63
|
-
expect(chunks[0]).toHaveLength(2);
|
|
64
|
-
expect(chunks[1]).toHaveLength(2);
|
|
65
|
-
expect(chunks[2]).toHaveLength(1);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('should create correct chunks when maxConcurrency is 3', () => {
|
|
69
|
-
const requests = createMockRequests(10);
|
|
70
|
-
const chunks = getExecutionChunks(requests, 3);
|
|
71
|
-
expect(chunks).toHaveLength(4);
|
|
72
|
-
expect(chunks[0]).toHaveLength(3);
|
|
73
|
-
expect(chunks[1]).toHaveLength(3);
|
|
74
|
-
expect(chunks[2]).toHaveLength(3);
|
|
75
|
-
expect(chunks[3]).toHaveLength(1);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('should handle edge case with maxConcurrency equal to requests length', () => {
|
|
79
|
-
const requests = createMockRequests(5);
|
|
80
|
-
const chunks = getExecutionChunks(requests, 5);
|
|
81
|
-
expect(chunks).toHaveLength(1);
|
|
82
|
-
expect(chunks[0]).toHaveLength(5);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('should preserve request order across chunks', () => {
|
|
86
|
-
const requests = createMockRequests(6);
|
|
87
|
-
const chunks = getExecutionChunks(requests, 2);
|
|
88
|
-
|
|
89
|
-
// Verify order is preserved
|
|
90
|
-
expect(chunks[0][0].name).toBe('Request 1');
|
|
91
|
-
expect(chunks[0][1].name).toBe('Request 2');
|
|
92
|
-
expect(chunks[1][0].name).toBe('Request 3');
|
|
93
|
-
expect(chunks[1][1].name).toBe('Request 4');
|
|
94
|
-
expect(chunks[2][0].name).toBe('Request 5');
|
|
95
|
-
expect(chunks[2][1].name).toBe('Request 6');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('GlobalConfig maxConcurrency validation', () => {
|
|
100
|
-
test('should accept valid maxConcurrency values', () => {
|
|
101
|
-
const config1: GlobalConfig = { execution: 'parallel', maxConcurrency: 1 };
|
|
102
|
-
expect(config1.maxConcurrency).toBe(1);
|
|
103
|
-
|
|
104
|
-
const config2: GlobalConfig = { execution: 'parallel', maxConcurrency: 5 };
|
|
105
|
-
expect(config2.maxConcurrency).toBe(5);
|
|
106
|
-
|
|
107
|
-
const config3: GlobalConfig = { execution: 'parallel', maxConcurrency: 100 };
|
|
108
|
-
expect(config3.maxConcurrency).toBe(100);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('should allow undefined maxConcurrency', () => {
|
|
112
|
-
const config: GlobalConfig = { execution: 'parallel' };
|
|
113
|
-
expect(config.maxConcurrency).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('integration with other settings', () => {
|
|
118
|
-
test('maxConcurrency should coexist with continueOnError', () => {
|
|
119
|
-
const config: GlobalConfig = {
|
|
120
|
-
execution: 'parallel',
|
|
121
|
-
maxConcurrency: 5,
|
|
122
|
-
continueOnError: true,
|
|
123
|
-
};
|
|
124
|
-
expect(config.maxConcurrency).toBe(5);
|
|
125
|
-
expect(config.continueOnError).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('maxConcurrency should coexist with CI settings', () => {
|
|
129
|
-
const config: GlobalConfig = {
|
|
130
|
-
execution: 'parallel',
|
|
131
|
-
maxConcurrency: 3,
|
|
132
|
-
ci: { strictExit: true, failOn: 2 },
|
|
133
|
-
};
|
|
134
|
-
expect(config.maxConcurrency).toBe(3);
|
|
135
|
-
expect(config.ci?.strictExit).toBe(true);
|
|
136
|
-
expect(config.ci?.failOn).toBe(2);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
});
|