@alwaysmeticulous/cli 2.21.0 → 2.21.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.
@@ -4,16 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runAllTestsInParallel = void 0;
7
- const child_process_1 = require("child_process");
8
7
  const os_1 = require("os");
9
- const path_1 = require("path");
10
8
  const common_1 = require("@alwaysmeticulous/common");
11
9
  const loglevel_1 = __importDefault(require("loglevel"));
12
- const config_utils_1 = require("../utils/config.utils");
13
- const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
14
10
  const merge_test_results_1 = require("./merge-test-results");
15
11
  /** Handler for running Meticulous tests in parallel using child processes */
16
- const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, commitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, replayEventsDependencies, onTestFinished, }) => {
12
+ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, executeTest, onTestFinished, onTestFailedToRun, }) => {
17
13
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
18
14
  let nextId = 0;
19
15
  let queue = testsToRun.map((test) => ({
@@ -29,7 +25,7 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, co
29
25
  * Results that have been fully checked for flakes. At most one per test case.
30
26
  */
31
27
  const finalResults = [];
32
- const progress = {
28
+ let progress = {
33
29
  runningTestCases: queue.length,
34
30
  failedTestCases: 0,
35
31
  flakedTestCases: 0,
@@ -39,68 +35,15 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, co
39
35
  let inProgress = 0;
40
36
  const maxTasks = parallelTasks !== null && parallelTasks !== void 0 ? parallelTasks : Math.max((0, os_1.cpus)().length, 1);
41
37
  logger.debug(`Running with ${maxTasks} maximum tasks in parallel`);
42
- const taskHandler = (0, path_1.join)(__dirname, "task.handler.js");
43
38
  // Starts running a test case in a child process
44
39
  const startTask = (rerunnableTestCase) => {
45
40
  const { id, ...testCase } = rerunnableTestCase;
46
- const deferredResult = (0, common_1.defer)();
47
- const child = (0, child_process_1.fork)(taskHandler, [], { stdio: "inherit" });
48
- const messageHandler = (message) => {
49
- if (message &&
50
- typeof message === "object" &&
51
- message["kind"] === "result") {
52
- const resultMessage = message;
53
- deferredResult.resolve(resultMessage.data.result);
54
- child.off("message", messageHandler);
55
- }
56
- };
57
- child.on("error", (error) => {
58
- if (deferredResult.getState() === "pending") {
59
- deferredResult.reject(error);
60
- }
61
- });
62
- child.on("exit", (code) => {
63
- if (code) {
64
- logger.debug(`child exited with code: ${code}`);
65
- }
66
- if (deferredResult.getState() === "pending") {
67
- deferredResult.reject(new Error("No result"));
68
- }
69
- });
70
- child.on("message", messageHandler);
71
- // Send test case and arguments to child process
72
41
  const isRetry = resultsByTestId.has(id);
73
- const initMessage = {
74
- kind: "init",
75
- data: {
76
- logLevel: logger.getLevel(),
77
- dataDir: (0, common_1.getMeticulousLocalDataDir)(),
78
- replayOptions: {
79
- apiToken,
80
- commitSha,
81
- testCase,
82
- deflake,
83
- replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
84
- useAssetsSnapshottedInBaseSimulation,
85
- appUrl,
86
- testCase,
87
- }),
88
- executionOptions,
89
- screenshottingOptions,
90
- generatedBy: { type: "testRun", runId: testRun.id },
91
- testRunId: testRun.id,
92
- replayEventsDependencies,
93
- suppressScreenshotDiffLogging: isRetry,
94
- },
95
- },
96
- };
97
- child.send(initMessage);
98
42
  // Handle task completion
99
- deferredResult.promise
43
+ executeTest(testCase, isRetry)
100
44
  .catch(() => null)
101
45
  .then(async (result) => {
102
46
  var _a;
103
- --inProgress;
104
47
  const resultsForTestCase = resultsByTestId.get(id);
105
48
  if (resultsForTestCase != null && result != null) {
106
49
  logRetrySummary(testName({ id, ...testCase }), result);
@@ -109,50 +52,58 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, co
109
52
  resultsForTestCase.currentResult.result === "flake") {
110
53
  // This test has already been declared as flakey, and we can ignore this result. This result
111
54
  // was from an already executing test that we weren't able to cancel.
112
- process.nextTick(checkNextTask);
113
55
  return;
114
56
  }
115
57
  if ((result === null || result === void 0 ? void 0 : result.result) === "fail" && resultsForTestCase == null) {
58
+ // Let's auto-retry to see if this failure persists
116
59
  queue.push(...Array.from(new Array(maxRetriesOnFailure)).map(() => ({
117
60
  ...testCase,
118
61
  id,
119
62
  baseReplayId: result.headReplayId,
120
63
  })));
121
64
  }
122
- const mergedResult = getNewMergedResult(testCase, (_a = resultsForTestCase === null || resultsForTestCase === void 0 ? void 0 : resultsForTestCase.currentResult) !== null && _a !== void 0 ? _a : null, result);
65
+ if (result == null && (resultsForTestCase === null || resultsForTestCase === void 0 ? void 0 : resultsForTestCase.currentResult) == null) {
66
+ // This means our original head replay failed fatally (not just a failed diff, but failed to even run)
67
+ progress = updateProgress(progress, "fail");
68
+ await (onTestFailedToRun === null || onTestFailedToRun === void 0 ? void 0 : onTestFailedToRun(progress));
69
+ return;
70
+ }
71
+ const mergedResult = getNewMergedResult((_a = resultsForTestCase === null || resultsForTestCase === void 0 ? void 0 : resultsForTestCase.currentResult) !== null && _a !== void 0 ? _a : null, result);
123
72
  const numberOfRetriesExecuted = resultsForTestCase == null
124
73
  ? 0
125
74
  : resultsForTestCase.numberOfRetriesExecuted + 1;
75
+ resultsByTestId.set(id, {
76
+ currentResult: mergedResult,
77
+ numberOfRetriesExecuted,
78
+ });
126
79
  // Our work is done for this test case if the first result was a pass,
127
80
  // we've performed all the retries, or one of the retries already proved
128
81
  // the result as flakey
129
82
  const isFinalResult = mergedResult.result !== "fail" ||
130
83
  numberOfRetriesExecuted === maxRetriesOnFailure;
131
- resultsByTestId.set(id, {
132
- currentResult: mergedResult,
133
- numberOfRetriesExecuted,
134
- });
135
84
  if (isFinalResult) {
136
85
  // Cancel any replays that are still scheduled
137
- queue = queue.filter(({ id }) => id !== id);
86
+ queue = queue.filter((otherReplay) => otherReplay.id !== id);
138
87
  finalResults.push(mergedResult);
139
- --progress.runningTestCases;
140
- progress.failedTestCases += mergedResult.result === "fail" ? 1 : 0;
141
- progress.flakedTestCases += mergedResult.result === "flake" ? 1 : 0;
142
- progress.passedTestCases += mergedResult.result === "pass" ? 1 : 0;
143
- await (onTestFinished === null || onTestFinished === void 0 ? void 0 : onTestFinished(progress, finalResults).then(() => {
144
- if (queue.length === 0 && inProgress === 0) {
145
- allTasksDone.resolve();
146
- }
147
- }));
88
+ progress = updateProgress(progress, mergedResult.result);
89
+ await (onTestFinished === null || onTestFinished === void 0 ? void 0 : onTestFinished(progress, finalResults));
148
90
  }
149
- process.nextTick(checkNextTask);
150
91
  })
151
92
  .catch((err) => {
152
93
  logger.error(`Error processing result of completed task for test '${testName({
153
94
  id,
154
95
  ...testCase,
155
96
  })}'`, err);
97
+ })
98
+ .finally(() => {
99
+ // We only decrement inProgress at the very end,
100
+ // otherwise another promise may call allTasksDone.resolve() while
101
+ // we're still saving results
102
+ --inProgress;
103
+ if (queue.length === 0 && inProgress === 0) {
104
+ allTasksDone.resolve();
105
+ }
106
+ process.nextTick(checkNextTask);
156
107
  });
157
108
  };
158
109
  // Checks if we can start a new child process
@@ -173,10 +124,7 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, co
173
124
  };
174
125
  process.nextTick(checkNextTask);
175
126
  await allTasksDone.promise;
176
- return (0, run_all_tests_utils_1.sortResults)({
177
- results: finalResults,
178
- testCases: config.testCases || [],
179
- });
127
+ return finalResults;
180
128
  };
181
129
  exports.runAllTestsInParallel = runAllTestsInParallel;
182
130
  const logRetrySummary = (nameOfTest, retryResult) => {
@@ -190,18 +138,11 @@ const logRetrySummary = (nameOfTest, retryResult) => {
190
138
  }
191
139
  };
192
140
  const testName = (testCase) => testCase.title != null ? `'${testCase.title}'` : `#${testCase.id + 1}`;
193
- const getNewMergedResult = (testCase, currentMergedResult, newResult) => {
141
+ const getNewMergedResult = (currentMergedResult, newResult) => {
194
142
  // If currentMergedResult is null then this is our first try, our original head replay
195
143
  if (currentMergedResult == null) {
196
144
  if (newResult == null) {
197
- // This means our original head replay failed fatally (not just a failed diff, but failed to even run)
198
- // In this case we just return it as result = fail, with no screenshot diffs
199
- return {
200
- ...testCase,
201
- headReplayId: "",
202
- result: "fail",
203
- screenshotDiffResults: [],
204
- };
145
+ throw new Error("Expected either newResult to be non-null or currentMergedResult to be non-null, but both were null. This case should be handled before getNewMergedResult is called");
205
146
  }
206
147
  // In this case the newResult is our first head replay, our first result,
207
148
  // so lets initialize the mergedResult to this
@@ -218,3 +159,11 @@ const getNewMergedResult = (testCase, currentMergedResult, newResult) => {
218
159
  comparisonToHeadReplay: newResult,
219
160
  });
220
161
  };
162
+ const updateProgress = (progress, newResult) => {
163
+ return {
164
+ runningTestCases: progress.runningTestCases - 1,
165
+ failedTestCases: progress.failedTestCases + (newResult === "fail" ? 1 : 0),
166
+ flakedTestCases: progress.flakedTestCases + (newResult === "flake" ? 1 : 0),
167
+ passedTestCases: progress.passedTestCases + (newResult === "pass" ? 1 : 0),
168
+ };
169
+ };
@@ -12,10 +12,12 @@ const test_run_api_1 = require("../api/test-run.api");
12
12
  const config_1 = require("../config/config");
13
13
  const replay_assets_1 = require("../local-data/replay-assets");
14
14
  const parallel_tests_handler_1 = require("../parallel-tests/parallel-tests.handler");
15
+ const config_utils_1 = require("../utils/config.utils");
15
16
  const github_summary_utils_1 = require("../utils/github-summary.utils");
16
17
  const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
17
18
  const test_run_environment_utils_1 = require("../utils/test-run-environment.utils");
18
19
  const version_utils_1 = require("../utils/version.utils");
20
+ const execute_test_in_child_process_1 = require("./execute-test-in-child-process");
19
21
  /**
20
22
  * Runs all the test cases in the provided file.
21
23
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
@@ -102,7 +104,7 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
102
104
  logger.error(`Error while pushing partial results: ${error}`);
103
105
  }
104
106
  };
105
- const onTestFinished = async (progress, resultsSoFar) => {
107
+ const onProgressUpdated = async (progress) => {
106
108
  onTestFinished_ === null || onTestFinished_ === void 0 ? void 0 : onTestFinished_({
107
109
  id: testRun.id,
108
110
  url: testRunUrl,
@@ -112,6 +114,9 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
112
114
  passedTestCases: progress.passedTestCases + cachedTestRunResults.length,
113
115
  },
114
116
  });
117
+ };
118
+ const onTestFinished = async (progress, resultsSoFar) => {
119
+ onProgressUpdated(progress);
115
120
  const newResult = resultsSoFar.at(-1);
116
121
  if ((newResult === null || newResult === void 0 ? void 0 : newResult.baseReplayId) != null) {
117
122
  await (0, replay_diff_api_1.createReplayDiff)({
@@ -128,30 +133,52 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
128
133
  await storeTestRunResults("Running", resultsSoFar);
129
134
  };
130
135
  const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
131
- config,
132
- testRun,
133
136
  testsToRun,
134
- executionOptions,
135
- screenshottingOptions,
136
- apiToken,
137
- commitSha,
138
- appUrl,
139
- useAssetsSnapshottedInBaseSimulation,
140
137
  parallelTasks,
141
- deflake,
142
- replayEventsDependencies,
143
- onTestFinished,
144
138
  maxRetriesOnFailure,
139
+ executeTest: (testCase, isRetry) => {
140
+ const initMessage = {
141
+ kind: "init",
142
+ data: {
143
+ logLevel: logger.getLevel(),
144
+ dataDir: (0, common_1.getMeticulousLocalDataDir)(),
145
+ replayOptions: {
146
+ apiToken,
147
+ commitSha,
148
+ testCase,
149
+ deflake,
150
+ replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
151
+ useAssetsSnapshottedInBaseSimulation,
152
+ appUrl,
153
+ testCase,
154
+ }),
155
+ executionOptions,
156
+ screenshottingOptions,
157
+ generatedBy: { type: "testRun", runId: testRun.id },
158
+ testRunId: testRun.id,
159
+ replayEventsDependencies,
160
+ suppressScreenshotDiffLogging: isRetry,
161
+ },
162
+ },
163
+ };
164
+ return (0, execute_test_in_child_process_1.executeTestInChildProcess)(initMessage);
165
+ },
166
+ onTestFinished,
167
+ onTestFailedToRun: onProgressUpdated,
145
168
  });
146
- const runAllFailure = results.find(({ result }) => result === "fail");
169
+ const sortedResults = (0, run_all_tests_utils_1.sortResults)({
170
+ results: results,
171
+ testCases: config.testCases || [],
172
+ });
173
+ const runAllFailure = sortedResults.find(({ result }) => result === "fail");
147
174
  const overallStatus = runAllFailure ? "Failure" : "Success";
148
- await storeTestRunResults(overallStatus, results);
175
+ await storeTestRunResults(overallStatus, sortedResults);
149
176
  logger.info("");
150
177
  logger.info("Results");
151
178
  logger.info("=======");
152
179
  logger.info(`URL: ${testRunUrl}`);
153
180
  logger.info("=======");
154
- results.forEach(({ title, result }) => {
181
+ sortedResults.forEach(({ title, result }) => {
155
182
  logger.info(`${title} => ${result}`);
156
183
  });
157
184
  if (githubSummary) {
@@ -163,16 +190,15 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
163
190
  id: testRun.id,
164
191
  status: overallStatus,
165
192
  progress: {
166
- flakedTestCases: results.filter(({ result }) => result === "flake")
167
- .length,
168
- passedTestCases: results.filter(({ result }) => result === "pass")
193
+ flakedTestCases: sortedResults.filter(({ result }) => result === "flake").length,
194
+ passedTestCases: sortedResults.filter(({ result }) => result === "pass")
169
195
  .length,
170
- failedTestCases: results.filter(({ result }) => result === "fail")
196
+ failedTestCases: sortedResults.filter(({ result }) => result === "fail")
171
197
  .length,
172
198
  runningTestCases: 0,
173
199
  },
174
200
  },
175
- testCaseResults: results,
201
+ testCaseResults: sortedResults,
176
202
  };
177
203
  };
178
204
  exports.runAllTests = runAllTests;