@alwaysmeticulous/cli 2.19.3 → 2.20.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.
Files changed (33) hide show
  1. package/dist/api/replay.api.d.ts +1 -1
  2. package/dist/api/session.api.d.ts +2 -1
  3. package/dist/commands/replay/replay.command.d.ts +3 -2
  4. package/dist/commands/replay/replay.command.js +46 -22
  5. package/dist/commands/replay/utils/{compute-and-save-diff.d.ts → compute-diff.d.ts} +3 -2
  6. package/dist/commands/replay/utils/{compute-and-save-diff.js → compute-diff.js} +5 -21
  7. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -0
  8. package/dist/commands/run-all-tests/run-all-tests.command.js +7 -1
  9. package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +5 -2
  10. package/dist/commands/screenshot-diff/screenshot-diff.command.js +23 -18
  11. package/dist/config/config.types.d.ts +6 -1
  12. package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
  13. package/dist/deflake-tests/deflake-tests.handler.js +2 -2
  14. package/dist/local-data/local-data.utils.d.ts +19 -0
  15. package/dist/local-data/local-data.utils.js +95 -1
  16. package/dist/local-data/replays.d.ts +2 -1
  17. package/dist/local-data/replays.js +30 -32
  18. package/dist/local-data/sessions.d.ts +2 -1
  19. package/dist/local-data/sessions.js +12 -27
  20. package/dist/main.js +1 -7
  21. package/dist/parallel-tests/__tests__/merge-test-results.spec.d.ts +1 -0
  22. package/dist/parallel-tests/__tests__/merge-test-results.spec.js +179 -0
  23. package/dist/parallel-tests/merge-test-results.d.ts +10 -0
  24. package/dist/parallel-tests/merge-test-results.js +98 -0
  25. package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
  26. package/dist/parallel-tests/parallel-tests.handler.js +113 -24
  27. package/dist/parallel-tests/run-all-tests.d.ts +7 -1
  28. package/dist/parallel-tests/run-all-tests.js +35 -52
  29. package/dist/parallel-tests/run-all-tests.types.d.ts +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/utils/sentry.utils.d.ts +0 -1
  32. package/dist/utils/sentry.utils.js +19 -14
  33. package/package.json +26 -6
@@ -11,13 +11,28 @@ const common_1 = require("@alwaysmeticulous/common");
11
11
  const loglevel_1 = __importDefault(require("loglevel"));
12
12
  const config_utils_1 = require("../utils/config.utils");
13
13
  const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
14
+ const merge_test_results_1 = require("./merge-test-results");
14
15
  /** Handler for running Meticulous tests in parallel using child processes */
