@alwaysmeticulous/cli 2.21.1 → 2.21.3-alpha.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.
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +2 -2
- package/dist/parallel-tests/__tests__/merge-test-results.spec.js +47 -136
- package/dist/parallel-tests/__tests__/mock-test-results.d.ts +17 -0
- package/dist/parallel-tests/__tests__/mock-test-results.js +104 -0
- package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.d.ts +1 -0
- package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.js +175 -0
- package/dist/parallel-tests/execute-test-in-child-process.d.ts +3 -0
- package/dist/parallel-tests/execute-test-in-child-process.js +43 -0
- package/dist/parallel-tests/parallel-tests.handler.d.ts +3 -14
- package/dist/parallel-tests/parallel-tests.handler.js +39 -90
- package/dist/parallel-tests/run-all-tests.js +46 -20
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/run-all-tests.utils.js +3 -7
- package/package.json +2 -2
|
@@ -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 ({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
86
|
+
queue = queue.filter((otherReplay) => otherReplay.id !== id);
|
|
138
87
|
finalResults.push(mergedResult);
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
167
|
-
|
|
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:
|
|
196
|
+
failedTestCases: sortedResults.filter(({ result }) => result === "fail")
|
|
171
197
|
.length,
|
|
172
198
|
runningTestCases: 0,
|
|
173
199
|
},
|
|
174
200
|
},
|
|
175
|
-
testCaseResults:
|
|
201
|
+
testCaseResults: sortedResults,
|
|
176
202
|
};
|
|
177
203
|
};
|
|
178
204
|
exports.runAllTests = runAllTests;
|