@alwaysmeticulous/cli 2.38.0 → 2.39.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.
@@ -22,7 +22,7 @@ const handler = async () => {
22
22
  logger.info(`Setting up ${chalk_1.default.green("test:meticulous")} script...`);
23
23
  await (0, npm_set_script_utils_1.npmSetScript)({
24
24
  script: "test:meticulous",
25
- command: "meticulous run-all-tests --headless --parallelize --deflake",
25
+ command: "meticulous run-all-tests --headless --parallelize",
26
26
  });
27
27
  };
28
28
  exports.bootstrapCommand = (0, command_builder_1.buildCommand)("bootstrap")
@@ -97,16 +97,16 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
97
97
  readonly description: "Number of tasks to run in parallel (defaults to two per CPU)";
98
98
  readonly coerce: (value: number | null | undefined) => number | null | undefined;
99
99
  };
100
- readonly deflake: {
101
- readonly boolean: true;
102
- readonly description: "Attempt to deflake failing tests";
103
- readonly default: false;
104
- };
105
100
  readonly maxRetriesOnFailure: {
106
101
  readonly number: true;
107
102
  readonly description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.";
108
103
  readonly default: 0;
109
104
  };
105
+ readonly rerunTestsNTimes: {
106
+ readonly number: true;
107
+ readonly description: "If set to a value greater than 0 then will re-run all replays the specified number of times and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.";
108
+ readonly default: 0;
109
+ };
110
110
  readonly useCache: {
111
111
  readonly boolean: true;
112
112
  readonly description: "Use result cache";
@@ -7,7 +7,7 @@ const command_builder_1 = require("../../command-utils/command-builder");
7
7
  const common_options_1 = require("../../command-utils/common-options");
8
8
  const run_all_tests_1 = require("../../parallel-tests/run-all-tests");
9
9
  const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
10
- const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, deflake, maxRetriesOnFailure, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, baseTestRunId, }) => {
10
+ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, maxRetriesOnFailure, rerunTestsNTimes, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, baseTestRunId, }) => {
11
11
  const executionOptions = {
12
12
  headless,
13
13
  devTools,
@@ -47,8 +47,8 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
47
47
  baseTestRunId: baseTestRunId !== null && baseTestRunId !== void 0 ? baseTestRunId : null,
48
48
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
49
49
  parallelTasks: parrelelTasks !== null && parrelelTasks !== void 0 ? parrelelTasks : null,
50
- deflake,
51
50
  maxRetriesOnFailure,
51
+ rerunTestsNTimes,
52
52
  cachedTestRunResults,
53
53
  githubSummary,
54
54
  });
@@ -89,16 +89,16 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
89
89
  return value;
90
90
  },
91
91
  },
92
- deflake: {
93
- boolean: true,
94
- description: "Attempt to deflake failing tests",
95
- default: false,
96
- },
97
92
  maxRetriesOnFailure: {
98
93
  number: true,
99
94
  description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.",
100
95
  default: 0,
101
96
  },
97
+ rerunTestsNTimes: {
98
+ number: true,
99
+ description: "If set to a value greater than 0 then will re-run all replays the specified number of times and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.",
100
+ default: 0,
101
+ },
102
102
  useCache: {
103
103
  boolean: true,
104
104
  description: "Use result cache",
@@ -12,14 +12,16 @@ describe("mergeResults", () => {
12
12
  });
13
13
  expect(mergedResult).toEqual((0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.noDiff)(1)]));
14
14
  });
15
- it("ignores diffs to screenshots which originally passed", () => {
15
+ it("doesn't ignore diffs to screenshots which originally passed", () => {
16
16
  const currentResult = (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)]);
17
17
  const comparisonToHeadReplay = (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.diff)(1)]);
18
18
  const mergedResult = (0, merge_test_results_1.mergeResults)({
19
19
  currentResult,
20
20
  comparisonToHeadReplay,
21
21
  });
22
- expect(mergedResult).toEqual((0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)]));
22
+ expect(mergedResult).toEqual(
23
+ // All screenshots are either "no-diff" or "flake" thus the overall result is "flake"
24
+ (0, mock_test_results_1.testResult)("flake", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.noDiff)(), [(0, mock_test_results_1.diff)()])]));
23
25
  });
24
26
  it("marks screenshots as flakes if the screenshot comparison originally failed, but the second retry gives a different screenshot again", () => {
25
27
  const currentResult = (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.diff)(1), (0, mock_test_results_1.diff)(2)]);
