@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.
Files changed (40) hide show
  1. package/package.json +2 -2
  2. package/src/ci-exit.test.ts +0 -216
  3. package/src/cli.ts +0 -1351
  4. package/src/commands/upgrade.ts +0 -262
  5. package/src/diff/baseline-manager.test.ts +0 -181
  6. package/src/diff/baseline-manager.ts +0 -266
  7. package/src/diff/diff-formatter.ts +0 -316
  8. package/src/diff/index.ts +0 -3
  9. package/src/diff/response-differ.test.ts +0 -330
  10. package/src/diff/response-differ.ts +0 -489
  11. package/src/executor/max-concurrency.test.ts +0 -139
  12. package/src/executor/profile-executor.test.ts +0 -132
  13. package/src/executor/profile-executor.ts +0 -167
  14. package/src/executor/request-executor.ts +0 -663
  15. package/src/parser/yaml.test.ts +0 -480
  16. package/src/parser/yaml.ts +0 -271
  17. package/src/snapshot/index.ts +0 -3
  18. package/src/snapshot/snapshot-differ.test.ts +0 -358
  19. package/src/snapshot/snapshot-differ.ts +0 -296
  20. package/src/snapshot/snapshot-formatter.ts +0 -170
  21. package/src/snapshot/snapshot-manager.test.ts +0 -204
  22. package/src/snapshot/snapshot-manager.ts +0 -342
  23. package/src/types/bun-yaml.d.ts +0 -11
  24. package/src/types/config.ts +0 -638
  25. package/src/utils/colors.ts +0 -30
  26. package/src/utils/condition-evaluator.test.ts +0 -415
  27. package/src/utils/condition-evaluator.ts +0 -327
  28. package/src/utils/curl-builder.test.ts +0 -165
  29. package/src/utils/curl-builder.ts +0 -209
  30. package/src/utils/installation-detector.test.ts +0 -52
  31. package/src/utils/installation-detector.ts +0 -123
  32. package/src/utils/logger.ts +0 -856
  33. package/src/utils/response-store.test.ts +0 -213
  34. package/src/utils/response-store.ts +0 -108
  35. package/src/utils/stats.test.ts +0 -161
  36. package/src/utils/stats.ts +0 -151
  37. package/src/utils/version-checker.ts +0 -158
  38. package/src/version.ts +0 -43
  39. package/src/watcher/file-watcher.test.ts +0 -186
  40. 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
- }
@@ -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
- });
@@ -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
- }