15
- const runAllTestsInParallel = async ({ config, testRun, testsToRun: queue, apiToken, commitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, replayEventsDependencies, onTestFinished, }) => {
16
+ const runAllTestsInParallel = async ({ config, testRun, testsToRun, apiToken, commitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, replayEventsDependencies, onTestFinished, }) => {
16
17
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
17
- const results = [];
18
+ let nextId = 0;
19
+ let queue = testsToRun.map((test) => ({
20
+ ...test,
21
+ id: ++nextId,
22
+ }));
23
+ /**
24
+ * The current results, which may still be being updated if we're re-running a test
25
+ * to check for flakes.
26
+ */
27
+ const resultsByTestId = new Map();
28
+ /**
29
+ * Results that have been fully checked for flakes. At most one per test case.
30
+ */
31
+ const finalResults = [];
18
32
  const progress = {
19
33
  runningTestCases: queue.length,
20
34
  failedTestCases: 0,
35
+ flakedTestCases: 0,
21
36
  passedTestCases: 0,
22
37
  };
23
38
  const allTasksDone = (0, common_1.defer)();
@@ -26,7 +41,8 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun: queue, apiTo
26
41
  logger.debug(`Running with ${maxTasks} maximum tasks in parallel`);
27
42
  const taskHandler = (0, path_1.join)(__dirname, "task.handler.js");
28
43
  // Starts running a test case in a child process
29
- const startTask = (testCase) => {
44
+ const startTask = (rerunnableTestCase) => {
45
+ const { id, ...testCase } = rerunnableTestCase;
30
46
  const deferredResult = (0, common_1.defer)();
31
47
  const child = (0, child_process_1.fork)(taskHandler, [], { stdio: "inherit" });
32
48
  const messageHandler = (message) => {
@@ -53,6 +69,7 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun: queue, apiTo
53
69
  });
54
70
  child.on("message", messageHandler);
55
71
  // Send test case and arguments to child process
72
+ const isRetry = resultsByTestId.has(id);
56
73
  const initMessage = {
57
74
  kind: "init",
58
75
  data: {
@@ -73,35 +90,62 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun: queue, apiTo
73
90
  generatedBy: { type: "testRun", runId: testRun.id },
74
91
  testRunId: testRun.id,
75
92
  replayEventsDependencies,
93
+ suppressScreenshotDiffLogging: isRetry,
76
94
  },
77
95
  },
78
96
  };
79
97
  child.send(initMessage);
80
98
  // Handle task completion
81
99
  deferredResult.promise
82
- .catch(() => {
83
- // If it threw an error then it's something fatal, rather than just a failed diff
84
- // (it resolves successfully on a failed diff)
85
- const result = {
86
- ...testCase,
87
- headReplayId: "",
88
- result: "fail",
89
- screenshotDiffResults: [],
90
- };
91
- return result;
92
- })
100
+ .catch(() => null)
93
101
  .then(async (result) => {
102
+ var _a;
103
+ const resultsForTestCase = resultsByTestId.get(id);
104
+ if (resultsForTestCase != null && result != null) {
105
+ logRetrySummary(testName({ id, ...testCase }), result);
106
+ }
107
+ if (resultsForTestCase != null &&
108
+ resultsForTestCase.currentResult.result === "flake") {
109
+ // This test has already been declared as flakey, and we can ignore this result. This result
110
+ // was from an already executing test that we weren't able to cancel.
111
+ process.nextTick(checkNextTask);
112
+ return;
113
+ }
94
114
  --inProgress;
95
- results.push(result);
96
- progress.failedTestCases += result.result === "fail" ? 1 : 0;
97
- progress.passedTestCases += result.result === "pass" ? 1 : 0;
98
- --progress.runningTestCases;
99
- onTestFinished === null || onTestFinished === void 0 ? void 0 : onTestFinished(progress, results).then(() => {
100
- var _a;
101
- if (results.length === (((_a = config.testCases) === null || _a === void 0 ? void 0 : _a.length) || 0)) {
102
- allTasksDone.resolve();
103
- }
115
+ if ((result === null || result === void 0 ? void 0 : result.result) === "fail" && resultsForTestCase == null) {
116
+ queue.push(...Array.from(new Array(maxRetriesOnFailure)).map(() => ({
117
+ ...testCase,
118
+ id,
119
+ baseReplayId: result.headReplayId,
120
+ })));
121
+ }
122
+ const mergedResult = getNewMergedResult(testCase, (_a = resultsForTestCase === null || resultsForTestCase === void 0 ? void 0 : resultsForTestCase.currentResult) !== null && _a !== void 0 ? _a : null, result);
123
+ const numberOfRetriesExecuted = resultsForTestCase == null
124
+ ? 0
125
+ : resultsForTestCase.numberOfRetriesExecuted + 1;
126
+ // Our work is done for this test case if the first result was a pass,
127
+ // we've performed all the retries, or one of the retries already proved
128
+ // the result as flakey
129
+ const isFinalResult = mergedResult.result !== "fail" ||
130
+ numberOfRetriesExecuted === maxRetriesOnFailure;
131
+ resultsByTestId.set(id, {
132
+ currentResult: mergedResult,
133
+ numberOfRetriesExecuted,
104
134
  });
135
+ if (isFinalResult) {
136
+ // Cancel any replays that are still scheduled
137
+ queue = queue.filter(({ id }) => id !== id);
138
+ 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
+ }));
148
+ }
105
149
  process.nextTick(checkNextTask);
106
150
  });
107
151
  };
@@ -115,11 +159,56 @@ const runAllTestsInParallel = async ({ config, testRun, testsToRun: queue, apiTo
115
159
  return;
116
160
  }
117
161
  ++inProgress;
162
+ if (resultsByTestId.has(testCase.id)) {
163
+ logger.info(`Test ${testName(testCase)} failed. Retrying to check for flakes...`);
164
+ }
118
165
  startTask(testCase);
119
166
  process.nextTick(checkNextTask);
120
167
  };
121
168
  process.nextTick(checkNextTask);
122
169
  await allTasksDone.promise;