@@ -17,6 +17,7 @@ describe("runAllTestsInParallel", () => {
17
17
  testsToRun: [testCase(0), testCase(1), testCase(2), testCase(3)],
18
18
  parallelTasks: 2,
19
19
  maxRetriesOnFailure: 0,
20
+ rerunTestsNTimes: 0,
20
21
  executeTest: async (testCase) => {
21
22
  var _a;
22
23
  testsStarted++;
@@ -77,6 +78,7 @@ describe("runAllTestsInParallel", () => {
77
78
  testsToRun: [testCase(0), testCase(1), testCase(2), testCase(3)],
78
79
  parallelTasks: 2,
79
80
  maxRetriesOnFailure: 0,
81
+ rerunTestsNTimes: 0,
80
82
  executeTest: async (testCase) => {
81
83
  var _a;
82
84
  await testRunPromises[parseInt((_a = testCase.title) !== null && _a !== void 0 ? _a : "")].promise;
@@ -109,6 +111,7 @@ describe("runAllTestsInParallel", () => {
109
111
  testsToRun: [testCase(0)],
110
112
  parallelTasks: 2,
111
113
  maxRetriesOnFailure: 0,
114
+ rerunTestsNTimes: 0,
112
115
  executeTest: async (testCase) => {
113
116
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0)], testCase);
114
117
  },
@@ -121,6 +124,7 @@ describe("runAllTestsInParallel", () => {
121
124
  testsToRun: [testCase(0)],
122
125
  parallelTasks: 2,
123
126
  maxRetriesOnFailure: 10,
127
+ rerunTestsNTimes: 0,
124
128
  executeTest: async (testCase, isRetry) => {
125
129
  if (!isRetry) {
126
130
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.diff)(1)], testCase);
@@ -145,6 +149,7 @@ describe("runAllTestsInParallel", () => {
145
149
  testsToRun: [testCase(0)],
146
150
  parallelTasks: 2,
147
151
  maxRetriesOnFailure: 3,
152
+ rerunTestsNTimes: 0,
148
153
  executeTest: async (testCase, isRetry) => {
149
154
  if (!isRetry) {
150
155
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.diff)(1)], testCase);
@@ -156,6 +161,31 @@ describe("runAllTestsInParallel", () => {
156
161
  (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.diff)(), [(0, mock_test_results_1.diff)(), (0, mock_test_results_1.diff)(), (0, mock_test_results_1.diff)()])], testCase(0)),
157
162
  ]);
158
163
  });
164
+ it("retries N times if rerunTestsNTimes > 0", async () => {
165
+ let rerunNum = 0;
166
+ const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
167
+ testsToRun: [testCase(0)],
168
+ parallelTasks: 2,
169
+ maxRetriesOnFailure: 0,
170
+ rerunTestsNTimes: 3,
171
+ executeTest: async (testCase, isRetry) => {
172
+ if (!isRetry) {
173
+ return (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.noDiff)(1)], testCase);
174
+ }
175
+ if (rerunNum === 2) {
176
+ return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.missingHead)(0), (0, mock_test_results_1.diff)(1)], testCase);
177
+ }
178
+ rerunNum++;
179
+ return (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)], testCase);
180
+ },
181
+ });
182
+ expect(rerunNum).toEqual(2);
183
+ expect(results).toEqual([
184
+ (0, mock_test_results_1.testResult)(
185
+ // Flake outcome is returned iff all screenshots are flaky.
186
+ "flake", [(0, mock_test_results_1.flake)(0, (0, mock_test_results_1.diff)(), [(0, mock_test_results_1.missingHead)()]), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.noDiff)(), [(0, mock_test_results_1.diff)()])], testCase(0)),
187
+ ]);
188
+ });
159
189
  });
