@flakiness/sdk 0.146.1 → 0.147.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/LICENSE +21 -45
- package/README.md +2 -27
- package/lib/browser/index.js +127 -0
- package/lib/createEnvironment.js +78 -0
- package/lib/createTestStepSnippets.js +142 -4
- package/lib/flakinessProjectConfig.js +22 -100
- package/lib/git.js +55 -0
- package/lib/{serverapi.js → httpUtils.js} +13 -25
- package/lib/index.js +695 -142
- package/lib/localGit.js +4 -2
- package/lib/localReportApi.js +155 -125
- package/lib/localReportServer.js +159 -129
- package/lib/pathutils.js +20 -0
- package/lib/reportUploader.js +77 -37
- package/lib/reportUtils.js +123 -0
- package/lib/showReport.js +195 -152
- package/lib/utils.js +0 -306
- package/package.json +8 -34
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/playwright-test.js +0 -1092
package/lib/showReport.js
CHANGED
|
@@ -1,102 +1,31 @@
|
|
|
1
1
|
// src/showReport.ts
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import open from "open";
|
|
4
|
-
import
|
|
4
|
+
import path3 from "path";
|
|
5
5
|
|
|
6
6
|
// src/flakinessProjectConfig.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
9
|
|
|
10
|
-
// src/
|
|
11
|
-
import { ReportUtils } from "@flakiness/report";
|
|
10
|
+
// src/git.ts
|
|
12
11
|
import assert from "assert";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
23
|
-
for (const timeout of backoff) {
|
|
24
|
-
try {
|
|
25
|
-
return await job();
|
|
26
|
-
} catch (e) {
|
|
27
|
-
if (e instanceof AggregateError)
|
|
28
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
29
|
-
else if (e instanceof Error)
|
|
30
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
31
|
-
else
|
|
32
|
-
console.error(`[flakiness.io err]`, e);
|
|
33
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
34
|
-
}
|
|
12
|
+
|
|
13
|
+
// src/pathutils.ts
|
|
14
|
+
import { posix as posixPath, win32 as win32Path } from "path";
|
|
15
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
16
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
17
|
+
function normalizePath(aPath) {
|
|
18
|
+
if (IS_WIN32_PATH.test(aPath)) {
|
|
19
|
+
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
35
20
|
}
|
|
36
|
-
|
|
21
|
+
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
22
|
+
return "/" + aPath[0] + aPath.substring(2);
|
|
23
|
+
return aPath;
|
|
37
24
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let reject;
|
|
43
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
44
|
-
resolve = a;
|
|
45
|
-
reject = b;
|
|
46
|
-
});
|
|
47
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
48
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
49
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
50
|
-
const chunks = [];
|
|
51
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
52
|
-
res.on("end", () => {
|
|
53
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
54
|
-
resolve(Buffer.concat(chunks));
|
|
55
|
-
else
|
|
56
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
57
|
-
});
|
|
58
|
-
res.on("error", (error) => reject(error));
|
|
59
|
-
});
|
|
60
|
-
request.on("error", reject);
|
|
61
|
-
return { request, responseDataPromise };
|
|
62
|
-
}
|
|
63
|
-
httpUtils2.createRequest = createRequest;
|
|
64
|
-
async function getBuffer(url, backoff) {
|
|
65
|
-
return await retryWithBackoff(async () => {
|
|
66
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
67
|
-
request.end();
|
|
68
|
-
return await responseDataPromise;
|
|
69
|
-
}, backoff);
|
|
70
|
-
}
|
|
71
|
-
httpUtils2.getBuffer = getBuffer;
|
|
72
|
-
async function getText(url, backoff) {
|
|
73
|
-
const buffer = await getBuffer(url, backoff);
|
|
74
|
-
return buffer.toString("utf-8");
|
|
75
|
-
}
|
|
76
|
-
httpUtils2.getText = getText;
|
|
77
|
-
async function getJSON(url) {
|
|
78
|
-
return JSON.parse(await getText(url));
|
|
79
|
-
}
|
|
80
|
-
httpUtils2.getJSON = getJSON;
|
|
81
|
-
async function postText(url, text, backoff) {
|
|
82
|
-
const headers = {
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
85
|
-
};
|
|
86
|
-
return await retryWithBackoff(async () => {
|
|
87
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
88
|
-
request.write(text);
|
|
89
|
-
request.end();
|
|
90
|
-
return await responseDataPromise;
|
|
91
|
-
}, backoff);
|
|
92
|
-
}
|
|
93
|
-
httpUtils2.postText = postText;
|
|
94
|
-
async function postJSON(url, json, backoff) {
|
|
95
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
96
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
97
|
-
}
|
|
98
|
-
httpUtils2.postJSON = postJSON;
|
|
99
|
-
})(httpUtils || (httpUtils = {}));
|
|
25
|
+
|
|
26
|
+
// src/utils.ts
|
|
27
|
+
import { spawnSync } from "child_process";
|
|
28
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
100
29
|
var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
|
|
101
30
|
function shell(command, args, options) {
|
|
102
31
|
try {
|
|
@@ -110,40 +39,8 @@ function shell(command, args, options) {
|
|
|
110
39
|
return void 0;
|
|
111
40
|
}
|
|
112
41
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
116
|
-
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
117
|
-
const missingAttachments = /* @__PURE__ */ new Set();
|
|
118
|
-
ReportUtils.visitTests(report, (test) => {
|
|
119
|
-
for (const attempt of test.attempts) {
|
|
120
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
121
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
122
|
-
if (!attachmentPath) {
|
|
123
|
-
missingAttachments.add(attachment.id);
|
|
124
|
-
} else {
|
|
125
|
-
attachmentIdToPath.set(attachment.id, {
|
|
126
|
-
contentType: attachment.contentType,
|
|
127
|
-
id: attachment.id,
|
|
128
|
-
path: attachmentPath
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
135
|
-
}
|
|
136
|
-
async function listFilesRecursively(dir, result = []) {
|
|
137
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
138
|
-
for (const entry of entries) {
|
|
139
|
-
const fullPath = path.join(dir, entry.name);
|
|
140
|
-
if (entry.isDirectory())
|
|
141
|
-
await listFilesRecursively(fullPath, result);
|
|
142
|
-
else
|
|
143
|
-
result.push(fullPath);
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
42
|
+
|
|
43
|
+
// src/git.ts
|
|
147
44
|
function computeGitRoot(somePathInsideGitRepo) {
|
|
148
45
|
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
149
46
|
cwd: somePathInsideGitRepo,
|
|
@@ -152,20 +49,10 @@ function computeGitRoot(somePathInsideGitRepo) {
|
|
|
152
49
|
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
153
50
|
return normalizePath(root);
|
|
154
51
|
}
|
|
155
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
156
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
157
|
-
function normalizePath(aPath) {
|
|
158
|
-
if (IS_WIN32_PATH.test(aPath)) {
|
|
159
|
-
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
160
|
-
}
|
|
161
|
-
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
162
|
-
return "/" + aPath[0] + aPath.substring(2);
|
|
163
|
-
return aPath;
|
|
164
|
-
}
|
|
165
52
|
|
|
166
53
|
// src/flakinessProjectConfig.ts
|
|
167
54
|
function createConfigPath(dir) {
|
|
168
|
-
return
|
|
55
|
+
return path.join(dir, ".flakiness", "config.json");
|
|
169
56
|
}
|
|
170
57
|
var gConfigPath;
|
|
171
58
|
function ensureConfigPath() {
|
|
@@ -174,9 +61,9 @@ function ensureConfigPath() {
|
|
|
174
61
|
return gConfigPath;
|
|
175
62
|
}
|
|
176
63
|
function computeConfigPath() {
|
|
177
|
-
for (let p = process.cwd(); p !==
|
|
64
|
+
for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
|
|
178
65
|
const configPath = createConfigPath(p);
|
|
179
|
-
if (
|
|
66
|
+
if (fs.existsSync(configPath))
|
|
180
67
|
return configPath;
|
|
181
68
|
}
|
|
182
69
|
try {
|
|
@@ -193,7 +80,7 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
193
80
|
}
|
|
194
81
|
static async load() {
|
|
195
82
|
const configPath = ensureConfigPath();
|
|
196
|
-
const data = await
|
|
83
|
+
const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
197
84
|
const json = data ? JSON.parse(data) : {};
|
|
198
85
|
return new _FlakinessProjectConfig(configPath, json);
|
|
199
86
|
}
|
|
@@ -213,8 +100,8 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
213
100
|
this._config.projectPublicId = projectId;
|
|
214
101
|
}
|
|
215
102
|
async save() {
|
|
216
|
-
await
|
|
217
|
-
await
|
|
103
|
+
await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
|
|
104
|
+
await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
218
105
|
}
|
|
219
106
|
};
|
|
220
107
|
|
|
@@ -224,20 +111,22 @@ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
|
224
111
|
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
225
112
|
import bodyParser from "body-parser";
|
|
226
113
|
import compression from "compression";
|
|
227
|
-
import
|
|
114
|
+
import debug2 from "debug";
|
|
228
115
|
import express from "express";
|
|
229
116
|
import "express-async-errors";
|
|
230
|
-
import
|
|
117
|
+
import http from "http";
|
|
231
118
|
|
|
232
119
|
// src/localReportApi.ts
|
|
233
120
|
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
234
|
-
import
|
|
235
|
-
import
|
|
121
|
+
import fs2 from "fs";
|
|
122
|
+
import path2 from "path";
|
|
236
123
|
import { z } from "zod/v4";
|
|
237
124
|
|
|
238
125
|
// src/localGit.ts
|
|
239
126
|
import { exec } from "child_process";
|
|
127
|
+
import debug from "debug";
|
|
240
128
|
import { promisify } from "util";
|
|
129
|
+
var log = debug("fk:git");
|
|
241
130
|
var execAsync = promisify(exec);
|
|
242
131
|
async function listLocalCommits(gitRoot, head, count) {
|
|
243
132
|
const FIELD_SEPARATOR = "|~|";
|
|
@@ -273,11 +162,131 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
273
162
|
};
|
|
274
163
|
});
|
|
275
164
|
} catch (error) {
|
|
276
|
-
|
|
277
|
-
|
|
165
|
+
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
166
|
+
return [];
|
|
278
167
|
}
|
|
279
168
|
}
|
|
280
169
|
|
|
170
|
+
// src/reportUtils.ts
|
|
171
|
+
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
172
|
+
import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
|
|
173
|
+
var ReportUtils;
|
|
174
|
+
((ReportUtils2) => {
|
|
175
|
+
function visitTests(report, testVisitor) {
|
|
176
|
+
function visitSuite(suite, parents) {
|
|
177
|
+
parents.push(suite);
|
|
178
|
+
for (const test of suite.tests ?? [])
|
|
179
|
+
testVisitor(test, parents);
|
|
180
|
+
for (const childSuite of suite.suites ?? [])
|
|
181
|
+
visitSuite(childSuite, parents);
|
|
182
|
+
parents.pop();
|
|
183
|
+
}
|
|
184
|
+
for (const test of report.tests ?? [])
|
|
185
|
+
testVisitor(test, []);
|
|
186
|
+
for (const suite of report.suites)
|
|
187
|
+
visitSuite(suite, []);
|
|
188
|
+
}
|
|
189
|
+
ReportUtils2.visitTests = visitTests;
|
|
190
|
+
function normalizeReport(report) {
|
|
191
|
+
const gEnvs = /* @__PURE__ */ new Map();
|
|
192
|
+
const gSuites = /* @__PURE__ */ new Map();
|
|
193
|
+
const gTests = new Multimap();
|
|
194
|
+
const gSuiteIds = /* @__PURE__ */ new Map();
|
|
195
|
+
const gTestIds = /* @__PURE__ */ new Map();
|
|
196
|
+
const gEnvIds = /* @__PURE__ */ new Map();
|
|
197
|
+
const gSuiteChildren = new Multimap();
|
|
198
|
+
const gSuiteTests = new Multimap();
|
|
199
|
+
for (const env of report.environments) {
|
|
200
|
+
const envId = computeEnvId(env);
|
|
201
|
+
gEnvs.set(envId, env);
|
|
202
|
+
gEnvIds.set(env, envId);
|
|
203
|
+
}
|
|
204
|
+
const usedEnvIds = /* @__PURE__ */ new Set();
|
|
205
|
+
function visitTests2(tests, suiteId) {
|
|
206
|
+
for (const test of tests ?? []) {
|
|
207
|
+
const testId = computeTestId(test, suiteId);
|
|
208
|
+
gTests.set(testId, test);
|
|
209
|
+
gTestIds.set(test, testId);
|
|
210
|
+
gSuiteTests.set(suiteId, test);
|
|
211
|
+
for (const attempt of test.attempts) {
|
|
212
|
+
const env = report.environments[attempt.environmentIdx];
|
|
213
|
+
const envId = gEnvIds.get(env);
|
|
214
|
+
usedEnvIds.add(envId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function visitSuite(suite, parentSuiteId) {
|
|
219
|
+
const suiteId = computeSuiteId(suite, parentSuiteId);
|
|
220
|
+
gSuites.set(suiteId, suite);
|
|
221
|
+
gSuiteIds.set(suite, suiteId);
|
|
222
|
+
for (const childSuite of suite.suites ?? []) {
|
|
223
|
+
visitSuite(childSuite, suiteId);
|
|
224
|
+
gSuiteChildren.set(suiteId, childSuite);
|
|
225
|
+
}
|
|
226
|
+
visitTests2(suite.tests ?? [], suiteId);
|
|
227
|
+
}
|
|
228
|
+
function transformTests(tests) {
|
|
229
|
+
const testIds = new Set(tests.map((test) => gTestIds.get(test)));
|
|
230
|
+
return [...testIds].map((testId) => {
|
|
231
|
+
const tests2 = gTests.getAll(testId);
|
|
232
|
+
const tags = tests2.map((test) => test.tags ?? []).flat();
|
|
233
|
+
return {
|
|
234
|
+
location: tests2[0].location,
|
|
235
|
+
title: tests2[0].title,
|
|
236
|
+
tags: tags.length ? tags : void 0,
|
|
237
|
+
attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
|
|
238
|
+
...attempt,
|
|
239
|
+
environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
|
|
240
|
+
}))
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function transformSuites(suites) {
|
|
245
|
+
const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
|
|
246
|
+
return [...suiteIds].map((suiteId) => {
|
|
247
|
+
const suite = gSuites.get(suiteId);
|
|
248
|
+
return {
|
|
249
|
+
location: suite.location,
|
|
250
|
+
title: suite.title,
|
|
251
|
+
type: suite.type,
|
|
252
|
+
suites: transformSuites(gSuiteChildren.getAll(suiteId)),
|
|
253
|
+
tests: transformTests(gSuiteTests.getAll(suiteId))
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
visitTests2(report.tests ?? [], "suiteless");
|
|
258
|
+
for (const suite of report.suites)
|
|
259
|
+
visitSuite(suite);
|
|
260
|
+
const newEnvironments = [...usedEnvIds];
|
|
261
|
+
const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
|
|
262
|
+
return {
|
|
263
|
+
...report,
|
|
264
|
+
environments: newEnvironments.map((envId) => gEnvs.get(envId)),
|
|
265
|
+
suites: transformSuites(report.suites),
|
|
266
|
+
tests: transformTests(report.tests ?? [])
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
ReportUtils2.normalizeReport = normalizeReport;
|
|
270
|
+
function computeEnvId(env) {
|
|
271
|
+
return xxHashObject(env);
|
|
272
|
+
}
|
|
273
|
+
function computeSuiteId(suite, parentSuiteId) {
|
|
274
|
+
return xxHash([
|
|
275
|
+
parentSuiteId ?? "",
|
|
276
|
+
suite.type,
|
|
277
|
+
suite.location?.file ?? "",
|
|
278
|
+
suite.title
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
function computeTestId(test, suiteId) {
|
|
282
|
+
return xxHash([
|
|
283
|
+
suiteId,
|
|
284
|
+
test.location?.file ?? "",
|
|
285
|
+
test.title
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
})(ReportUtils || (ReportUtils = {}));
|
|
289
|
+
|
|
281
290
|
// src/localReportApi.ts
|
|
282
291
|
var ReportInfo = class {
|
|
283
292
|
constructor(_options) {
|
|
@@ -287,7 +296,7 @@ var ReportInfo = class {
|
|
|
287
296
|
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
288
297
|
commits = [];
|
|
289
298
|
async refresh() {
|
|
290
|
-
const report = await
|
|
299
|
+
const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
291
300
|
if (!report) {
|
|
292
301
|
this.report = void 0;
|
|
293
302
|
this.commits = [];
|
|
@@ -297,7 +306,7 @@ var ReportInfo = class {
|
|
|
297
306
|
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
298
307
|
return;
|
|
299
308
|
this.report = report;
|
|
300
|
-
this.commits = await listLocalCommits(
|
|
309
|
+
this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
|
|
301
310
|
const attachmentsDir = this._options.attachmentsFolder;
|
|
302
311
|
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
303
312
|
if (missingAttachments.length) {
|
|
@@ -331,7 +340,7 @@ var localReportRouter = {
|
|
|
331
340
|
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
332
341
|
if (!idx)
|
|
333
342
|
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
334
|
-
const buffer = await
|
|
343
|
+
const buffer = await fs2.promises.readFile(idx.path);
|
|
335
344
|
return TypedHTTP.ok(buffer, idx.contentType);
|
|
336
345
|
}
|
|
337
346
|
}),
|
|
@@ -343,9 +352,43 @@ var localReportRouter = {
|
|
|
343
352
|
})
|
|
344
353
|
}
|
|
345
354
|
};
|
|
355
|
+
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
356
|
+
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
357
|
+
const filenameToPath = new Map(attachmentFiles.map((file) => [path2.basename(file), file]));
|
|
358
|
+
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
359
|
+
const missingAttachments = /* @__PURE__ */ new Set();
|
|
360
|
+
ReportUtils.visitTests(report, (test) => {
|
|
361
|
+
for (const attempt of test.attempts) {
|
|
362
|
+
for (const attachment of attempt.attachments ?? []) {
|
|
363
|
+
const attachmentPath = filenameToPath.get(attachment.id);
|
|
364
|
+
if (!attachmentPath) {
|
|
365
|
+
missingAttachments.add(attachment.id);
|
|
366
|
+
} else {
|
|
367
|
+
attachmentIdToPath.set(attachment.id, {
|
|
368
|
+
contentType: attachment.contentType,
|
|
369
|
+
id: attachment.id,
|
|
370
|
+
path: attachmentPath
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
377
|
+
}
|
|
378
|
+
async function listFilesRecursively(dir, result = []) {
|
|
379
|
+
const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const fullPath = path2.join(dir, entry.name);
|
|
382
|
+
if (entry.isDirectory())
|
|
383
|
+
await listFilesRecursively(fullPath, result);
|
|
384
|
+
else
|
|
385
|
+
result.push(fullPath);
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
346
389
|
|
|
347
390
|
// src/localReportServer.ts
|
|
348
|
-
var logHTTPServer =
|
|
391
|
+
var logHTTPServer = debug2("fk:http");
|
|
349
392
|
var LocalReportServer = class _LocalReportServer {
|
|
350
393
|
constructor(_server, _port, _authToken) {
|
|
351
394
|
this._server = _server;
|
|
@@ -386,7 +429,7 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
386
429
|
logHTTPServer(err);
|
|
387
430
|
res.status(500).send({ error: "Internal Server Error" });
|
|
388
431
|
});
|
|
389
|
-
const server =
|
|
432
|
+
const server = http.createServer(app);
|
|
390
433
|
server.on("error", (err) => {
|
|
391
434
|
if (err.code === "ECONNRESET") {
|
|
392
435
|
logHTTPServer("Client connection reset. Ignoring.");
|
|
@@ -412,7 +455,7 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
412
455
|
|
|
413
456
|
// src/showReport.ts
|
|
414
457
|
async function showReport(reportFolder) {
|
|
415
|
-
const reportPath =
|
|
458
|
+
const reportPath = path3.join(reportFolder, "report.json");
|
|
416
459
|
const config = await FlakinessProjectConfig.load();
|
|
417
460
|
const projectPublicId = config.projectPublicId();
|
|
418
461
|
const reportViewerEndpoint = config.reportViewerEndpoint();
|