123
- return (0, run_all_tests_utils_1.sortResults)({ results, testCases: config.testCases || [] });
170
+ return (0, run_all_tests_utils_1.sortResults)({
171
+ results: finalResults,
172
+ testCases: config.testCases || [],
173
+ });
124
174
  };
125
175
  exports.runAllTestsInParallel = runAllTestsInParallel;
176
+ const logRetrySummary = (nameOfTest, retryResult) => {
177
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
178
+ if (retryResult.result === "pass") {
179
+ logger.info(`Retried taking screenshots for failed test ${nameOfTest}, but got the same results`);
180
+ }
181
+ else {
182
+ const numDifferingScreenshots = retryResult.screenshotDiffResults.filter((result) => result.outcome !== "no-diff").length;
183
+ logger.info(`Retried taking screenshots for failed test ${nameOfTest}, and ${numDifferingScreenshots} screenshots came out different. Results for these screenshots are assumed to be flakes, and so will be ignored.`);
184
+ }
185
+ };
186
+ const testName = (testCase) => testCase.title != null ? `'${testCase.title}'` : `#${testCase.id + 1}`;
187
+ const getNewMergedResult = (testCase, currentMergedResult, newResult) => {
188
+ // If currentMergedResult is null then this is our first try, our original head replay
189
+ if (currentMergedResult == null) {
190
+ if (newResult == null) {
191
+ // This means our original head replay failed fatally (not just a failed diff, but failed to even run)
192
+ // In this case we just return it as result = fail, with no screenshot diffs
193
+ return {
194
+ ...testCase,
195
+ headReplayId: "",
196
+ result: "fail",
197
+ screenshotDiffResults: [],
198
+ };
199
+ }
200
+ // In this case the newResult is our first head replay, our first result,
201
+ // so lets initialize the mergedResult to this
202
+ return newResult;
203
+ }
204
+ // If currentMergedResult is not null then newResult is a retry, containing comparison screenshots to our original head replay
205
+ if (newResult == null) {
206
+ // In this case newResult is a retry of the head replay, but it failed fatally (not just a failed diff, but failed to even run)
207
+ // So we just ignore this retry
208
+ return currentMergedResult;
209
+ }
210
+ return (0, merge_test_results_1.mergeResults)({
211
+ currentResult: currentMergedResult,
212
+ comparisonToHeadReplay: newResult,
213
+ });
214
+ };
@@ -23,6 +23,12 @@ export interface Options {
23
23
  */
24
24
  parallelTasks: number | null;
25
25
  deflake: boolean;
26
+ /**
27
+ * If set to a value greater than 1 then will re-run any replays that give a screenshot diff
28
+ * and mark them as a flake if the screenshot generated on one of the retryed replays differs from that
29
+ * in the first replay.
30
+ */
31
+ maxRetriesOnFailure: number;
26
32
  githubSummary: boolean;
27
33
  /**
28
34
  * If provided it will incorportate the cachedTestRunResults in any calls to store
@@ -57,4 +63,4 @@ export interface TestRun {
57
63
  * Runs all the test cases in the provided file.
58
64
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
59
65
  */
60
- export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
66
+ export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
@@ -7,12 +7,11 @@ exports.runAllTests = void 0;
7
7
  const common_1 = require("@alwaysmeticulous/common");
8
8
  const loglevel_1 = __importDefault(require("loglevel"));
9
9
  const client_1 = require("../api/client");
10
+ const replay_diff_api_1 = require("../api/replay-diff.api");
10
11
  const test_run_api_1 = require("../api/test-run.api");
11
12
  const config_1 = require("../config/config");
12
- const deflake_tests_handler_1 = require("../deflake-tests/deflake-tests.handler");
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");
16
15
  const github_summary_utils_1 = require("../utils/github-summary.utils");
17
16
  const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
18
17
  const test_run_environment_utils_1 = require("../utils/test-run-environment.utils");
@@ -21,10 +20,13 @@ const version_utils_1 = require("../utils/version.utils");
21
20
  * Runs all the test cases in the provided file.
22
21
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
23
22
  */
24
- const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
23
+ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
25
24
  if (appUrl != null && useAssetsSnapshottedInBaseSimulation) {
26
25
  throw new Error("Arguments useAssetsSnapshottedInBaseSimulation and appUrl are mutually exclusive");
27
26
  }
27
+ if (deflake && maxRetriesOnFailure > 1) {
28
+ throw new Error("Arguments deflake and maxRetriesOnFailure are mutually exclusive");
29
+ }
28
30
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
29
31
  const client = (0, client_1.createClient)({ apiToken });
30
32
  const cachedTestRunResults = cachedTestRunResults_ !== null && cachedTestRunResults_ !== void 0 ? cachedTestRunResults_ : [];
@@ -66,6 +68,7 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
66
68
  status: "Running",
67
69
  progress: {
68
70
  failedTestCases: 0,
71
+ flakedTestCases: 0,
69
72
  passedTestCases: cachedTestRunResults.length,
70
73
  runningTestCases: testCases.length,
71
74
  },
@@ -109,59 +112,37 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
109
112
  passedTestCases: progress.passedTestCases + cachedTestRunResults.length,
110
113
  },
