@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.
- package/dist/api/replay.api.d.ts +1 -1
- package/dist/api/session.api.d.ts +2 -1
- package/dist/commands/replay/replay.command.d.ts +3 -2
- package/dist/commands/replay/replay.command.js +46 -22
- package/dist/commands/replay/utils/{compute-and-save-diff.d.ts → compute-diff.d.ts} +3 -2
- package/dist/commands/replay/utils/{compute-and-save-diff.js → compute-diff.js} +5 -21
- package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -0
- package/dist/commands/run-all-tests/run-all-tests.command.js +7 -1
- package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +5 -2
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +23 -18
- package/dist/config/config.types.d.ts +6 -1
- package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
- package/dist/deflake-tests/deflake-tests.handler.js +2 -2
- package/dist/local-data/local-data.utils.d.ts +19 -0
- package/dist/local-data/local-data.utils.js +95 -1
- package/dist/local-data/replays.d.ts +2 -1
- package/dist/local-data/replays.js +30 -32
- package/dist/local-data/sessions.d.ts +2 -1
- package/dist/local-data/sessions.js +12 -27
- package/dist/main.js +1 -7
- package/dist/parallel-tests/__tests__/merge-test-results.spec.d.ts +1 -0
- package/dist/parallel-tests/__tests__/merge-test-results.spec.js +179 -0
- package/dist/parallel-tests/merge-test-results.d.ts +10 -0
- package/dist/parallel-tests/merge-test-results.js +98 -0
- package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
- package/dist/parallel-tests/parallel-tests.handler.js +113 -24
- package/dist/parallel-tests/run-all-tests.d.ts +7 -1
- package/dist/parallel-tests/run-all-tests.js +35 -52
- package/dist/parallel-tests/run-all-tests.types.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/sentry.utils.d.ts +0 -1
- package/dist/utils/sentry.utils.js +19 -14
- package/package.json +26 -6
|
@@ -11,25 +11,19 @@ const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
|
11
11
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
12
12
|
const download_1 = require("../api/download");
|
|
13
13
|
const replay_api_1 = require("../api/replay.api");
|
|
14
|
+
const local_data_utils_1 = require("./local-data.utils");
|
|
14
15
|
const getOrFetchReplay = async (client, replayId) => {
|
|
15
16
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
16
|
-
const
|
|
17
|
-
await (0,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (existingReplay) {
|
|
23
|
-
logger.debug(`Reading replay from local copy in ${replayFile}`);
|
|
24
|
-
return existingReplay;
|
|
25
|
-
}
|
|
26
|
-
const replay = await (0, replay_api_1.getReplay)(client, replayId);
|
|
17
|
+
const replayFile = (0, path_1.join)((0, exports.getReplayDir)(replayId), `${replayId}.json`);
|
|
18
|
+
const replay = await (0, local_data_utils_1.getOrDownloadJsonFile)({
|
|
19
|
+
filePath: replayFile,
|
|
20
|
+
dataDescription: "replay",
|
|
21
|
+
downloadJson: () => (0, replay_api_1.getReplay)(client, replayId),
|
|
22
|
+
});
|
|
27
23
|
if (!replay) {
|
|
28
24
|
logger.error(`Error: Could not retrieve replay with id "${replayId}". Is the API token correct?`);
|
|
29
25
|
process.exit(1);
|
|
30
26
|
}
|
|
31
|
-
await (0, promises_1.writeFile)(replayFile, JSON.stringify(replay, null, 2));
|
|
32
|
-
logger.debug(`Wrote replay to ${replayFile}`);
|
|
33
27
|
return replay;
|
|
34
28
|
};
|
|
35
29
|
exports.getOrFetchReplay = getOrFetchReplay;
|
|
@@ -37,27 +31,31 @@ const getOrFetchReplayArchive = async (client, replayId) => {
|
|
|
37
31
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
38
32
|
const replayDir = (0, exports.getReplayDir)(replayId);
|
|
39
33
|
await (0, promises_1.mkdir)(replayDir, { recursive: true });
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
const releaseLock = await (0, local_data_utils_1.waitToAcquireLockOnDirectory)(replayDir);
|
|
35
|
+
try {
|
|
36
|
+
const replayArchiveFile = (0, path_1.join)(replayDir, `${replayId}.zip`);
|
|
37
|
+
const paramsFile = (0, path_1.join)(replayDir, "replayEventsParams.json");
|
|
38
|
+
// Check if "replayEventsParams.json" exists. If yes, we assume the replay
|
|
39
|
+
// zip archive has been downloaded and extracted.
|
|
40
|
+
if (await (0, local_data_utils_1.fileExists)(paramsFile)) {
|
|
41
|
+
logger.debug(`Replay archive already downloaded at ${replayDir}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const downloadUrlData = await (0, replay_api_1.getReplayDownloadUrl)(client, replayId);
|
|
45
|
+
if (!downloadUrlData) {
|
|
46
|
+
logger.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
|
|
47
|
+
await releaseLock();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
await (0, download_1.downloadFile)(downloadUrlData.dowloadUrl, replayArchiveFile);
|
|
51
|
+
const zipFile = new adm_zip_1.default(replayArchiveFile);
|
|
52
|
+
zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
|
|
53
|
+
await (0, promises_1.rm)(replayArchiveFile);
|
|
54
|
+
logger.debug(`Extracted replay archive in ${replayDir}`);
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
logger.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
|
|
54
|
-
process.exit(1);
|
|
56
|
+
finally {
|
|
57
|
+
await releaseLock();
|
|
55
58
|
}
|
|
56
|
-
await (0, download_1.downloadFile)(downloadUrlData.dowloadUrl, replayArchiveFile);
|
|
57
|
-
const zipFile = new adm_zip_1.default(replayArchiveFile);
|
|
58
|
-
zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
|
|
59
|
-
await (0, promises_1.rm)(replayArchiveFile);
|
|
60
|
-
logger.debug(`Exrtracted replay archive in ${replayDir}`);
|
|
61
59
|
};
|
|
62
60
|
exports.getOrFetchReplayArchive = getOrFetchReplayArchive;
|
|
63
61
|
const getScreenshotFiles = async (screenshotsDirPath) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionData } from "@alwaysmeticulous/api";
|
|
1
2
|
import { AxiosInstance } from "axios";
|
|
2
3
|
export declare const getOrFetchRecordedSession: (client: AxiosInstance, sessionId: string) => Promise<any>;
|
|
3
|
-
export declare const getOrFetchRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<
|
|
4
|
+
export declare const getOrFetchRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<SessionData>;
|
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getOrFetchRecordedSessionData = exports.getOrFetchRecordedSession = void 0;
|
|
7
|
-
const promises_1 = require("fs/promises");
|
|
8
7
|
const path_1 = require("path");
|
|
9
8
|
const common_1 = require("@alwaysmeticulous/common");
|
|
10
9
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
@@ -12,45 +11,31 @@ const session_api_1 = require("../api/session.api");
|
|
|
12
11
|
const local_data_utils_1 = require("./local-data.utils");
|
|
13
12
|
const getOrFetchRecordedSession = async (client, sessionId) => {
|
|
14
13
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
15
|
-
const
|
|
16
|
-
await (0,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (existingSession) {
|
|
22
|
-
logger.debug(`Reading session from local copy in ${sessionFile}`);
|
|
23
|
-
return existingSession;
|
|
24
|
-
}
|
|
25
|
-
const session = await (0, session_api_1.getRecordedSession)(client, sessionId);
|
|
14
|
+
const sessionFile = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions", `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}.json`);
|
|
15
|
+
const session = await (0, local_data_utils_1.getOrDownloadJsonFile)({
|
|
16
|
+
filePath: sessionFile,
|
|
17
|
+
dataDescription: "session",
|
|
18
|
+
downloadJson: () => (0, session_api_1.getRecordedSession)(client, sessionId),
|
|
19
|
+
});
|
|
26
20
|
if (!session) {
|
|
27
21
|
logger.error("Error: Could not retrieve session. Is the API token correct?");
|
|
28
22
|
process.exit(1);
|
|
29
23
|
}
|
|
30
|
-
await (0, promises_1.writeFile)(sessionFile, JSON.stringify(session, null, 2));
|
|
31
|
-
logger.debug(`Wrote session to ${sessionFile}`);
|
|
32
24
|
return session;
|
|
33
25
|
};
|
|
34
26
|
exports.getOrFetchRecordedSession = getOrFetchRecordedSession;
|
|
35
27
|
const getOrFetchRecordedSessionData = async (client, sessionId) => {
|
|
36
28
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
37
|
-
const
|
|
38
|
-
await (0,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (existingSessionData) {
|
|
44
|
-
logger.debug(`Reading session data from local copy in ${sessionDataFile}`);
|
|
45
|
-
return existingSessionData;
|
|
46
|
-
}
|
|
47
|
-
const sessionData = await (0, session_api_1.getRecordedSessionData)(client, sessionId);
|
|
29
|
+
const sessionFile = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions", `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}_data.json`);
|
|
30
|
+
const sessionData = await (0, local_data_utils_1.getOrDownloadJsonFile)({
|
|
31
|
+
filePath: sessionFile,
|
|
32
|
+
dataDescription: "session data",
|
|
33
|
+
downloadJson: () => (0, session_api_1.getRecordedSessionData)(client, sessionId),
|
|
34
|
+
});
|
|
48
35
|
if (!sessionData) {
|
|
49
36
|
logger.error("Error: Could not retrieve session data. This may be an invalid session");
|
|
50
37
|
process.exit(1);
|
|
51
38
|
}
|
|
52
|
-
await (0, promises_1.writeFile)(sessionDataFile, JSON.stringify(sessionData, null, 2));
|
|
53
|
-
logger.debug(`Wrote session data to ${sessionDataFile}`);
|
|
54
39
|
return sessionData;
|
|
55
40
|
};
|
|
56
41
|
exports.getOrFetchRecordedSessionData = getOrFetchRecordedSessionData;
|
package/dist/main.js
CHANGED
|
@@ -28,7 +28,7 @@ const main = async () => {
|
|
|
28
28
|
(0, logger_utils_1.initLogger)();
|
|
29
29
|
const meticulousVersion = await (0, version_utils_1.getMeticulousVersion)();
|
|
30
30
|
(0, sentry_utils_1.initSentry)(meticulousVersion);
|
|
31
|
-
|
|
31
|
+
yargs_1.default
|
|
32
32
|
.scriptName("meticulous")
|
|
33
33
|
.usage(`$0 <command>
|
|
34
34
|
|
|
@@ -63,12 +63,6 @@ const main = async () => {
|
|
|
63
63
|
(argv) => handleDataDir(argv.dataDir),
|
|
64
64
|
(argv) => (0, sentry_utils_1.setOptions)(argv),
|
|
65
65
|
]).argv;
|
|
66
|
-
if (promise instanceof Promise) {
|
|
67
|
-
promise.catch((error) => {
|
|
68
|
-
console.error(error);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
66
|
};
|
|
73
67
|
exports.main = main;
|
|
74
68
|
(0, exports.main)();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const merge_test_results_1 = require("../merge-test-results");
|
|
4
|
+
describe("mergeResults", () => {
|
|
5
|
+
it("keeps the result as a failure when all retried screenshots are 'identical'", () => {
|
|
6
|
+
const currentResult = testResult("fail", [diff(0), noDiff(1)]);
|
|
7
|
+
const comparisonToHeadReplay = testResult("pass", [noDiff(0), noDiff(1)]);
|
|
8
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
9
|
+
currentResult,
|
|
10
|
+
comparisonToHeadReplay,
|
|
11
|
+
});
|
|
12
|
+
expect(mergedResult).toEqual(testResult("fail", [diff(0), noDiff(1)]));
|
|
13
|
+
});
|
|
14
|
+
it("ignores diffs to screenshots which originally passed", () => {
|
|
15
|
+
const currentResult = testResult("pass", [noDiff(0), noDiff(1)]);
|
|
16
|
+
const comparisonToHeadReplay = testResult("fail", [noDiff(0), diff(1)]);
|
|
17
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
18
|
+
currentResult,
|
|
19
|
+
comparisonToHeadReplay,
|
|
20
|
+
});
|
|
21
|
+
expect(mergedResult).toEqual(testResult("pass", [noDiff(0), noDiff(1)]));
|
|
22
|
+
});
|
|
23
|
+
it("marks screenshots as flakes if the screenshot comparison originally failed, but the second retry gives a different screenshot again", () => {
|
|
24
|
+
const currentResult = testResult("fail", [noDiff(0), diff(1), diff(2)]);
|
|
25
|
+
const comparisonToHeadReplay = testResult("fail", [
|
|
26
|
+
noDiff(0),
|
|
27
|
+
diff(1),
|
|
28
|
+
noDiff(2),
|
|
29
|
+
]);
|
|
30
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
31
|
+
currentResult,
|
|
32
|
+
comparisonToHeadReplay,
|
|
33
|
+
});
|
|
34
|
+
expect(mergedResult).toEqual(testResult("fail", [noDiff(0), flake(1, diff(), [diff()]), diff(2)]));
|
|
35
|
+
});
|
|
36
|
+
it("marks overall test as a flake if there are only flakey screenshots and no failed screenshots", () => {
|
|
37
|
+
const currentResult = testResult("fail", [noDiff(0), diff(1), diff(2)]);
|
|
38
|
+
const comparisonToHeadReplay = testResult("fail", [
|
|
39
|
+
noDiff(0),
|
|
40
|
+
diff(1),
|
|
41
|
+
diff(2),
|
|
42
|
+
]);
|
|
43
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
44
|
+
currentResult,
|
|
45
|
+
comparisonToHeadReplay,
|
|
46
|
+
});
|
|
47
|
+
expect(mergedResult).toEqual(testResult("flake", [
|
|
48
|
+
noDiff(0),
|
|
49
|
+
flake(1, diff(), [diff()]),
|
|
50
|
+
flake(2, diff(), [diff()]),
|
|
51
|
+
]));
|
|
52
|
+
});
|
|
53
|
+
it("adds to diffsToHeadScreenshotOnRetries for existing flakes", () => {
|
|
54
|
+
const currentResult = testResult("fail", [
|
|
55
|
+
diff(0),
|
|
56
|
+
flake(1, diff(), [missingHead()]),
|
|
57
|
+
flake(2, differentSize(), [diff()]),
|
|
58
|
+
flake(3, missingBase(), [diff(), diff()]),
|
|
59
|
+
]);
|
|
60
|
+
const comparisonToHeadReplay = testResult("fail", [
|
|
61
|
+
noDiff(0),
|
|
62
|
+
diff(1),
|
|
63
|
+
differentSize(2),
|
|
64
|
+
missingHead(3),
|
|
65
|
+
]);
|
|
66
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
67
|
+
currentResult,
|
|
68
|
+
comparisonToHeadReplay,
|
|
69
|
+
});
|
|
70
|
+
expect(mergedResult).toEqual(testResult("fail", [
|
|
71
|
+
diff(0),
|
|
72
|
+
flake(1, diff(), [missingHead(), diff()]),
|
|
73
|
+
flake(2, differentSize(), [diff(), differentSize()]),
|
|
74
|
+
flake(3, missingBase(), [diff(), diff(), missingHead()]),
|
|
75
|
+
]));
|
|
76
|
+
});
|
|
77
|
+
it("keeps a missing-head as is, if there is no corresponding retry screenshot", () => {
|
|
78
|
+
const currentResult = testResult("fail", [missingHead(0)]);
|
|
79
|
+
const comparisonToHeadReplay = testResult("pass", []);
|
|
80
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
81
|
+
currentResult,
|
|
82
|
+
comparisonToHeadReplay,
|
|
83
|
+
});
|
|
84
|
+
expect(mergedResult).toEqual(testResult("fail", [missingHead(0)]));
|
|
85
|
+
});
|
|
86
|
+
it("adds to diffsToHeadScreenshotOnRetries for a flakey missing-head, if there is no corresponding retry screenshot", () => {
|
|
87
|
+
const currentResult = testResult("fail", [
|
|
88
|
+
flake(0, missingHead(), [missingBase()]),
|
|
89
|
+
diff(1),
|
|
90
|
+
]);
|
|
91
|
+
const comparisonToHeadReplay = testResult("pass", [noDiff(1)]);
|
|
92
|
+
const mergedResult = (0, merge_test_results_1.mergeResults)({
|
|
93
|
+
currentResult,
|
|
94
|
+
comparisonToHeadReplay,
|
|
95
|
+
});
|
|
96
|
+
expect(mergedResult).toEqual(testResult("fail", [
|
|
97
|
+
flake(0, missingHead(), [missingBase(), missingBaseAndHead()]),
|
|
98
|
+
diff(1),
|
|
99
|
+
]));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
const id = (eventNumber = 0) => ({
|
|
103
|
+
type: "after-event",
|
|
104
|
+
eventNumber,
|
|
105
|
+
});
|
|
106
|
+
const testResult = (result, screenshotDiffResults) => {
|
|
107
|
+
return {
|
|
108
|
+
sessionId: "mock-session-id",
|
|
109
|
+
headReplayId: "mock-head-replay-id",
|
|
110
|
+
result,
|
|
111
|
+
screenshotDiffResults,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const diff = (eventNumber) => {
|
|
115
|
+
return {
|
|
116
|
+
identifier: id(eventNumber),
|
|
117
|
+
outcome: "diff",
|
|
118
|
+
headScreenshotFile: "mock-head-file",
|
|
119
|
+
baseScreenshotFile: "mock-base-file",
|
|
120
|
+
width: 1920,
|
|
121
|
+
height: 1080,
|
|
122
|
+
mismatchFraction: 0.01,
|
|
123
|
+
mismatchPixels: 1000,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const noDiff = (eventNumber) => {
|
|
127
|
+
return {
|
|
128
|
+
identifier: id(eventNumber),
|
|
129
|
+
outcome: "no-diff",
|
|
130
|
+
headScreenshotFile: "mock-head-file",
|
|
131
|
+
baseScreenshotFile: "mock-base-file",
|
|
132
|
+
width: 1920,
|
|
133
|
+
height: 1080,
|
|
134
|
+
mismatchFraction: 0.01,
|
|
135
|
+
mismatchPixels: 1000,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const flake = (eventNumber, diffToBaseScreenshot, diffsToHeadScreenshotOnRetries) => {
|
|
139
|
+
return {
|
|
140
|
+
identifier: id(eventNumber),
|
|
141
|
+
outcome: "flake",
|
|
142
|
+
diffToBaseScreenshot: withoutIdentifier(diffToBaseScreenshot),
|
|
143
|
+
diffsToHeadScreenshotOnRetries: diffsToHeadScreenshotOnRetries.map(withoutIdentifier),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const missingBase = (eventNumber) => {
|
|
147
|
+
return {
|
|
148
|
+
identifier: id(eventNumber),
|
|
149
|
+
outcome: "missing-base",
|
|
150
|
+
headScreenshotFile: "mock-head-file",
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const missingHead = (eventNumber) => {
|
|
154
|
+
return {
|
|
155
|
+
identifier: id(eventNumber),
|
|
156
|
+
outcome: "missing-head",
|
|
157
|
+
baseScreenshotFile: "mock-base-file",
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const missingBaseAndHead = (eventNumber) => {
|
|
161
|
+
return {
|
|
162
|
+
identifier: id(eventNumber),
|
|
163
|
+
outcome: "missing-base-and-head",
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const differentSize = (eventNumber) => {
|
|
167
|
+
return {
|
|
168
|
+
identifier: id(eventNumber),
|
|
169
|
+
outcome: "different-size",
|
|
170
|
+
baseScreenshotFile: "mock-base-file",
|
|
171
|
+
headScreenshotFile: "mock-head-file",
|
|
172
|
+
baseWidth: 1920,
|
|
173
|
+
baseHeight: 1080,
|
|
174
|
+
headWidth: 10,
|
|
175
|
+
headHeight: 5,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const withoutIdentifier = ({ identifier, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
179
|
+
...rest }) => rest;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DetailedTestCaseResult } from "../config/config.types";
|
|
2
|
+
export interface ResultsToMerge {
|
|
3
|
+
currentResult: DetailedTestCaseResult;
|
|
4
|
+
/**
|
|
5
|
+
* The result of replaying the session once more against the head commit and
|
|
6
|
+
* comparing the screenshots to those taken on the first replay against the head commit.
|
|
7
|
+
*/
|
|
8
|
+
comparisonToHeadReplay: DetailedTestCaseResult;
|
|
9
|
+
}
|
|
10
|
+
export declare const mergeResults: ({ currentResult, comparisonToHeadReplay, }: ResultsToMerge) => DetailedTestCaseResult;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeResults = void 0;
|
|
4
|
+
const utils_1 = require("@sentry/utils");
|
|
5
|
+
const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
|
|
6
|
+
// If any of the screenshots diffs in comparisonToHeadReplay show a diff against one
|
|
7
|
+
// of the screenshots that orignally failed in currentResult then we have a flake
|
|
8
|
+
// on our hands
|
|
9
|
+
const retryDiffById = new Map();
|
|
10
|
+
comparisonToHeadReplay.screenshotDiffResults.forEach((result) => {
|
|
11
|
+
const hash = hashScreenshotIdentifier(result.identifier);
|
|
12
|
+
if (retryDiffById.has(hash)) {
|
|
13
|
+
throw new Error(`Received two screenshots for the same identifier '${hash}'. Screenshots should be unique.`);
|
|
14
|
+
}
|
|
15
|
+
retryDiffById.set(hash, result);
|
|
16
|
+
});
|
|
17
|
+
const newScreenshotDiffResults = currentResult.screenshotDiffResults.map((currentDiff) => {
|
|
18
|
+
if (currentDiff.outcome === "no-diff") {
|
|
19
|
+
return currentDiff;
|
|
20
|
+
}
|
|
21
|
+
const hash = hashScreenshotIdentifier(currentDiff.identifier);
|
|
22
|
+
const diffWhenRetrying = retryDiffById.get(hash);
|
|
23
|
+
// diffWhenRetrying is null in the case that there is a base screenshot for the base replay,
|
|
24
|
+
// but the first replay on head did not generate a head screenshot (got 'missing-head'),
|
|
25
|
+
// and the replay of that did not generate a head screenshot either (if the first replay on head
|
|
26
|
+
// did generate a screenshot, then diffWhenRetrying.outcome would be 'missing-head' instead)
|
|
27
|
+
if (diffWhenRetrying == null) {
|
|
28
|
+
const diffToBaseScreenshotOutcome = currentDiff.outcome === "flake"
|
|
29
|
+
? currentDiff.diffToBaseScreenshot.outcome
|
|
30
|
+
: currentDiff.outcome;
|
|
31
|
+
if (diffToBaseScreenshotOutcome !== "missing-head") {
|
|
32
|
+
throw new Error(`Expected to find a screenshot comparison for ${hash}, but none was found. The screenshot must
|
|
33
|
+
have existed in the orginal replay, since the orginal comparison outcome was not '${diffToBaseScreenshotOutcome}'.`);
|
|
34
|
+
}
|
|
35
|
+
if (currentDiff.outcome === "flake") {
|
|
36
|
+
// Original comparison had missing-head, so we re-ran to check if it was flakey. In this case
|
|
37
|
+
// we got the same result.
|
|
38
|
+
return {
|
|
39
|
+
...currentDiff,
|
|
40
|
+
diffsToHeadScreenshotOnRetries: [
|
|
41
|
+
...currentDiff.diffsToHeadScreenshotOnRetries,
|
|
42
|
+
{ outcome: "missing-base-and-head" },
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return currentDiff; // no difference, both screenshots were missing
|
|
47
|
+
}
|
|
48
|
+
if (currentDiff.outcome === "flake") {
|
|
49
|
+
return {
|
|
50
|
+
...currentDiff,
|
|
51
|
+
diffsToHeadScreenshotOnRetries: [
|
|
52
|
+
...currentDiff.diffsToHeadScreenshotOnRetries,
|
|
53
|
+
withoutIdentifier(diffWhenRetrying),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else if ((diffWhenRetrying === null || diffWhenRetrying === void 0 ? void 0 : diffWhenRetrying.outcome) === "no-diff") {
|
|
58
|
+
return currentDiff;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return {
|
|
62
|
+
identifier: currentDiff.identifier,
|
|
63
|
+
outcome: "flake",
|
|
64
|
+
diffToBaseScreenshot: withoutIdentifier(currentDiff),
|
|
65
|
+
diffsToHeadScreenshotOnRetries: [withoutIdentifier(diffWhenRetrying)],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const noLongerHasFailures = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
|
|
70
|
+
return {
|
|
71
|
+
...currentResult,
|
|
72
|
+
result: currentResult.result === "fail" && noLongerHasFailures
|
|
73
|
+
? "flake"
|
|
74
|
+
: currentResult.result,
|
|
75
|
+
screenshotDiffResults: newScreenshotDiffResults,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
exports.mergeResults = mergeResults;
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
80
|
+
const withoutIdentifier = ({ identifier, ...rest }) => rest;
|
|
81
|
+
const hashScreenshotIdentifier = (identifier) => {
|
|
82
|
+
if (identifier.type === "end-state") {
|
|
83
|
+
return "end-state";
|
|
84
|
+
}
|
|
85
|
+
else if (identifier.type === "after-event") {
|
|
86
|
+
return `after-event-${identifier.eventNumber}`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
unknownScreenshotIdentifierType(identifier);
|
|
90
|
+
// The identifier is probably from a newer version of the bundle script
|
|
91
|
+
// and we're on an old version of the CLI. Our best bet is to stringify it
|
|
92
|
+
// and use that as a hash.
|
|
93
|
+
return JSON.stringify(identifier);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const unknownScreenshotIdentifierType = (identifier) => {
|
|
97
|
+
utils_1.logger.error(`Unknown type of screenshot identifier: ${JSON.stringify(identifier)}`);
|
|
98
|
+
};
|
|
@@ -17,6 +17,7 @@ export interface RunAllTestsInParallelOptions {
|
|
|
17
17
|
parallelTasks: number | null;
|
|
18
18
|
deflake: boolean;
|
|
19
19
|
replayEventsDependencies: ReplayEventsDependencies;
|
|
20
|
+
maxRetriesOnFailure: number;
|
|
20
21
|
onTestFinished?: (progress: TestRunProgress, resultsSoFar: DetailedTestCaseResult[]) => Promise<void>;
|
|
21
22
|
}
|
|
22
23
|
/** Handler for running Meticulous tests in parallel using child processes */
|