@flemist/test-variants 2.0.5 → 3.0.1

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.
@@ -24,23 +24,11 @@ function testVariantsRun(testRun, variants, options = {}) {
24
24
  return __awaiter(this, void 0, void 0, function* () {
25
25
  const saveErrorVariants = options.saveErrorVariants;
26
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;
27
28
  const sessionDate = new Date();
28
29
  const errorVariantFilePath = saveErrorVariants
29
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 }))
30
31
  : null;
31
- // Replay phase: run previously saved error variants before normal iteration
32
- if (saveErrorVariants) {
33
- const files = yield readErrorVariantFiles(saveErrorVariants.dir);
34
- for (const filePath of files) {
35
- const args = yield parseErrorVariantFile(filePath, saveErrorVariants.jsonToArgs);
36
- for (let retry = 0; retry < retriesPerVariant; retry++) {
37
- const promiseOrResult = testRun(args, -1, null);
38
- if (isPromiseLike(promiseOrResult)) {
39
- yield promiseOrResult;
40
- }
41
- }
42
- }
43
- }
44
32
  const GC_Iterations = (_d = options.GC_Iterations) !== null && _d !== void 0 ? _d : 1000000;
45
33
  const GC_IterationsAsync = (_e = options.GC_IterationsAsync) !== null && _e !== void 0 ? _e : 10000;
46
34
  const GC_Interval = (_f = options.GC_Interval) !== null && _f !== void 0 ? _f : 1000;