111
114
  });
112
- await storeTestRunResults("Running", resultsSoFar);
113
- };
114
- const getResults = async () => {
115
- if (parallelTasks == null || parallelTasks > 1) {
116
- const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
117
- config,
118
- testRun,
119
- testsToRun,
120
- executionOptions,
121
- screenshottingOptions,
122
- apiToken,
123
- commitSha,
124
- appUrl,
125
- useAssetsSnapshottedInBaseSimulation,
126
- parallelTasks,
127
- deflake,
128
- replayEventsDependencies,
129
- onTestFinished,
130
- });
131
- return results;
132
- }
133
- const results = [];
134
- const progress = {
135
- runningTestCases: testsToRun.length,
136
- failedTestCases: 0,
137
- passedTestCases: 0,
138
- };
139
- for (const testCase of testsToRun) {
140
- const result = await (0, deflake_tests_handler_1.deflakeReplayCommandHandler)({
141
- replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
142
- useAssetsSnapshottedInBaseSimulation,
143
- appUrl,
144
- testCase,
145
- }),
146
- executionOptions,
147
- screenshottingOptions,
148
- testCase,
149
- apiToken,
150
- commitSha,
151
- deflake,
152
- generatedBy: { type: "testRun", runId: testRun.id },
115
+ const newResult = resultsSoFar.at(-1);
116
+ if ((newResult === null || newResult === void 0 ? void 0 : newResult.baseReplayId) != null) {
117
+ await (0, replay_diff_api_1.createReplayDiff)({
118
+ client,
119
+ headReplayId: newResult.headReplayId,
120
+ baseReplayId: newResult.baseReplayId,
153
121
  testRunId: testRun.id,
154
- replayEventsDependencies,
122
+ data: {
123
+ screenshotAssertionsOptions: screenshottingOptions,
124
+ screenshotDiffResults: newResult.screenshotDiffResults,
125
+ },
155
126
  });
156
- results.push(result);
157
- progress.failedTestCases += result.result === "fail" ? 1 : 0;
158
- progress.passedTestCases += result.result === "pass" ? 1 : 0;
159
- --progress.runningTestCases;
160
- await onTestFinished(progress, results);
161
127
  }
162
- return (0, run_all_tests_utils_1.sortResults)({ results, testCases });
128
+ await storeTestRunResults("Running", resultsSoFar);
163
129
  };
164
- const results = await getResults();
130
+ const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
131
+ config,
132
+ testRun,
133
+ testsToRun,
134
+ executionOptions,
135
+ screenshottingOptions,
136
+ apiToken,
137
+ commitSha,
138
+ appUrl,
139
+ useAssetsSnapshottedInBaseSimulation,
140
+ parallelTasks,
141
+ deflake,
142
+ replayEventsDependencies,
143
+ onTestFinished,
144
+ maxRetriesOnFailure,
145
+ });
165
146
  const runAllFailure = results.find(({ result }) => result === "fail");
166
147
  const overallStatus = runAllFailure ? "Failure" : "Success";
167
148
  await storeTestRunResults(overallStatus, results);
@@ -182,6 +163,8 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
182
163
  id: testRun.id,
183
164
  status: overallStatus,
184
165
  progress: {
166
+ flakedTestCases: results.filter(({ result }) => result === "flake")
167
+ .length,
185
168
  passedTestCases: results.filter(({ result }) => result === "pass")
186
169
  .length,
187
170
  failedTestCases: results.filter(({ result }) => result === "fail")
@@ -1,5 +1,6 @@
1
1
  export interface TestRunProgress {
2
2
  failedTestCases: number;
3
+ flakedTestCases: number;
3
4
  passedTestCases: number;
4
5
  runningTestCases: number;
5
6
  }