@flemist/test-variants 2.0.4 → 3.0.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.
@@ -7,28 +7,28 @@ import { generateErrorVariantFilePath, readErrorVariantFiles, parseErrorVariantF
7
7
  import * as path from 'path';
8
8
  import 'fs';
9
9
 
10
+ function formatDuration(ms) {
11
+ const seconds = ms / 1000;
12
+ if (seconds < 60) {
13
+ return `${seconds.toFixed(1)}s`;
14
+ }
15
+ const minutes = seconds / 60;
16
+ if (minutes < 60) {
17
+ return `${minutes.toFixed(1)}m`;
18
+ }
19
+ const hours = minutes / 60;
20
+ return `${hours.toFixed(1)}h`;
21
+ }
10
22
  function testVariantsRun(testRun, variants, options = {}) {
11
23
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
12
24
  return __awaiter(this, void 0, void 0, function* () {
13
25
  const saveErrorVariants = options.saveErrorVariants;
14
26
  const retriesPerVariant = (_a = saveErrorVariants === null || saveErrorVariants === void 0 ? void 0 : saveErrorVariants.retriesPerVariant) !== null && _a !== void 0 ? _a : 1;
27
+ const useToFindBestError = saveErrorVariants === null || saveErrorVariants === void 0 ? void 0 : saveErrorVariants.useToFindBestError;
15
28
  const sessionDate = new Date();
16
29
  const errorVariantFilePath = saveErrorVariants
17
30
  ? path.resolve(saveErrorVariants.dir, (_c = (_b = saveErrorVariants.getFilePath) === null || _b === void 0 ? void 0 : _b.call(saveErrorVariants, { sessionDate })) !== null && _c !== void 0 ? _c : generateErrorVariantFilePath({ sessionDate }))
18
31
  : null;
19
- // Replay phase: run previously saved error variants before normal iteration
20
- if (saveErrorVariants) {
21
- const files = yield readErrorVariantFiles(saveErrorVariants.dir);
22
- for (const filePath of files) {
23
- const args = yield parseErrorVariantFile(filePath, saveErrorVariants.jsonToArgs);
24
- for (let retry = 0; retry < retriesPerVariant; retry++) {
25
- const promiseOrResult = testRun(args, -1, null);
26
- if (isPromiseLike(promiseOrResult)) {
27
- yield promiseOrResult;
28
- }
29
- }
30
- }
31
- }
32
32
  const GC_Iterations = (_d = options.GC_Iterations) !== null && _d !== void 0 ? _d : 1000000;
33
33
  const GC_IterationsAsync = (_e = options.GC_IterationsAsync) !== null && _e !== void 0 ? _e : 10000;
34
34
  const GC_Interval = (_f = options.GC_Interval) !== null && _f !== void 0 ? _f : 1000;
@@ -36,80 +36,47 @@ function testVariantsRun(testRun, variants, options = {}) {
36
36
  const logCompleted = (_h = options.logCompleted) !== null && _h !== void 0 ? _h : true;
37
37
  const abortSignalExternal = options.abortSignal;
38
38
  const findBestError = options.findBestError;
39
+ const cycles = (_j = findBestError === null || findBestError === void 0 ? void 0 : findBestError.cycles) !== null && _j !== void 0 ? _j : 1;
39
40
  const parallel = options.parallel === true
40
41
  ? Math.pow(2, 31)
41
42
  : !options.parallel || options.parallel <= 0
42
43
  ? 1
43
44
  : options.parallel;
44
- const limitVariantsCount = (_j = options.limitVariantsCount) !== null && _j !== void 0 ? _j : null;
45
- let prevCycleVariantsCount = null;
46
- let cycleIndex = 0;
47
- let repeatIndex = 0;
48
- let seed = void 0;
49
- let bestError = null;
50
- let index = -1;
51
- let args = {};
52
- let variantsIterator = variants[Symbol.iterator]();
53
- function getLimitVariantsCount() {
54
- if (limitVariantsCount != null && bestError != null) {
55
- return Math.min(limitVariantsCount, bestError.index);
56
- }
57
- if (limitVariantsCount != null) {
58
- return limitVariantsCount;
59
- }
60
- if (bestError != null) {
61
- return bestError.index;
62
- }
63
- return null;
45
+ // Apply initial limits
46
+ if (options.limitVariantsCount != null) {
47
+ variants.addLimit({ index: options.limitVariantsCount });
64
48
  }
65
- function nextVariant() {
66
- while (true) {
67
- // Try next repeat for current variant
68
- if (findBestError && index >= 0 && (bestError == null || index < bestError.index)) {
69
- repeatIndex++;
70
- if (repeatIndex < findBestError.repeatsPerVariant) {
71
- seed = findBestError.getSeed({
72
- variantIndex: index,
73
- cycleIndex,
74
- repeatIndex,
75
- totalIndex: cycleIndex * findBestError.repeatsPerVariant + repeatIndex,
76
- });
77
- return true;
49
+ // Replay phase: run previously saved error variants before normal iteration
50
+ if (saveErrorVariants) {
51
+ const files = yield readErrorVariantFiles(saveErrorVariants.dir);
52
+ for (const filePath of files) {
53
+ const args = yield parseErrorVariantFile(filePath, saveErrorVariants.jsonToArgs);
54
+ for (let retry = 0; retry < retriesPerVariant; retry++) {
55
+ try {
56
+ const promiseOrResult = testRun(args, -1, null);
57
+ if (isPromiseLike(promiseOrResult)) {
58
+ yield promiseOrResult;
59
+ }
78
60
  }
79
- }
80
- repeatIndex = 0;
81
- index++;
82
- if (findBestError && cycleIndex >= findBestError.cycles) {
83
- return false;
84
- }
85
- const _limitVariantsCount = getLimitVariantsCount();
86
- if (_limitVariantsCount == null || index < _limitVariantsCount) {
87
- const result = variantsIterator.next();
88
- if (!result.done) {
89
- args = result.value;
90
- if (findBestError) {
91
- seed = findBestError.getSeed({
92
- variantIndex: index,
93
- cycleIndex,
94
- repeatIndex,
95
- totalIndex: cycleIndex * findBestError.repeatsPerVariant + repeatIndex,
96
- });
61
+ catch (error) {
62
+ if (useToFindBestError && findBestError) {
63
+ // Store as pending limit for findBestError cycle
64
+ variants.addLimit({ args, error });
65
+ break; // Exit retry loop, continue to next file
66
+ }
67
+ else {
68
+ throw error;
97
69
  }
98
- return true;
99
70
  }
100
71
  }
101
- if (!findBestError) {
102
- return false;
103
- }
104
- prevCycleVariantsCount = index;
105
- cycleIndex++;
106
- if (cycleIndex >= findBestError.cycles) {
107
- return false;
108
- }
109
- index = -1;
110
- variantsIterator = variants[Symbol.iterator]();
72
+ // If no error occurred during replays, the saved variant is no longer reproducible
73
+ // (templates may have changed) - silently skip
111
74
  }
112
75
  }
76
+ let prevCycleVariantsCount = null;
77
+ let prevCycleDuration = null;
78
+ const startTime = Date.now();
79
+ let cycleStartTime = startTime;
113
80
  const abortControllerParallel = new AbortControllerFast();
114
81
  const abortSignalParallel = combineAbortSignals(abortSignalExternal, abortControllerParallel.signal);
115
82
  const abortSignalAll = abortSignalParallel;
@@ -125,50 +92,105 @@ function testVariantsRun(testRun, variants, options = {}) {
125
92
  : new Pool(parallel);
126
93
  function onCompleted() {
127
94
  if (logCompleted) {
128
- console.log(`[test-variants] variants: ${index}, iterations: ${iterations}, async: ${iterationsAsync}`);
95
+ console.log(`[test-variants] variants: ${variants.index}, iterations: ${iterations}, async: ${iterationsAsync}`);
129
96
  }
130
97
  }
131
- function next() {
132
- return __awaiter(this, void 0, void 0, function* () {
133
- while (!(abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) && (debug || nextVariant())) {
134
- const _index = index;
135
- const _args = Object.assign(Object.assign({}, args), { seed });
136
- const now = (logInterval || GC_Interval) && Date.now();
137
- if (logInterval && now - prevLogTime >= logInterval) {
138
- // the log is required to prevent the karma browserNoActivityTimeout
139
- let log = '';
140
- if (findBestError) {
141
- log += `cycle: ${cycleIndex}, variant: ${index}`;
142
- if (cycleIndex > 0) {
143
- let max = getLimitVariantsCount();
144
- if (max != null) {
145
- if (prevCycleVariantsCount != null && prevCycleVariantsCount < max) {
146
- max = prevCycleVariantsCount;
147
- }
148
- log += `/${max}`;
149
- }
98
+ // Main iteration using iterator
99
+ variants.start();
100
+ while (variants.cycleIndex < cycles) {
101
+ let args;
102
+ while (!(abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) && (debug || (args = variants.next()) != null)) {
103
+ const _index = variants.index;
104
+ const _args = args;
105
+ const now = (logInterval || GC_Interval) && Date.now();
106
+ if (logInterval && now - prevLogTime >= logInterval) {
107
+ // the log is required to prevent the karma browserNoActivityTimeout
108
+ let log = '';
109
+ const cycleElapsed = now - cycleStartTime;
110
+ const totalElapsed = now - startTime;
111
+ if (findBestError) {
112
+ log += `cycle: ${variants.cycleIndex}, variant: ${variants.index}`;
113
+ let max = variants.count;
114
+ if (max != null) {
115
+ if (prevCycleVariantsCount != null && prevCycleVariantsCount < max) {
116
+ max = prevCycleVariantsCount;
150
117
  }
151
118
  }
119
+ if (max != null && variants.index > 0) {
120
+ let estimatedCycleTime;
121
+ if (prevCycleDuration != null && prevCycleVariantsCount != null
122
+ && variants.index < prevCycleVariantsCount && cycleElapsed < prevCycleDuration) {
123
+ const adjustedDuration = prevCycleDuration - cycleElapsed;
124
+ const adjustedCount = prevCycleVariantsCount - variants.index;
125
+ const speedForRemaining = adjustedDuration / adjustedCount;
126
+ const remainingTime = (max - variants.index) * speedForRemaining;
127
+ estimatedCycleTime = cycleElapsed + remainingTime;
128
+ }
129
+ else {
130
+ estimatedCycleTime = cycleElapsed * max / variants.index;
131
+ }
132
+ log += `/${max} (${formatDuration(cycleElapsed)}/${formatDuration(estimatedCycleTime)})`;
133
+ }
152
134
  else {
153
- log += `variant: ${index}`;
135
+ log += ` (${formatDuration(cycleElapsed)})`;
154
136
  }
155
- log += `, total: ${iterations}`;
156
- console.log(log);
157
- prevLogTime = now;
158
137
  }
159
- if (GC_Iterations && iterations - prevGC_Iterations >= GC_Iterations
160
- || GC_IterationsAsync && iterationsAsync - prevGC_IterationsAsync >= GC_IterationsAsync
161
- || GC_Interval && now - prevGC_Time >= GC_Interval) {
162
- prevGC_Iterations = iterations;
163
- prevGC_IterationsAsync = iterationsAsync;
164
- prevGC_Time = now;
165
- yield garbageCollect(1);
138
+ else {
139
+ log += `variant: ${variants.index} (${formatDuration(cycleElapsed)})`;
140
+ }
141
+ log += `, total: ${iterations} (${formatDuration(totalElapsed)})`;
142
+ console.log(log);
143
+ prevLogTime = now;
144
+ }
145
+ if (GC_Iterations && iterations - prevGC_Iterations >= GC_Iterations
146
+ || GC_IterationsAsync && iterationsAsync - prevGC_IterationsAsync >= GC_IterationsAsync
147
+ || GC_Interval && now - prevGC_Time >= GC_Interval) {
148
+ prevGC_Iterations = iterations;
149
+ prevGC_IterationsAsync = iterationsAsync;
150
+ prevGC_Time = now;
151
+ yield garbageCollect(1);
152
+ }
153
+ if (abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) {
154
+ continue;
155
+ }
156
+ if (!pool || abortSignalParallel.aborted) {
157
+ try {
158
+ let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
159
+ if (isPromiseLike(promiseOrIterations)) {
160
+ promiseOrIterations = yield promiseOrIterations;
161
+ }
162
+ if (!promiseOrIterations) {
163
+ debug = true;
164
+ abortControllerParallel.abort();
165
+ continue;
166
+ }
167
+ const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
168
+ iterationsAsync += _iterationsAsync;
169
+ iterations += _iterationsSync + _iterationsAsync;
166
170
  }
167
- if (abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) {
168
- continue;
171
+ catch (err) {
172
+ if (errorVariantFilePath) {
173
+ yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
174
+ }
175
+ if (findBestError) {
176
+ variants.addLimit({ error: err });
177
+ debug = false;
178
+ }
179
+ else {
180
+ throw err;
181
+ }
169
182
  }
170
- if (!pool || abortSignalParallel.aborted) {
183
+ }
184
+ else {
185
+ if (!pool.hold(1)) {
186
+ yield pool.holdWait(1);
187
+ }
188
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
189
+ void (() => __awaiter(this, void 0, void 0, function* () {
171
190
  try {
191
+ if (abortSignalParallel === null || abortSignalParallel === void 0 ? void 0 : abortSignalParallel.aborted) {
192
+ return;
193
+ }
172
194
  let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
173
195
  if (isPromiseLike(promiseOrIterations)) {
174
196
  promiseOrIterations = yield promiseOrIterations;
@@ -176,7 +198,7 @@ function testVariantsRun(testRun, variants, options = {}) {
176
198
  if (!promiseOrIterations) {
177
199
  debug = true;
178
200
  abortControllerParallel.abort();
179
- continue;
201
+ return;
180
202
  }
181
203
  const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
182
204
  iterationsAsync += _iterationsAsync;
@@ -187,78 +209,44 @@ function testVariantsRun(testRun, variants, options = {}) {
187
209
  yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
188
210
  }
189
211
  if (findBestError) {
190
- bestError = {
191
- error: err,
192
- args: _args,
193
- index: _index,
194
- };
212
+ variants.addLimit({ error: err });
195
213
  debug = false;
196
214
  }
197
215
  else {
198
216
  throw err;
199
217
  }
200
218
  }
201
- }
202
- else {
203
- if (!pool.hold(1)) {
204
- yield pool.holdWait(1);
219
+ finally {
220
+ void pool.release(1);
205
221
  }
206
- // eslint-disable-next-line @typescript-eslint/no-loop-func
207
- void (() => __awaiter(this, void 0, void 0, function* () {
208
- try {
209
- if (abortSignalParallel === null || abortSignalParallel === void 0 ? void 0 : abortSignalParallel.aborted) {
210
- return;
211
- }
212
- let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
213
- if (isPromiseLike(promiseOrIterations)) {
214
- promiseOrIterations = yield promiseOrIterations;
215
- }
216
- if (!promiseOrIterations) {
217
- debug = true;
218
- abortControllerParallel.abort();
219
- return;
220
- }
221
- const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
222
- iterationsAsync += _iterationsAsync;
223
- iterations += _iterationsSync + _iterationsAsync;
224
- }
225
- catch (err) {
226
- if (errorVariantFilePath) {
227
- yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
228
- }
229
- if (findBestError) {
230
- bestError = {
231
- error: err,
232
- args: _args,
233
- index: _index,
234
- };
235
- debug = false;
236
- }
237
- else {
238
- throw err;
239
- }
240
- }
241
- finally {
242
- void pool.release(1);
243
- }
244
- }))();
245
- }
222
+ }))();
246
223
  }
247
- if (pool) {
248
- yield pool.holdWait(parallel);
249
- void pool.release(parallel);
250
- }
251
- if (abortSignalAll === null || abortSignalAll === void 0 ? void 0 : abortSignalAll.aborted) {
252
- throw abortSignalAll.reason;
253
- }
254
- onCompleted();
255
- yield garbageCollect(1);
256
- return iterations;
257
- });
224
+ }
225
+ // Track cycle metrics for logging
226
+ prevCycleVariantsCount = variants.count;
227
+ prevCycleDuration = Date.now() - cycleStartTime;
228
+ cycleStartTime = Date.now();
229
+ variants.start();
230
+ }
231
+ if (pool) {
232
+ yield pool.holdWait(parallel);
233
+ void pool.release(parallel);
258
234
  }
259
- const result = yield next();
235
+ if (abortSignalAll === null || abortSignalAll === void 0 ? void 0 : abortSignalAll.aborted) {
236
+ throw abortSignalAll.reason;
237
+ }
238
+ onCompleted();
239
+ yield garbageCollect(1);
240
+ // Construct bestError from iterator state
241
+ const bestError = variants.limit
242
+ ? {
243
+ error: variants.limit.error,
244
+ args: variants.limit.args,
245
+ index: variants.count,
246
+ }
247
+ : null;
260
248
  return {
261
- iterations: result,
249
+ iterations,
262
250
  bestError,
263
251
  };
264
252
  });
@@ -15,4 +15,6 @@ export declare type SaveErrorVariantsOptions<Args, SavedArgs = Args> = {
15
15
  argsToJson?: null | ((args: Args) => string | SavedArgs);
16
16
  /** Transform parsed JSON back to args */
17
17
  jsonToArgs?: null | ((json: SavedArgs) => Args);
18
+ /** Use saved errors to set findBestError limits instead of throwing on replay */
19
+ useToFindBestError?: null | boolean;
18
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flemist/test-variants",
3
- "version": "2.0.4",
3
+ "version": "3.0.0",
4
4
  "description": "Runs a test function with all possible combinations of its parameters.",
5
5
  "main": "dist/lib/index.cjs",
6
6
  "module": "dist/lib/index.mjs",