@@ -48,85 +36,49 @@ function testVariantsRun(testRun, variants, options = {}) {
48
36
  const logCompleted = (_h = options.logCompleted) !== null && _h !== void 0 ? _h : true;
49
37
  const abortSignalExternal = options.abortSignal;
50
38
  const findBestError = options.findBestError;
39
+ const cycles = (_j = findBestError === null || findBestError === void 0 ? void 0 : findBestError.cycles) !== null && _j !== void 0 ? _j : 1;
40
+ const dontThrowIfError = findBestError === null || findBestError === void 0 ? void 0 : findBestError.dontThrowIfError;
41
+ const limitTime = options.limitTime;
51
42
  const parallel = options.parallel === true
52
43
  ? Math.pow(2, 31)
53
44
  : !options.parallel || options.parallel <= 0
54
45
  ? 1
55
46
  : options.parallel;
56
- const limitVariantsCount = (_j = options.limitVariantsCount) !== null && _j !== void 0 ? _j : null;
57
- let prevCycleVariantsCount = null;
58
- let prevCycleDuration = null;
59
- let cycleIndex = 0;
60
- let repeatIndex = 0;
61
- const startTime = Date.now();
62
- let cycleStartTime = startTime;
63
- let seed = void 0;
64
- let bestError = null;
65
- let index = -1;
66
- let args = {};
67
- let variantsIterator = variants[Symbol.iterator]();
68
- function getLimitVariantsCount() {
69
- if (limitVariantsCount != null && bestError != null) {
70
- return Math.min(limitVariantsCount, bestError.index);
71
- }
72
- if (limitVariantsCount != null) {
73
- return limitVariantsCount;
74
- }
75
- if (bestError != null) {
76
- return bestError.index;
77
- }
78
- return null;
47
+ // Apply initial limits
48
+ if (options.limitVariantsCount != null) {
49
+ variants.addLimit({ index: options.limitVariantsCount });
79
50
  }
80
- function nextVariant() {
81
- while (true) {
82
- // Try next repeat for current variant
83
- if (findBestError && index >= 0 && (bestError == null || index < bestError.index)) {
84
- repeatIndex++;
85
- if (repeatIndex < findBestError.repeatsPerVariant) {
86
- seed = findBestError.getSeed({
87
- variantIndex: index,
88
- cycleIndex,
89
- repeatIndex,
90
- totalIndex: cycleIndex * findBestError.repeatsPerVariant + repeatIndex,
91
- });
92
- return true;
51
+ // Replay phase: run previously saved error variants before normal iteration
52
+ if (saveErrorVariants) {
53
+ const files = yield readErrorVariantFiles(saveErrorVariants.dir);
54
+ for (const filePath of files) {
55
+ const args = yield parseErrorVariantFile(filePath, saveErrorVariants.jsonToArgs);
56
+ for (let retry = 0; retry < retriesPerVariant; retry++) {
57
+ try {
58
+ const promiseOrResult = testRun(args, -1, null);
59
+ if (isPromiseLike(promiseOrResult)) {
60
+ yield promiseOrResult;
61
+ }
93
62
  }
94
- }
95
- repeatIndex = 0;
96
- index++;
97
- if (findBestError && cycleIndex >= findBestError.cycles) {
98
- return false;
99
- }
100
- const _limitVariantsCount = getLimitVariantsCount();
101
- if (_limitVariantsCount == null || index < _limitVariantsCount) {
102
- const result = variantsIterator.next();
103
- if (!result.done) {
104
- args = result.value;
105
- if (findBestError) {
106
- seed = findBestError.getSeed({
107
- variantIndex: index,
108
- cycleIndex,
109
- repeatIndex,
110
- totalIndex: cycleIndex * findBestError.repeatsPerVariant + repeatIndex,
111
- });
63
+ catch (error) {
64
+ if (useToFindBestError && findBestError) {
65
+ // Store as pending limit for findBestError cycle
66
+ variants.addLimit({ args, error });
67
+ break; // Exit retry loop, continue to next file
68
+ }
69
+ else {
70
+ throw error;
112
71
  }
113
- return true;
114
72
  }
115
73
  }
116
- if (!findBestError) {
117
- return false;
118
- }
119
- prevCycleVariantsCount = index;
120
- prevCycleDuration = Date.now() - cycleStartTime;
121
- cycleIndex++;
122
- cycleStartTime = Date.now();
123
- if (cycleIndex >= findBestError.cycles) {
124
- return false;
125
- }
126
- index = -1;
127
- variantsIterator = variants[Symbol.iterator]();
74
+ // If no error occurred during replays, the saved variant is no longer reproducible
75
+ // (templates may have changed) - silently skip
128
76
  }
129
77
  }
78
+ let prevCycleVariantsCount = null;
79
+ let prevCycleDuration = null;
80
+ const startTime = Date.now();
81
+ let cycleStartTime = startTime;
130
82
  const abortControllerParallel = new AbortControllerFast();
131
83
  const abortSignalParallel = combineAbortSignals(abortSignalExternal, abortControllerParallel.signal);
132
84
  const abortSignalAll = abortSignalParallel;
@@ -142,67 +94,110 @@ function testVariantsRun(testRun, variants, options = {}) {
142
94
  : new Pool(parallel);
143
95
  function onCompleted() {
144
96
  if (logCompleted) {
145
- console.log(`[test-variants] variants: ${index}, iterations: ${iterations}, async: ${iterationsAsync}`);
97
+ console.log(`[test-variants] variants: ${variants.index}, iterations: ${iterations}, async: ${iterationsAsync}`);
146
98
  }
147
99
  }
148
- function next() {
149
- return __awaiter(this, void 0, void 0, function* () {
150
- while (!(abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) && (debug || nextVariant())) {
151
- const _index = index;
152
- const _args = Object.assign(Object.assign({}, args), { seed });
153
- const now = (logInterval || GC_Interval) && Date.now();
154
- if (logInterval && now - prevLogTime >= logInterval) {
155
- // the log is required to prevent the karma browserNoActivityTimeout
156
- let log = '';
157
- const cycleElapsed = now - cycleStartTime;
158
- const totalElapsed = now - startTime;
159
- if (findBestError) {
160
- log += `cycle: ${cycleIndex}, variant: ${index}`;
161
- let max = getLimitVariantsCount();
162
- if (max != null) {
163
- if (prevCycleVariantsCount != null && prevCycleVariantsCount < max) {
164
- max = prevCycleVariantsCount;
165
- }
100
+ // Main iteration using iterator
101
+ let timeLimitExceeded = false;
102
+ variants.start();
103
+ while (variants.cycleIndex < cycles && !timeLimitExceeded) {
104
+ let args;
105
+ while (!(abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) && (debug || (args = variants.next()) != null)) {
106
+ const _index = variants.index;
107
+ const _args = args;
108
+ const now = (logInterval || GC_Interval || limitTime) && Date.now();
109
+ if (limitTime && now - startTime >= limitTime) {
110
+ timeLimitExceeded = true;
111
+ break;
112
+ }
113
+ if (logInterval && now - prevLogTime >= logInterval) {
114
+ // the log is required to prevent the karma browserNoActivityTimeout
115
+ let log = '';
116
+ const cycleElapsed = now - cycleStartTime;
117
+ const totalElapsed = now - startTime;
118
+ if (findBestError) {
119
+ log += `cycle: ${variants.cycleIndex}, variant: ${variants.index}`;
120
+ let max = variants.count;
121
+ if (max != null) {
122
+ if (prevCycleVariantsCount != null && prevCycleVariantsCount < max) {
123
+ max = prevCycleVariantsCount;
166
124
  }
167
- if (max != null && index > 0) {
168
- let estimatedCycleTime;
169
- if (prevCycleDuration != null && prevCycleVariantsCount != null
170
- && index < prevCycleVariantsCount && cycleElapsed < prevCycleDuration) {
171
- const adjustedDuration = prevCycleDuration - cycleElapsed;
172
- const adjustedCount = prevCycleVariantsCount - index;
173
- const speedForRemaining = adjustedDuration / adjustedCount;
174
- const remainingTime = (max - index) * speedForRemaining;
175
- estimatedCycleTime = cycleElapsed + remainingTime;
176
- }
177
- else {
178
- estimatedCycleTime = cycleElapsed * max / index;
179
- }
180
- log += `/${max} (${formatDuration(cycleElapsed)}/${formatDuration(estimatedCycleTime)})`;
125
+ }
126
+ if (max != null && variants.index > 0) {
127
+ let estimatedCycleTime;
128
+ if (prevCycleDuration != null && prevCycleVariantsCount != null
129
+ && variants.index < prevCycleVariantsCount && cycleElapsed < prevCycleDuration) {
130
+ const adjustedDuration = prevCycleDuration - cycleElapsed;
131
+ const adjustedCount = prevCycleVariantsCount - variants.index;
132
+ const speedForRemaining = adjustedDuration / adjustedCount;
133
+ const remainingTime = (max - variants.index) * speedForRemaining;
134
+ estimatedCycleTime = cycleElapsed + remainingTime;
181
135
  }
182
136
  else {
183
- log += ` (${formatDuration(cycleElapsed)})`;
137
+ estimatedCycleTime = cycleElapsed * max / variants.index;
184
138
  }
139
+ log += `/${max} (${formatDuration(cycleElapsed)}/${formatDuration(estimatedCycleTime)})`;
185
140
  }
186
141
  else {
187
- log += `variant: ${index} (${formatDuration(cycleElapsed)})`;
142
+ log += ` (${formatDuration(cycleElapsed)})`;
188
143
  }
189
- log += `, total: ${iterations} (${formatDuration(totalElapsed)})`;
190
- console.log(log);
191
- prevLogTime = now;
192
144
  }
193
- if (GC_Iterations && iterations - prevGC_Iterations >= GC_Iterations
194
- || GC_IterationsAsync && iterationsAsync - prevGC_IterationsAsync >= GC_IterationsAsync
195
- || GC_Interval && now - prevGC_Time >= GC_Interval) {
196
- prevGC_Iterations = iterations;
197
- prevGC_IterationsAsync = iterationsAsync;
198
- prevGC_Time = now;
199
- yield garbageCollect(1);
145
+ else {
146
+ log += `variant: ${variants.index} (${formatDuration(cycleElapsed)})`;
200
147
  }
201
- if (abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) {
202
- continue;
148
+ log += `, total: ${iterations} (${formatDuration(totalElapsed)})`;
149
+ console.log(log);
150
+ prevLogTime = now;
151
+ }
152
+ if (GC_Iterations && iterations - prevGC_Iterations >= GC_Iterations
153
+ || GC_IterationsAsync && iterationsAsync - prevGC_IterationsAsync >= GC_IterationsAsync
154
+ || GC_Interval && now - prevGC_Time >= GC_Interval) {
155
+ prevGC_Iterations = iterations;
156
+ prevGC_IterationsAsync = iterationsAsync;
157
+ prevGC_Time = now;
158
+ yield garbageCollect(1);
159
+ }
160
+ if (abortSignalExternal === null || abortSignalExternal === void 0 ? void 0 : abortSignalExternal.aborted) {
161
+ continue;
162
+ }
163
+ if (!pool || abortSignalParallel.aborted) {
164
+ try {
165
+ let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
166
+ if (isPromiseLike(promiseOrIterations)) {
167
+ promiseOrIterations = yield promiseOrIterations;
168
+ }
169
+ if (!promiseOrIterations) {
170
+ debug = true;
171
+ abortControllerParallel.abort();
172
+ continue;
173
+ }
174
+ const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
175
+ iterationsAsync += _iterationsAsync;
176
+ iterations += _iterationsSync + _iterationsAsync;
177
+ }
178
+ catch (err) {
179
+ if (errorVariantFilePath) {
180
+ yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
181
+ }
182
+ if (findBestError) {
183
+ variants.addLimit({ error: err });
184
+ debug = false;
185
+ }
186
+ else {
187
+ throw err;
188
+ }
203
189
  }
204
- if (!pool || abortSignalParallel.aborted) {
190
+ }
191
+ else {
192
+ if (!pool.hold(1)) {
193
+ yield pool.holdWait(1);
194
+ }
195
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
196
+ void (() => __awaiter(this, void 0, void 0, function* () {
205
197
  try {
198
+ if (abortSignalParallel === null || abortSignalParallel === void 0 ? void 0 : abortSignalParallel.aborted) {
199
+ return;
200
+ }
206
201
  let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
207
202
  if (isPromiseLike(promiseOrIterations)) {
208
203
  promiseOrIterations = yield promiseOrIterations;
@@ -210,7 +205,7 @@ function testVariantsRun(testRun, variants, options = {}) {
210
205
  if (!promiseOrIterations) {
211
206
  debug = true;
212
207
  abortControllerParallel.abort();
213
- continue;
208
+ return;
214
209
  }
215
210
  const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
216
211
  iterationsAsync += _iterationsAsync;
@@ -221,78 +216,47 @@ function testVariantsRun(testRun, variants, options = {}) {
221
216
  yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
222
217
  }
223
218
  if (findBestError) {
224
- bestError = {
225
- error: err,
226
- args: _args,
227
- index: _index,
228
- };
219
+ variants.addLimit({ error: err });
229
220
  debug = false;
230
221
  }
231
222
  else {
232
223
  throw err;
233
224
  }
234
225
  }
235
- }
236
- else {
237
- if (!pool.hold(1)) {
238
- yield pool.holdWait(1);
226
+ finally {
227
+ void pool.release(1);
239
228
  }
240
- // eslint-disable-next-line @typescript-eslint/no-loop-func
241
- void (() => __awaiter(this, void 0, void 0, function* () {
242
- try {
243
- if (abortSignalParallel === null || abortSignalParallel === void 0 ? void 0 : abortSignalParallel.aborted) {
244
- return;
245
- }
246
- let promiseOrIterations = testRun(_args, _index, abortSignalParallel);
247
- if (isPromiseLike(promiseOrIterations)) {
248
- promiseOrIterations = yield promiseOrIterations;
249
- }
250
- if (!promiseOrIterations) {
251
- debug = true;
252
- abortControllerParallel.abort();
253
- return;
254
- }
255
- const { iterationsAsync: _iterationsAsync, iterationsSync: _iterationsSync } = promiseOrIterations;
256
- iterationsAsync += _iterationsAsync;
257
- iterations += _iterationsSync + _iterationsAsync;
258
- }
259
- catch (err) {
260
- if (errorVariantFilePath) {
261
- yield saveErrorVariantFile(_args, errorVariantFilePath, saveErrorVariants.argsToJson);
262
- }
263
- if (findBestError) {
264
- bestError = {
265
- error: err,
266
- args: _args,
267
- index: _index,
268
- };
269
- debug = false;
270
- }
271
- else {
272
- throw err;
273
- }
274
- }
275
- finally {
276
- void pool.release(1);
277
- }
278
- }))();
279
- }
229
+ }))();
280
230
  }
281
- if (pool) {
282
- yield pool.holdWait(parallel);
283
- void pool.release(parallel);
284
- }
285
- if (abortSignalAll === null || abortSignalAll === void 0 ? void 0 : abortSignalAll.aborted) {
286
- throw abortSignalAll.reason;
287
- }
288
- onCompleted();
289
- yield garbageCollect(1);
290
- return iterations;
291
- });
231
+ }
232
+ // Track cycle metrics for logging
233
+ prevCycleVariantsCount = variants.count;
234
+ prevCycleDuration = Date.now() - cycleStartTime;
235
+ cycleStartTime = Date.now();
236
+ variants.start();
237
+ }
238
+ if (pool) {
239
+ yield pool.holdWait(parallel);
240
+ void pool.release(parallel);
241
+ }
242
+ if (abortSignalAll === null || abortSignalAll === void 0 ? void 0 : abortSignalAll.aborted) {
243
+ throw abortSignalAll.reason;
244
+ }
245
+ onCompleted();
246
+ yield garbageCollect(1);
247
+ // Construct bestError from iterator state
248
+ const bestError = variants.limit
249
+ ? {
250
+ error: variants.limit.error,
251
+ args: variants.limit.args,
252
+ index: variants.count,
253
+ }
254
+ : null;
255
+ if (bestError && !dontThrowIfError) {
256
+ throw bestError.error;
292
257
  }
293
- const result = yield next();
294
258
  return {
295
- iterations: result,
259
+ iterations,
296
260
  bestError,
297
261
  };
298
262
  });
@@ -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.5",
3
+ "version": "3.0.1",
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",
@@ -32,6 +32,24 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
+ "scripts": {
36
+ "_prepublishOnly": "pnpm run audit && pnpm run lint && pnpm run build && pnpm run test:mocha:ci && npm login",
37
+ "audit": "pnpm audit --prod",
38
+ "lint": "eslint ./**/*.{js,cjs,mjs,ts,tsx}",
39
+ "lint:fix": "eslint --fix ./**/*.{js,cjs,mjs,ts,tsx}",
40
+ "lint:wizard": "eslint-nibble --cache --multi ./**/*.{js,cjs,mjs,ts,tsx}",
41
+ "build:js": "rimraf dist/lib && cpy \"**/assets/**\" \"**/*.{js,cjs,mjs}\" \"../dist/lib/\" --parents --cwd=src && rollup -c",
42
+ "build": "rimraf dist && pnpm run build:js",
43
+ "coverage:merge": "rimraf tmp/coverage/{all,merge} && cp-flat \"tmp/coverage/*/json/**/*.json\" \"tmp/coverage/merge\" && nyc report -r lcov --report-dir tmp/coverage/all/lcov --temp-dir \"tmp/coverage/merge/\"",
44
+ "coverage:check": "pnpm run coverage:merge && nyc check-coverage --report-dir tmp/coverage/all/lcov --lines 50 --functions 50 --branches 50 --statements 50",
45
+ "test:mocha": "mocha ./src/**/*.test.*",
46
+ "test:mocha:coverage": "rimraf tmp/coverage/mocha && nyc --all mocha ./src/**/*.test.*",
47
+ "test:mocha:watch": "mocha --watch ./src/**/*.test.*",
48
+ "test:karma": "rimraf tmp/coverage/karma && karma start --single-run --log-level debug",
49
+ "test:mocha:ci": "rimraf tmp/coverage/mocha && nyc --all mocha ./{src,dist/lib}/**/*.test.*",
50
+ "coveralls": "pnpm run coverage:check && nyc report --reporter=text-lcov --temp-dir \"tmp/coverage/merge/\" | coveralls",
51
+ "mcp:tools": "mcp-project-tools"
52
+ },
35
53
  "devDependencies": {
36
54
  "@anthropic-ai/claude-code": "^2.0.76",
37
55
  "@babel/core": "7.18.5",
@@ -87,22 +105,5 @@
87
105
  "@flemist/async-utils": "^1.0.0",
88
106
  "@flemist/time-limits": "^1.0.1",
89
107
  "tslib": "2.5.3"
90
- },
91
- "scripts": {
92
- "audit": "pnpm audit --prod",
93
- "lint": "eslint ./**/*.{js,cjs,mjs,ts,tsx}",
94
- "lint:fix": "eslint --fix ./**/*.{js,cjs,mjs,ts,tsx}",
95
- "lint:wizard": "eslint-nibble --cache --multi ./**/*.{js,cjs,mjs,ts,tsx}",
96
- "build:js": "rimraf dist/lib && cpy \"**/assets/**\" \"**/*.{js,cjs,mjs}\" \"../dist/lib/\" --parents --cwd=src && rollup -c",
97
- "build": "rimraf dist && pnpm run build:js",
98
- "coverage:merge": "rimraf tmp/coverage/{all,merge} && cp-flat \"tmp/coverage/*/json/**/*.json\" \"tmp/coverage/merge\" && nyc report -r lcov --report-dir tmp/coverage/all/lcov --temp-dir \"tmp/coverage/merge/\"",
99
- "coverage:check": "pnpm run coverage:merge && nyc check-coverage --report-dir tmp/coverage/all/lcov --lines 50 --functions 50 --branches 50 --statements 50",
100
- "test:mocha": "mocha ./src/**/*.test.*",
101
- "test:mocha:coverage": "rimraf tmp/coverage/mocha && nyc --all mocha ./src/**/*.test.*",
102
- "test:mocha:watch": "mocha --watch ./src/**/*.test.*",
103
- "test:karma": "rimraf tmp/coverage/karma && karma start --single-run --log-level debug",
104
- "test:mocha:ci": "rimraf tmp/coverage/mocha && nyc --all mocha ./{src,dist/lib}/**/*.test.*",
105
- "coveralls": "pnpm run coverage:check && nyc report --reporter=text-lcov --temp-dir \"tmp/coverage/merge/\" | coveralls",
106
- "mcp:tools": "mcp-project-tools"
107
108
  }
108
- }
109
+ }