@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,213 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import type { ExecutionResult } from '../types/config';
|
|
3
|
-
import {
|
|
4
|
-
createStoreContext,
|
|
5
|
-
extractStoreValues,
|
|
6
|
-
getValueByPath,
|
|
7
|
-
mergeStoreContext,
|
|
8
|
-
valueToString,
|
|
9
|
-
} from './response-store';
|
|
10
|
-
|
|
11
|
-
describe('getValueByPath', () => {
|
|
12
|
-
const testObj = {
|
|
13
|
-
status: 200,
|
|
14
|
-
body: {
|
|
15
|
-
id: 123,
|
|
16
|
-
user: {
|
|
17
|
-
name: 'John',
|
|
18
|
-
email: 'john@example.com',
|
|
19
|
-
},
|
|
20
|
-
items: [
|
|
21
|
-
{ id: 1, name: 'Item 1' },
|
|
22
|
-
{ id: 2, name: 'Item 2' },
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
headers: {
|
|
26
|
-
'content-type': 'application/json',
|
|
27
|
-
'x-request-id': 'abc123',
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
test('should get top-level value', () => {
|
|
32
|
-
expect(getValueByPath(testObj, 'status')).toBe(200);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('should get nested value', () => {
|
|
36
|
-
expect(getValueByPath(testObj, 'body.id')).toBe(123);
|
|
37
|
-
expect(getValueByPath(testObj, 'body.user.name')).toBe('John');
|
|
38
|
-
expect(getValueByPath(testObj, 'body.user.email')).toBe('john@example.com');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('should get header value', () => {
|
|
42
|
-
expect(getValueByPath(testObj, 'headers.content-type')).toBe('application/json');
|
|
43
|
-
expect(getValueByPath(testObj, 'headers.x-request-id')).toBe('abc123');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('should get array element by index', () => {
|
|
47
|
-
expect(getValueByPath(testObj, 'body.items.0.id')).toBe(1);
|
|
48
|
-
expect(getValueByPath(testObj, 'body.items.1.name')).toBe('Item 2');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('should get array element using bracket notation', () => {
|
|
52
|
-
expect(getValueByPath(testObj, 'body.items[0].id')).toBe(1);
|
|
53
|
-
expect(getValueByPath(testObj, 'body.items[1].name')).toBe('Item 2');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('should return undefined for non-existent path', () => {
|
|
57
|
-
expect(getValueByPath(testObj, 'body.nonexistent')).toBeUndefined();
|
|
58
|
-
expect(getValueByPath(testObj, 'body.user.age')).toBeUndefined();
|
|
59
|
-
expect(getValueByPath(testObj, 'nonexistent.path')).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('should return undefined for null or undefined object', () => {
|
|
63
|
-
expect(getValueByPath(null, 'any.path')).toBeUndefined();
|
|
64
|
-
expect(getValueByPath(undefined, 'any.path')).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('should handle primitive values correctly', () => {
|
|
68
|
-
expect(getValueByPath('string', 'length')).toBeUndefined();
|
|
69
|
-
expect(getValueByPath(123, 'toString')).toBeUndefined();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('valueToString', () => {
|
|
74
|
-
test('should convert string to string', () => {
|
|
75
|
-
expect(valueToString('hello')).toBe('hello');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('should convert number to string', () => {
|
|
79
|
-
expect(valueToString(123)).toBe('123');
|
|
80
|
-
expect(valueToString(45.67)).toBe('45.67');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('should convert boolean to string', () => {
|
|
84
|
-
expect(valueToString(true)).toBe('true');
|
|
85
|
-
expect(valueToString(false)).toBe('false');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('should convert null and undefined to empty string', () => {
|
|
89
|
-
expect(valueToString(null)).toBe('');
|
|
90
|
-
expect(valueToString(undefined)).toBe('');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('should JSON stringify objects', () => {
|
|
94
|
-
expect(valueToString({ a: 1 })).toBe('{"a":1}');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('should JSON stringify arrays', () => {
|
|
98
|
-
expect(valueToString([1, 2, 3])).toBe('[1,2,3]');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('extractStoreValues', () => {
|
|
103
|
-
const mockResult: ExecutionResult = {
|
|
104
|
-
request: {
|
|
105
|
-
url: 'https://api.example.com/users',
|
|
106
|
-
method: 'POST',
|
|
107
|
-
},
|
|
108
|
-
success: true,
|
|
109
|
-
status: 201,
|
|
110
|
-
headers: {
|
|
111
|
-
'content-type': 'application/json',
|
|
112
|
-
'x-request-id': 'req-12345',
|
|
113
|
-
},
|
|
114
|
-
body: {
|
|
115
|
-
id: 456,
|
|
116
|
-
data: {
|
|
117
|
-
token: 'jwt-token-here',
|
|
118
|
-
user: {
|
|
119
|
-
id: 789,
|
|
120
|
-
name: 'Test User',
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
metrics: {
|
|
125
|
-
duration: 150,
|
|
126
|
-
size: 1024,
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
test('should extract status', () => {
|
|
131
|
-
const result = extractStoreValues(mockResult, {
|
|
132
|
-
statusCode: 'status',
|
|
133
|
-
});
|
|
134
|
-
expect(result.statusCode).toBe('201');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('should extract body fields', () => {
|
|
138
|
-
const result = extractStoreValues(mockResult, {
|
|
139
|
-
userId: 'body.id',
|
|
140
|
-
token: 'body.data.token',
|
|
141
|
-
userName: 'body.data.user.name',
|
|
142
|
-
});
|
|
143
|
-
expect(result.userId).toBe('456');
|
|
144
|
-
expect(result.token).toBe('jwt-token-here');
|
|
145
|
-
expect(result.userName).toBe('Test User');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('should extract header values', () => {
|
|
149
|
-
const result = extractStoreValues(mockResult, {
|
|
150
|
-
contentType: 'headers.content-type',
|
|
151
|
-
requestId: 'headers.x-request-id',
|
|
152
|
-
});
|
|
153
|
-
expect(result.contentType).toBe('application/json');
|
|
154
|
-
expect(result.requestId).toBe('req-12345');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('should extract metrics', () => {
|
|
158
|
-
const result = extractStoreValues(mockResult, {
|
|
159
|
-
duration: 'metrics.duration',
|
|
160
|
-
});
|
|
161
|
-
expect(result.duration).toBe('150');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('should handle non-existent paths', () => {
|
|
165
|
-
const result = extractStoreValues(mockResult, {
|
|
166
|
-
missing: 'body.nonexistent',
|
|
167
|
-
});
|
|
168
|
-
expect(result.missing).toBe('');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('should extract multiple values', () => {
|
|
172
|
-
const result = extractStoreValues(mockResult, {
|
|
173
|
-
id: 'body.id',
|
|
174
|
-
status: 'status',
|
|
175
|
-
contentType: 'headers.content-type',
|
|
176
|
-
});
|
|
177
|
-
expect(result.id).toBe('456');
|
|
178
|
-
expect(result.status).toBe('201');
|
|
179
|
-
expect(result.contentType).toBe('application/json');
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('createStoreContext', () => {
|
|
184
|
-
test('should create empty context', () => {
|
|
185
|
-
const context = createStoreContext();
|
|
186
|
-
expect(context).toEqual({});
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('mergeStoreContext', () => {
|
|
191
|
-
test('should merge contexts', () => {
|
|
192
|
-
const existing = { a: '1', b: '2' };
|
|
193
|
-
const newValues = { c: '3', d: '4' };
|
|
194
|
-
const merged = mergeStoreContext(existing, newValues);
|
|
195
|
-
expect(merged).toEqual({ a: '1', b: '2', c: '3', d: '4' });
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('should override existing values', () => {
|
|
199
|
-
const existing = { a: '1', b: '2' };
|
|
200
|
-
const newValues = { b: 'new', c: '3' };
|
|
201
|
-
const merged = mergeStoreContext(existing, newValues);
|
|
202
|
-
expect(merged).toEqual({ a: '1', b: 'new', c: '3' });
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('should not mutate original contexts', () => {
|
|
206
|
-
const existing = { a: '1' };
|
|
207
|
-
const newValues = { b: '2' };
|
|
208
|
-
const merged = mergeStoreContext(existing, newValues);
|
|
209
|
-
expect(existing).toEqual({ a: '1' });
|
|
210
|
-
expect(newValues).toEqual({ b: '2' });
|
|
211
|
-
expect(merged).toEqual({ a: '1', b: '2' });
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { ExecutionResult, ResponseStoreContext, StoreConfig } from '../types/config';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extracts a value from an object using a dot-notation path.
|
|
5
|
-
* Supports paths like: "body.id", "body.data.token", "headers.content-type", "status"
|
|
6
|
-
*
|
|
7
|
-
* @param obj - The object to extract from
|
|
8
|
-
* @param path - Dot-notation path to the value
|
|
9
|
-
* @returns The extracted value or undefined if not found
|
|
10
|
-
*/
|
|
11
|
-
export function getValueByPath(obj: unknown, path: string): unknown {
|
|
12
|
-
const parts = path.split('.');
|
|
13
|
-
let current: unknown = obj;
|
|
14
|
-
|
|
15
|
-
for (const part of parts) {
|
|
16
|
-
if (current === null || current === undefined) {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (typeof current !== 'object') {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Handle array index access like "items.0.id" or "items[0].id"
|
|
25
|
-
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
26
|
-
if (arrayMatch) {
|
|
27
|
-
const [, key, indexStr] = arrayMatch;
|
|
28
|
-
const index = Number.parseInt(indexStr, 10);
|
|
29
|
-
current = (current as Record<string, unknown>)[key];
|
|
30
|
-
if (Array.isArray(current)) {
|
|
31
|
-
current = current[index];
|
|
32
|
-
} else {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
} else if (/^\d+$/.test(part) && Array.isArray(current)) {
|
|
36
|
-
// Direct numeric index for arrays
|
|
37
|
-
current = current[Number.parseInt(part, 10)];
|
|
38
|
-
} else {
|
|
39
|
-
current = (current as Record<string, unknown>)[part];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return current;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Converts a value to a string for storage.
|
|
48
|
-
* Objects and arrays are JSON stringified.
|
|
49
|
-
*/
|
|
50
|
-
export function valueToString(value: unknown): string {
|
|
51
|
-
if (value === undefined || value === null) {
|
|
52
|
-
return '';
|
|
53
|
-
}
|
|
54
|
-
if (typeof value === 'string') {
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
58
|
-
return String(value);
|
|
59
|
-
}
|
|
60
|
-
return JSON.stringify(value);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extracts values from an execution result based on the store configuration.
|
|
65
|
-
*
|
|
66
|
-
* @param result - The execution result to extract from
|
|
67
|
-
* @param storeConfig - Configuration mapping variable names to JSON paths
|
|
68
|
-
* @returns Object containing the extracted values as strings
|
|
69
|
-
*/
|
|
70
|
-
export function extractStoreValues(
|
|
71
|
-
result: ExecutionResult,
|
|
72
|
-
storeConfig: StoreConfig,
|
|
73
|
-
): ResponseStoreContext {
|
|
74
|
-
const extracted: ResponseStoreContext = {};
|
|
75
|
-
|
|
76
|
-
// Build an object that represents the full response structure
|
|
77
|
-
const responseObj: Record<string, unknown> = {
|
|
78
|
-
status: result.status,
|
|
79
|
-
headers: result.headers || {},
|
|
80
|
-
body: result.body,
|
|
81
|
-
metrics: result.metrics,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
for (const [varName, path] of Object.entries(storeConfig)) {
|
|
85
|
-
const value = getValueByPath(responseObj, path);
|
|
86
|
-
extracted[varName] = valueToString(value);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return extracted;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Creates a new response store context.
|
|
94
|
-
*/
|
|
95
|
-
export function createStoreContext(): ResponseStoreContext {
|
|
96
|
-
return {};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Merges new values into an existing store context.
|
|
101
|
-
* New values override existing ones with the same key.
|
|
102
|
-
*/
|
|
103
|
-
export function mergeStoreContext(
|
|
104
|
-
existing: ResponseStoreContext,
|
|
105
|
-
newValues: ResponseStoreContext,
|
|
106
|
-
): ResponseStoreContext {
|
|
107
|
-
return { ...existing, ...newValues };
|
|
108
|
-
}
|
package/src/utils/stats.test.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
calculateMean,
|
|
4
|
-
calculatePercentile,
|
|
5
|
-
calculateProfileStats,
|
|
6
|
-
calculateStdDev,
|
|
7
|
-
exportToCSV,
|
|
8
|
-
exportToJSON,
|
|
9
|
-
generateHistogram,
|
|
10
|
-
} from './stats';
|
|
11
|
-
|
|
12
|
-
describe('calculatePercentile', () => {
|
|
13
|
-
test('returns 0 for empty array', () => {
|
|
14
|
-
expect(calculatePercentile([], 50)).toBe(0);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('returns single value for array of 1', () => {
|
|
18
|
-
expect(calculatePercentile([100], 50)).toBe(100);
|
|
19
|
-
expect(calculatePercentile([100], 99)).toBe(100);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('calculates p50 (median) correctly', () => {
|
|
23
|
-
expect(calculatePercentile([1, 2, 3, 4, 5], 50)).toBe(3);
|
|
24
|
-
expect(calculatePercentile([1, 2, 3, 4], 50)).toBe(2.5);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('calculates p95 correctly', () => {
|
|
28
|
-
const values = Array.from({ length: 100 }, (_, i) => i + 1);
|
|
29
|
-
expect(calculatePercentile(values, 95)).toBeCloseTo(95.05, 1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('calculates p99 correctly', () => {
|
|
33
|
-
const values = Array.from({ length: 100 }, (_, i) => i + 1);
|
|
34
|
-
expect(calculatePercentile(values, 99)).toBeCloseTo(99.01, 1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('handles unsorted input (requires pre-sorting)', () => {
|
|
38
|
-
// Note: function expects sorted input
|
|
39
|
-
const sorted = [10, 20, 30, 40, 50].sort((a, b) => a - b);
|
|
40
|
-
expect(calculatePercentile(sorted, 50)).toBe(30);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('calculateMean', () => {
|
|
45
|
-
test('returns 0 for empty array', () => {
|
|
46
|
-
expect(calculateMean([])).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('calculates mean correctly', () => {
|
|
50
|
-
expect(calculateMean([1, 2, 3, 4, 5])).toBe(3);
|
|
51
|
-
expect(calculateMean([10, 20, 30])).toBe(20);
|
|
52
|
-
expect(calculateMean([100])).toBe(100);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('calculateStdDev', () => {
|
|
57
|
-
test('returns 0 for empty array', () => {
|
|
58
|
-
expect(calculateStdDev([], 0)).toBe(0);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('returns 0 for single value', () => {
|
|
62
|
-
expect(calculateStdDev([100], 100)).toBe(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('calculates standard deviation correctly', () => {
|
|
66
|
-
const values = [2, 4, 4, 4, 5, 5, 7, 9];
|
|
67
|
-
const mean = calculateMean(values);
|
|
68
|
-
expect(calculateStdDev(values, mean)).toBeCloseTo(2, 0);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('calculateProfileStats', () => {
|
|
73
|
-
test('calculates stats correctly with no warmup', () => {
|
|
74
|
-
const timings = [10, 20, 30, 40, 50];
|
|
75
|
-
const stats = calculateProfileStats(timings, 0, 0);
|
|
76
|
-
|
|
77
|
-
expect(stats.iterations).toBe(5);
|
|
78
|
-
expect(stats.warmup).toBe(0);
|
|
79
|
-
expect(stats.min).toBe(10);
|
|
80
|
-
expect(stats.max).toBe(50);
|
|
81
|
-
expect(stats.mean).toBe(30);
|
|
82
|
-
expect(stats.failures).toBe(0);
|
|
83
|
-
expect(stats.failureRate).toBe(0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('excludes warmup iterations from stats', () => {
|
|
87
|
-
const timings = [100, 10, 20, 30, 40]; // First value is warmup outlier
|
|
88
|
-
const stats = calculateProfileStats(timings, 1, 0);
|
|
89
|
-
|
|
90
|
-
expect(stats.iterations).toBe(4);
|
|
91
|
-
expect(stats.warmup).toBe(1);
|
|
92
|
-
expect(stats.min).toBe(10);
|
|
93
|
-
expect(stats.max).toBe(40);
|
|
94
|
-
expect(stats.mean).toBe(25);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('calculates failure rate correctly', () => {
|
|
98
|
-
const timings = [10, 20, 30];
|
|
99
|
-
const stats = calculateProfileStats(timings, 0, 2);
|
|
100
|
-
|
|
101
|
-
expect(stats.failures).toBe(2);
|
|
102
|
-
expect(stats.failureRate).toBeCloseTo(66.67, 1);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('handles empty timings', () => {
|
|
106
|
-
const stats = calculateProfileStats([], 0, 0);
|
|
107
|
-
|
|
108
|
-
expect(stats.iterations).toBe(0);
|
|
109
|
-
expect(stats.min).toBe(0);
|
|
110
|
-
expect(stats.max).toBe(0);
|
|
111
|
-
expect(stats.mean).toBe(0);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('generateHistogram', () => {
|
|
116
|
-
test('returns "No data" for empty array', () => {
|
|
117
|
-
const result = generateHistogram([]);
|
|
118
|
-
expect(result).toEqual(['No data']);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('generates histogram with correct bucket count', () => {
|
|
122
|
-
const timings = Array.from({ length: 100 }, (_, i) => i);
|
|
123
|
-
const result = generateHistogram(timings, 5, 20);
|
|
124
|
-
|
|
125
|
-
expect(result.length).toBe(5);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('histogram lines contain bucket ranges', () => {
|
|
129
|
-
const timings = [10, 20, 30, 40, 50];
|
|
130
|
-
const result = generateHistogram(timings, 2, 10);
|
|
131
|
-
|
|
132
|
-
expect(result[0]).toContain('ms -');
|
|
133
|
-
expect(result[0]).toContain('ms │');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('exportToCSV', () => {
|
|
138
|
-
test('exports stats to CSV format', () => {
|
|
139
|
-
const stats = calculateProfileStats([10, 20, 30], 0, 0);
|
|
140
|
-
const csv = exportToCSV(stats, 'Test Request');
|
|
141
|
-
|
|
142
|
-
expect(csv).toContain('iteration,latency_ms');
|
|
143
|
-
expect(csv).toContain('1,10');
|
|
144
|
-
expect(csv).toContain('2,20');
|
|
145
|
-
expect(csv).toContain('3,30');
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('exportToJSON', () => {
|
|
150
|
-
test('exports stats to JSON format', () => {
|
|
151
|
-
const stats = calculateProfileStats([10, 20, 30], 0, 0);
|
|
152
|
-
const json = exportToJSON(stats, 'Test Request');
|
|
153
|
-
const parsed = JSON.parse(json);
|
|
154
|
-
|
|
155
|
-
expect(parsed.request).toBe('Test Request');
|
|
156
|
-
expect(parsed.summary.iterations).toBe(3);
|
|
157
|
-
expect(parsed.summary.min).toBe(10);
|
|
158
|
-
expect(parsed.summary.max).toBe(30);
|
|
159
|
-
expect(parsed.timings).toEqual([10, 20, 30]);
|
|
160
|
-
});
|
|
161
|
-
});
|
package/src/utils/stats.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import type { ProfileStats } from '../types/config';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Calculate percentile from sorted array.
|
|
5
|
-
* Uses linear interpolation for non-integer indices.
|
|
6
|
-
*/
|
|
7
|
-
export function calculatePercentile(sorted: number[], percentile: number): number {
|
|
8
|
-
if (sorted.length === 0) {
|
|
9
|
-
return 0;
|
|
10
|
-
}
|
|
11
|
-
if (sorted.length === 1) {
|
|
12
|
-
return sorted[0];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const index = (percentile / 100) * (sorted.length - 1);
|
|
16
|
-
const lower = Math.floor(index);
|
|
17
|
-
const upper = Math.ceil(index);
|
|
18
|
-
const fraction = index - lower;
|
|
19
|
-
|
|
20
|
-
if (lower === upper) {
|
|
21
|
-
return sorted[lower];
|
|
22
|
-
}
|
|
23
|
-
return sorted[lower] * (1 - fraction) + sorted[upper] * fraction;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Calculate arithmetic mean.
|
|
28
|
-
*/
|
|
29
|
-
export function calculateMean(values: number[]): number {
|
|
30
|
-
if (values.length === 0) {
|
|
31
|
-
return 0;
|
|
32
|
-
}
|
|
33
|
-
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Calculate standard deviation.
|
|
38
|
-
*/
|
|
39
|
-
export function calculateStdDev(values: number[], mean: number): number {
|
|
40
|
-
if (values.length <= 1) {
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
const squaredDiffs = values.map((v) => (v - mean) ** 2);
|
|
44
|
-
const variance = squaredDiffs.reduce((sum, v) => sum + v, 0) / values.length;
|
|
45
|
-
return Math.sqrt(variance);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Calculate profile statistics from raw timings.
|
|
50
|
-
*/
|
|
51
|
-
export function calculateProfileStats(
|
|
52
|
-
timings: number[],
|
|
53
|
-
warmup: number,
|
|
54
|
-
failures: number,
|
|
55
|
-
): ProfileStats {
|
|
56
|
-
// Exclude warmup iterations
|
|
57
|
-
const effectiveTimings = timings.slice(warmup);
|
|
58
|
-
const sorted = [...effectiveTimings].sort((a, b) => a - b);
|
|
59
|
-
|
|
60
|
-
const mean = calculateMean(sorted);
|
|
61
|
-
const totalIterations = timings.length;
|
|
62
|
-
const effectiveIterations = effectiveTimings.length;
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
iterations: effectiveIterations,
|
|
66
|
-
warmup,
|
|
67
|
-
min: sorted.length > 0 ? sorted[0] : 0,
|
|
68
|
-
max: sorted.length > 0 ? sorted[sorted.length - 1] : 0,
|
|
69
|
-
mean: Math.round(mean * 100) / 100,
|
|
70
|
-
median: Math.round(calculatePercentile(sorted, 50) * 100) / 100,
|
|
71
|
-
p50: Math.round(calculatePercentile(sorted, 50) * 100) / 100,
|
|
72
|
-
p95: Math.round(calculatePercentile(sorted, 95) * 100) / 100,
|
|
73
|
-
p99: Math.round(calculatePercentile(sorted, 99) * 100) / 100,
|
|
74
|
-
stdDev: Math.round(calculateStdDev(sorted, mean) * 100) / 100,
|
|
75
|
-
failures,
|
|
76
|
-
failureRate: totalIterations > 0 ? Math.round((failures / totalIterations) * 10000) / 100 : 0,
|
|
77
|
-
timings: effectiveTimings,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Generate ASCII histogram for latency distribution.
|
|
83
|
-
*/
|
|
84
|
-
export function generateHistogram(timings: number[], buckets = 10, width = 40): string[] {
|
|
85
|
-
if (timings.length === 0) {
|
|
86
|
-
return ['No data'];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const min = Math.min(...timings);
|
|
90
|
-
const max = Math.max(...timings);
|
|
91
|
-
const range = max - min || 1;
|
|
92
|
-
const bucketSize = range / buckets;
|
|
93
|
-
|
|
94
|
-
// Count values per bucket
|
|
95
|
-
const counts = new Array(buckets).fill(0);
|
|
96
|
-
for (const t of timings) {
|
|
97
|
-
const bucket = Math.min(Math.floor((t - min) / bucketSize), buckets - 1);
|
|
98
|
-
counts[bucket]++;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const maxCount = Math.max(...counts);
|
|
102
|
-
const lines: string[] = [];
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < buckets; i++) {
|
|
105
|
-
const bucketMin = min + i * bucketSize;
|
|
106
|
-
const bucketMax = min + (i + 1) * bucketSize;
|
|
107
|
-
const barLength = maxCount > 0 ? Math.round((counts[i] / maxCount) * width) : 0;
|
|
108
|
-
const bar = '█'.repeat(barLength);
|
|
109
|
-
const label = `${bucketMin.toFixed(0).padStart(6)}ms - ${bucketMax.toFixed(0).padStart(6)}ms`;
|
|
110
|
-
lines.push(`${label} │${bar} ${counts[i]}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return lines;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Export stats to CSV format.
|
|
118
|
-
*/
|
|
119
|
-
export function exportToCSV(stats: ProfileStats, _requestName: string): string {
|
|
120
|
-
const headers = ['iteration', 'latency_ms'];
|
|
121
|
-
const rows = stats.timings.map((t, i) => `${i + 1},${t}`);
|
|
122
|
-
return [headers.join(','), ...rows].join('\n');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Export stats to JSON format.
|
|
127
|
-
*/
|
|
128
|
-
export function exportToJSON(stats: ProfileStats, requestName: string): string {
|
|
129
|
-
return JSON.stringify(
|
|
130
|
-
{
|
|
131
|
-
request: requestName,
|
|
132
|
-
summary: {
|
|
133
|
-
iterations: stats.iterations,
|
|
134
|
-
warmup: stats.warmup,
|
|
135
|
-
failures: stats.failures,
|
|
136
|
-
failureRate: stats.failureRate,
|
|
137
|
-
min: stats.min,
|
|
138
|
-
max: stats.max,
|
|
139
|
-
mean: stats.mean,
|
|
140
|
-
median: stats.median,
|
|
141
|
-
p50: stats.p50,
|
|
142
|
-
p95: stats.p95,
|
|
143
|
-
p99: stats.p99,
|
|
144
|
-
stdDev: stats.stdDev,
|
|
145
|
-
},
|
|
146
|
-
timings: stats.timings,
|
|
147
|
-
},
|
|
148
|
-
null,
|
|
149
|
-
2,
|
|
150
|
-
);
|
|
151
|
-
}
|