160
190
  const testCase = (num) => ({
161
191
  sessionId: "mock-session-id",
@@ -1,4 +1,5 @@
1
- import { DetailedTestCaseResult } from "../config/config.types";
1
+ import { ScreenshotDiffResult } from "@alwaysmeticulous/api";
2
+ import { DetailedTestCaseResult, TestCaseResult } from "../config/config.types";
2
3
  export interface ResultsToMerge {
3
4
  currentResult: DetailedTestCaseResult;
4
5
  /**
@@ -8,3 +9,4 @@ export interface ResultsToMerge {
8
9
  comparisonToHeadReplay: DetailedTestCaseResult;
9
10
  }
10
11
  export declare const mergeResults: ({ currentResult, comparisonToHeadReplay, }: ResultsToMerge) => DetailedTestCaseResult;
12
+ export declare const testRunOutcomeFromDiffResults: (currentResult: TestCaseResult["result"], newScreenshotDiffResults: ScreenshotDiffResult[]) => TestCaseResult["result"];
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.mergeResults = void 0;
6
+ exports.testRunOutcomeFromDiffResults = exports.mergeResults = void 0;
7
7
  const utils_1 = require("@sentry/utils");
8
8
  const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify"));
9
+ const has_notable_differences_1 = require("../commands/screenshot-diff/utils/has-notable-differences");
9
10
  const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
10
11
  const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
11
12
  // If any of the screenshots diffs in comparisonToHeadReplay show a diff against one
@@ -20,9 +21,6 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
20
21
  retryDiffById.set(hash, result);
21
22
  });
22
23
  const newScreenshotDiffResults = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(currentResult).map((currentDiff) => {
23
- if (currentDiff.outcome === "no-diff") {
24
- return currentDiff;
25
- }
26
24
  const hash = hashScreenshotIdentifier(currentDiff.identifier);
27
25
  const diffWhenRetrying = retryDiffById.get(hash);
28
26
  // diffWhenRetrying is null in the case that there is a base screenshot for the base replay,
@@ -75,12 +73,9 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
75
73
  };
76
74
  }
77
75
  });
78
- const noLongerHasFailures = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
79
76
  return {
80
77
  ...currentResult,
81
- result: currentResult.result === "fail" && noLongerHasFailures
82
- ? "flake"
83
- : currentResult.result,
78
+ result: (0, exports.testRunOutcomeFromDiffResults)(currentResult.result, newScreenshotDiffResults),
84
79
  screenshotDiffResultsByBaseReplayId: (0, screenshot_diff_results_utils_1.groupScreenshotDiffResults)(newScreenshotDiffResults),
85
80
  };
86
81
  };
@@ -108,3 +103,20 @@ const hashScreenshotIdentifier = (identifier) => {
108
103
  const unknownScreenshotIdentifierType = (identifier) => {
109
104
  utils_1.logger.error(`Unknown type of screenshot identifier: ${JSON.stringify(identifier)}`);
110
105
  };
106
+ const testRunOutcomeFromDiffResults = (currentResult, newScreenshotDiffResults) => {
107
+ // If a test run is already flakey, we don't want to overwrite that with a 'fail' result.
108
+ if (currentResult === "flake") {
109
+ return "flake";
110
+ }
111
+ const hasOnlyFlakes = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
112
+ if (hasOnlyFlakes) {
113
+ return "flake";
114
+ }
115
+ // If we've had a test run has already failed, we don't want to overwrite that with a 'pass' result.
116
+ // Otherwise, if there are any notable differences, we want to fail the test run.
117
+ return currentResult === "fail" ||
118
+ (0, has_notable_differences_1.hasNotableDifferences)(newScreenshotDiffResults)
119
+ ? "fail"
120
+ : "pass";
121
+ };
122
+ exports.testRunOutcomeFromDiffResults = testRunOutcomeFromDiffResults;
@@ -1,12 +1,12 @@
1
1
  import log from "loglevel";
2
2
  import { DetailedTestCaseResult } from "../config/config.types";
3
- import { DeflakeReplayCommandHandlerOptions } from "../deflake-tests/deflake-tests.handler";
3
+ import { ParallelTestsReplayOptions } from "./parallel-replay.types";
4
4
  export interface InitMessage {
5
5
  kind: "init";
6
6
  data: {
7
7
  logLevel: log.LogLevel[keyof log.LogLevel];
8
8
  dataDir: string;
9
- replayOptions: DeflakeReplayCommandHandlerOptions;
9
+ replayOptions: ParallelTestsReplayOptions;
10
10
  };
11
11
  }
12
12
  export interface ResultMessage {
@@ -0,0 +1,3 @@
1
+ import { DetailedTestCaseResult } from "../config/config.types";
2
+ import { ParallelTestsReplayOptions } from "./parallel-replay.types";
3
+ export declare const handleReplay: ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, baseTestRunId, }: ParallelTestsReplayOptions) => Promise<DetailedTestCaseResult>;
@@ -1,11 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deflakeReplayCommandHandler = void 0;
7
- const common_1 = require("@alwaysmeticulous/common");
8
- const loglevel_1 = __importDefault(require("loglevel"));
3
+ exports.handleReplay = void 0;
9
4
  const replay_command_1 = require("../commands/replay/replay.command");
10
5
  const has_notable_differences_1 = require("../commands/screenshot-diff/utils/has-notable-differences");
11
6
  const handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, baseTestRunId, }) => {
@@ -35,21 +30,7 @@ const handleReplay = async ({ testCase, replayTarget, executionOptions, screensh
35
30
  screenshotDiffResultsByBaseReplayId,
36
31
  };
37
32
  };
38
- const deflakeReplayCommandHandler = async ({ deflake, ...otherOptions }) => {
39
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
40
- const firstResult = await handleReplay(otherOptions);
41
- if (firstResult.result === "pass" || !deflake) {
42
- return firstResult;
43
- }
44
- const secondResult = await handleReplay(otherOptions);
45
- if (secondResult.result === "fail") {
46
- return secondResult;
47
- }
48
- const thirdResult = await handleReplay(otherOptions);
49
- logger.info(`FLAKE: ${thirdResult.title} => ${thirdResult.result}`);
50
- return thirdResult;
51
- };
52
- exports.deflakeReplayCommandHandler = deflakeReplayCommandHandler;
33
+ exports.handleReplay = handleReplay;
53
34
  const applyTestCaseExecutionOptionOverrides = (executionOptionsFromCliFlags, overridesFromTestCase) => {
54
35
  var _a;
55
36
  // Options specified in the test case override those passed as CLI flags
@@ -1,11 +1,7 @@
1
1
  import { TestCase } from "@alwaysmeticulous/api";
2
2
  import { GeneratedBy, ReplayEventsDependencies, ReplayExecutionOptions, ReplayTarget } from "@alwaysmeticulous/common";
3
3
  import { ScreenshotAssertionsEnabledOptions } from "../command-utils/common-types";
4
- import { DetailedTestCaseResult } from "../config/config.types";
5
- export interface DeflakeReplayCommandHandlerOptions extends HandleReplayOptions {
6
- deflake: boolean;
7
- }
8
- interface HandleReplayOptions {
4
+ export interface ParallelTestsReplayOptions {
9
5
  replayTarget: ReplayTarget;
10
6
  executionOptions: ReplayExecutionOptions;
11
7
  screenshottingOptions: ScreenshotAssertionsEnabledOptions;
@@ -18,5 +14,3 @@ interface HandleReplayOptions {
18
14
  replayEventsDependencies: ReplayEventsDependencies;
19
15
  suppressScreenshotDiffLogging: boolean;
20
16
  }
21
- export declare const deflakeReplayCommandHandler: ({ deflake, ...otherOptions }: DeflakeReplayCommandHandlerOptions) => Promise<DetailedTestCaseResult>;
22
- export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,6 +5,7 @@ export interface RunAllTestsInParallelOptions {
5
5
  testsToRun: TestCase[];
6
6
  parallelTasks: number | null;
7
7
  maxRetriesOnFailure: number;
8
+ rerunTestsNTimes: number;
8
9
  executeTest: (testCase: TestCase, isRetry: boolean) => Promise<DetailedTestCaseResult>;
9
10
  onTestFinished?: (progress: TestRunProgress, resultsSoFar: DetailedTestCaseResult[]) => Promise<void>;
10
11
  onTestFailedToRun?: (progress: TestRunProgress) => Promise<void>;
@@ -10,7 +10,10 @@ const loglevel_1 = __importDefault(require("loglevel"));
10
10
  const merge_test_results_1 = require("./merge-test-results");
11
11
  const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
12
12
  /** Handler for running Meticulous tests in parallel using child processes */
13
- const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, executeTest, onTestFinished, onTestFailedToRun, }) => {
13
+ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, executeTest, onTestFinished, onTestFailedToRun, }) => {
14
+ if (maxRetriesOnFailure && rerunTestsNTimes) {
15
+ throw new Error("maxRetriesOnFailure and rerunTestsNTimes are mutually exclusive.");
16
+ }
14
17
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
15
18
  let nextId = 0;
16
19
  let queue = testsToRun.map((test) => ({
@@ -47,8 +50,16 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
47
50
  .then(async (result) => {
48
51
  var _a;
49
52
  const resultsForTestCase = resultsByTestId.get(id);
53
+ // Re-run the test if needed, comparing to the base replay.
54
+ if (!isRetry) {
55
+ queue.push(...Array.from(new Array(rerunTestsNTimes)).map(() => ({
56
+ ...testCase,
57
+ id,
58
+ baseReplayId: result === null || result === void 0 ? void 0 : result.headReplayId,
59
+ })));
60
+ }
50
61
  if (resultsForTestCase != null && result != null) {
51
- logRetrySummary(testName({ id, ...testCase }), result);
62
+ logRetrySummary(testName({ id, ...testCase }), result, resultsForTestCase.currentResult);
52
63
  }
53
64
  if (resultsForTestCase != null &&
54
65
  resultsForTestCase.currentResult.result === "flake") {
@@ -81,8 +92,15 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
81
92
  // Our work is done for this test case if the first result was a pass,
82
93
  // we've performed all the retries, or one of the retries already proved
83
94
  // the result as flakey
84
- const isFinalResult = mergedResult.result !== "fail" ||
85
- numberOfRetriesExecuted === maxRetriesOnFailure;
95
+ let isFinalResult;
96
+ if (rerunTestsNTimes > 0) {
97
+ isFinalResult = numberOfRetriesExecuted >= rerunTestsNTimes;
98
+ }
99
+ else {
100
+ isFinalResult =
101
+ mergedResult.result !== "fail" ||
102
+ numberOfRetriesExecuted >= maxRetriesOnFailure;
103
+ }
86
104
  if (isFinalResult) {
87
105
  // Cancel any replays that are still scheduled
88
106
  queue = queue.filter((otherReplay) => otherReplay.id !== id);
@@ -119,7 +137,12 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
119
137
  }
120
138
  ++inProgress;
121
139
  if (resultsByTestId.has(testCase.id)) {
122
- logger.info(`Test ${testName(testCase)} failed. Retrying to check for flakes...`);
140
+ if (maxRetriesOnFailure > 0) {
141
+ logger.info(`Test ${testName(testCase)} failed. Retrying to check for flakes...`);
142
+ }
143
+ else if (rerunTestsNTimes > 0) {
144
+ logger.info(`Re-running ${testName(testCase)} to check for flakes...`);
145
+ }
123
146
  }
124
147
  startTask(testCase);
125
148
  process.nextTick(checkNextTask);
@@ -129,10 +152,10 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
129
152
  return finalResults;
130
153
  };
131
154
  exports.runAllTestsInParallel = runAllTestsInParallel;
132
- const logRetrySummary = (nameOfTest, retryResult) => {
155
+ const logRetrySummary = (nameOfTest, retryResult, resultSoFar) => {
133
156
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
134
157
  if (retryResult.result === "pass") {
135
- logger.info(`Retried taking screenshots for failed test ${nameOfTest}, but got the same results`);
158
+ logger.info(`Retried taking screenshots for${resultSoFar.result == "fail" ? " failed" : ""} test ${nameOfTest}, but got the same results`);
136
159
  }
137
160
  else {
138
161
  const numDifferingScreenshots = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(retryResult).filter((result) => result.outcome !== "no-diff").length;
@@ -25,13 +25,22 @@ export interface Options {
25
25
  * Set to 1 to disable parralelism.
26
26
  */
27
27
  parallelTasks: number | null;
28
- deflake: boolean;
29
28
  /**
30
29
  * If set to a value greater than 1 then will re-run any replays that give a screenshot diff
31
30
  * and mark them as a flake if the screenshot generated on one of the retryed replays differs from that
32
31
  * in the first replay.
33
32
  */
34
33
  maxRetriesOnFailure: number;
34
+ /**
35
+ * If set to a value greater than 0 then will re-run all replays the specified number of times
36
+ * and mark them as a flake if the screenshot generated on one of the retryed replays differs from that
37
+ * in the first replay.
38
+ *
39
+ * This is useful for checking flake rates.
40
+ *
41
+ * This option is mutually exclusive with maxRetriesOnFailure.
42
+ */
43
+ rerunTestsNTimes: number;
35
44
  githubSummary: boolean;
36
45
  /**
37
46
  * If provided it will incorportate the cachedTestRunResults in any calls to store
@@ -61,4 +70,4 @@ export interface RunAllTestsResult {
61
70
  * Runs all the test cases in the provided file.
62
71
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
63
72
  */
64
- export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
73
+ export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
@@ -12,21 +12,18 @@ const replay_diff_api_1 = require("../api/replay-diff.api");
12
12
  const test_run_api_1 = require("../api/test-run.api");
13
13
  const config_1 = require("../config/config");
14
14
  const replay_assets_1 = require("../local-data/replay-assets");
15
- const parallel_tests_handler_1 = require("../parallel-tests/parallel-tests.handler");
16
15
  const config_utils_1 = require("../utils/config.utils");
17
16
  const github_summary_utils_1 = require("../utils/github-summary.utils");
18
17
  const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
19
18
  const test_run_environment_utils_1 = require("../utils/test-run-environment.utils");
20
19
  const version_utils_1 = require("../utils/version.utils");
21
20
  const execute_test_in_child_process_1 = require("./execute-test-in-child-process");
21
+ const parallel_tests_handler_1 = require("./parallel-tests.handler");
22
22
  /**
23
23
  * Runs all the test cases in the provided file.
24
24
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
25
25
  */
26
- const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
27
- if (deflake && maxRetriesOnFailure > 1) {
28
- throw new Error("Arguments deflake and maxRetriesOnFailure are mutually exclusive");
29
- }
26
+ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
30
27
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
31
28
  const client = (0, client_1.createClient)({ apiToken });
32
29
  const project = await (0, project_api_1.getProject)(client);
@@ -60,7 +57,6 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
60
57
  baseCommitSha,
61
58
  appUrl,
62
59
  parallelTasks,
63
- deflake,
64
60
  githubSummary,
65
61
  },
66
62
  environment: (0, test_run_environment_utils_1.getEnvironment)(environment),
@@ -145,6 +141,7 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
145
141
  testsToRun,
146
142
  parallelTasks,
147
143
  maxRetriesOnFailure,
144
+ rerunTestsNTimes,
148
145
  executeTest: (testCase, isRetry) => {
149
146
  var _a;
150
147
  const initMessage = {
@@ -156,7 +153,6 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
156
153
  apiToken,
157
154
  commitSha,
158
155
  testCase,
159
- deflake,
160
156
  replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
161
157
  appUrl,
162
158
  testCase,
@@ -30,9 +30,9 @@ const common_1 = require("@alwaysmeticulous/common");
30
30
  const Sentry = __importStar(require("@sentry/node"));
31
31
  const loglevel_1 = __importDefault(require("loglevel"));
32
32
  const luxon_1 = require("luxon");
33
- const deflake_tests_handler_1 = require("../deflake-tests/deflake-tests.handler");
34
33
  const logger_utils_1 = require("../utils/logger.utils");
35
34
  const sentry_utils_1 = require("../utils/sentry.utils");
35
+ const parallel_replay_handler_1 = require("./parallel-replay.handler");
36
36
  const INIT_TIMEOUT = luxon_1.Duration.fromObject({ second: 1 });
37
37
  const waitForInitMessage = () => {
38
38
  return Promise.race([
@@ -67,7 +67,7 @@ const main = async () => {
67
67
  const { logLevel, dataDir, replayOptions } = initMessage.data;
68
68
  logger.setLevel(logLevel);
69
69
  (0, common_1.setMeticulousLocalDataDir)(dataDir);
70
- const result = await (0, deflake_tests_handler_1.deflakeReplayCommandHandler)(replayOptions);
70
+ const result = await (0, parallel_replay_handler_1.handleReplay)(replayOptions);
71
71
  const resultMessage = {
72
72
  kind: "result",
73
73
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwaysmeticulous/cli",
3
- "version": "2.38.0",
3
+ "version": "2.39.0",
4
4
  "description": "The Meticulous CLI",
5
5
  "license": "ISC",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "jest"
27
27
  },
28
28
  "dependencies": {
29
- "@alwaysmeticulous/common": "^2.38.0",
29
+ "@alwaysmeticulous/common": "^2.39.0",
30
30
  "@sentry/node": "^7.36.0",
31
31
  "@sentry/tracing": "^7.36.0",
32
32
  "adm-zip": "^0.5.9",
@@ -45,7 +45,7 @@
45
45
  "yargs": "^17.5.1"
46
46
  },
47
47
  "devDependencies": {
48
- "@alwaysmeticulous/api": "^2.37.0",
48
+ "@alwaysmeticulous/api": "^2.39.0",
49
49
  "@types/express": "^4.17.14",
50
50
  "@types/proper-lockfile": "^4.1.2"
51
51
  },
@@ -95,5 +95,5 @@
95
95
  "coverageDirectory": "../coverage",
96
96
  "testEnvironment": "node"
97
97
  },
98
- "gitHead": "2c4fbc9225d5e0dc09f14a1e90a67008a5e3f376"
98
+ "gitHead": "b6488b15a8233fcd1380b1607ec254d5d3bc0378"
99
99
  }