@flakiness/sdk 0.129.4 → 0.131.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/lib/cli/cli.js +179 -90
- package/lib/cli/cmd-convert.js +35 -24
- package/lib/cli/cmd-link.js +79 -4
- package/lib/cli/cmd-login.js +6 -3
- package/lib/cli/cmd-show-report.js +45 -21
- package/lib/junit.js +3 -3
- package/lib/localReportApi.js +207 -5
- package/lib/localReportServer.js +76 -52
- package/lib/playwright-test.js +87 -58
- package/lib/utils.js +24 -0
- package/package.json +10 -5
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cmd-link.js
CHANGED
|
@@ -249,13 +249,88 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
249
249
|
}
|
|
250
250
|
};
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
|
|
252
|
+
// ../server/lib/common/knownClientIds.js
|
|
253
|
+
var KNOWN_CLIENT_IDS = {
|
|
254
|
+
OFFICIAL_WEB: "flakiness-io-official-cli",
|
|
255
|
+
OFFICIAL_CLI: "flakiness-io-official-website"
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/cli/cmd-login.ts
|
|
259
|
+
import open from "open";
|
|
260
|
+
import os2 from "os";
|
|
261
|
+
|
|
262
|
+
// src/cli/cmd-logout.ts
|
|
263
|
+
async function cmdLogout() {
|
|
254
264
|
const session = await FlakinessSession.load();
|
|
255
|
-
if (!session)
|
|
256
|
-
|
|
265
|
+
if (!session)
|
|
266
|
+
return;
|
|
267
|
+
const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
|
|
268
|
+
if (currentSession)
|
|
269
|
+
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
270
|
+
await FlakinessSession.remove();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/cli/cmd-login.ts
|
|
274
|
+
var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
|
|
275
|
+
async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
276
|
+
await cmdLogout();
|
|
277
|
+
const api = createServerAPI(endpoint);
|
|
278
|
+
const data = await api.deviceauth.createRequest.POST({
|
|
279
|
+
clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
|
|
280
|
+
name: os2.hostname()
|
|
281
|
+
});
|
|
282
|
+
await open(new URL(data.verificationUrl, endpoint).href);
|
|
283
|
+
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
284
|
+
let token;
|
|
285
|
+
while (Date.now() < data.deadline) {
|
|
286
|
+
await new Promise((x) => setTimeout(x, 2e3));
|
|
287
|
+
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
|
|
288
|
+
if (!result) {
|
|
289
|
+
console.error(`Authorization request was rejected.`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
token = result.token;
|
|
293
|
+
if (token)
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (!token) {
|
|
297
|
+
console.log(`Failed to login.`);
|
|
257
298
|
process.exit(1);
|
|
258
299
|
}
|
|
300
|
+
const session = new FlakinessSession({
|
|
301
|
+
endpoint,
|
|
302
|
+
token
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
const user = await session.api.user.whoami.GET();
|
|
306
|
+
await session.save();
|
|
307
|
+
console.log(`\u2713 Logged in as ${user.userName} (${user.userLogin})`);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
310
|
+
console.error(`x Failed to login:`, message);
|
|
311
|
+
}
|
|
312
|
+
return session;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/cli/cmd-link.ts
|
|
316
|
+
async function cmdLink(slugOrUrl) {
|
|
317
|
+
let slug = slugOrUrl;
|
|
318
|
+
let endpoint = DEFAULT_FLAKINESS_ENDPOINT;
|
|
319
|
+
if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
|
|
320
|
+
const url = URL.parse(slugOrUrl);
|
|
321
|
+
if (!url) {
|
|
322
|
+
console.error(`Invalid URL: ${slugOrUrl}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
slug = url.pathname.substring(1);
|
|
326
|
+
endpoint = url.origin;
|
|
327
|
+
} else if (slugOrUrl.startsWith("flakiness.io/")) {
|
|
328
|
+
endpoint = "https://flakiness.io";
|
|
329
|
+
slug = slugOrUrl.substring("flakiness.io/".length);
|
|
330
|
+
}
|
|
331
|
+
let session = await FlakinessSession.load();
|
|
332
|
+
if (!session || session.endpoint() !== endpoint)
|
|
333
|
+
session = await cmdLogin(endpoint);
|
|
259
334
|
const [orgSlug, projectSlug] = slug.split("/");
|
|
260
335
|
const project = await session.api.project.findProject.GET({
|
|
261
336
|
orgSlug,
|
package/lib/cli/cmd-login.js
CHANGED
|
@@ -176,7 +176,8 @@ async function cmdLogout() {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/cli/cmd-login.ts
|
|
179
|
-
|
|
179
|
+
var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
|
|
180
|
+
async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
180
181
|
await cmdLogout();
|
|
181
182
|
const api = createServerAPI(endpoint);
|
|
182
183
|
const data = await api.deviceauth.createRequest.POST({
|
|
@@ -190,8 +191,8 @@ async function cmdLogin(endpoint) {
|
|
|
190
191
|
await new Promise((x) => setTimeout(x, 2e3));
|
|
191
192
|
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
|
|
192
193
|
if (!result) {
|
|
193
|
-
console.
|
|
194
|
-
|
|
194
|
+
console.error(`Authorization request was rejected.`);
|
|
195
|
+
process.exit(1);
|
|
195
196
|
}
|
|
196
197
|
token = result.token;
|
|
197
198
|
if (token)
|
|
@@ -213,8 +214,10 @@ async function cmdLogin(endpoint) {
|
|
|
213
214
|
const message = e instanceof Error ? e.message : String(e);
|
|
214
215
|
console.error(`x Failed to login:`, message);
|
|
215
216
|
}
|
|
217
|
+
return session;
|
|
216
218
|
}
|
|
217
219
|
export {
|
|
220
|
+
DEFAULT_FLAKINESS_ENDPOINT,
|
|
218
221
|
cmdLogin
|
|
219
222
|
};
|
|
220
223
|
//# sourceMappingURL=cmd-login.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/cli/cmd-show-report.ts
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import open from "open";
|
|
4
|
-
import
|
|
4
|
+
import path5 from "path";
|
|
5
5
|
|
|
6
6
|
// src/flakinessConfig.ts
|
|
7
7
|
import fs2 from "fs";
|
|
@@ -298,9 +298,14 @@ import compression from "compression";
|
|
|
298
298
|
import debug from "debug";
|
|
299
299
|
import express from "express";
|
|
300
300
|
import "express-async-errors";
|
|
301
|
-
import fs5 from "fs";
|
|
302
301
|
import http2 from "http";
|
|
303
302
|
|
|
303
|
+
// src/localReportApi.ts
|
|
304
|
+
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
305
|
+
import fs4 from "fs";
|
|
306
|
+
import path4 from "path";
|
|
307
|
+
import { z } from "zod/v4";
|
|
308
|
+
|
|
304
309
|
// src/localGit.ts
|
|
305
310
|
import { exec } from "child_process";
|
|
306
311
|
import { promisify } from "util";
|
|
@@ -342,9 +347,37 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
342
347
|
}
|
|
343
348
|
|
|
344
349
|
// src/localReportApi.ts
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
350
|
+
var ReportInfo = class {
|
|
351
|
+
constructor(_options) {
|
|
352
|
+
this._options = _options;
|
|
353
|
+
}
|
|
354
|
+
report;
|
|
355
|
+
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
356
|
+
commits = [];
|
|
357
|
+
async refresh() {
|
|
358
|
+
const report = await fs4.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
359
|
+
if (!report) {
|
|
360
|
+
this.report = void 0;
|
|
361
|
+
this.commits = [];
|
|
362
|
+
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
366
|
+
return;
|
|
367
|
+
this.report = report;
|
|
368
|
+
this.commits = await listLocalCommits(path4.dirname(this._options.reportPath), report.commitId, 100);
|
|
369
|
+
const attachmentsDir = this._options.attachmentsFolder;
|
|
370
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
371
|
+
if (missingAttachments.length) {
|
|
372
|
+
const first = missingAttachments.slice(0, 3);
|
|
373
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
374
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
375
|
+
if (missingAttachments.length > 3)
|
|
376
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
377
|
+
}
|
|
378
|
+
this.attachmentIdToPath = attachmentIdToPath;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
348
381
|
var t = TypedHTTP2.Router.create();
|
|
349
382
|
var localReportRouter = {
|
|
350
383
|
ping: t.get({
|
|
@@ -354,7 +387,7 @@ var localReportRouter = {
|
|
|
354
387
|
}),
|
|
355
388
|
lastCommits: t.get({
|
|
356
389
|
handler: async ({ ctx }) => {
|
|
357
|
-
return ctx.commits;
|
|
390
|
+
return ctx.reportInfo.commits;
|
|
358
391
|
}
|
|
359
392
|
}),
|
|
360
393
|
report: {
|
|
@@ -363,7 +396,7 @@ var localReportRouter = {
|
|
|
363
396
|
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
364
397
|
}),
|
|
365
398
|
handler: async ({ ctx, input }) => {
|
|
366
|
-
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
399
|
+
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
367
400
|
if (!idx)
|
|
368
401
|
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
369
402
|
const buffer = await fs4.promises.readFile(idx.path);
|
|
@@ -372,7 +405,8 @@ var localReportRouter = {
|
|
|
372
405
|
}),
|
|
373
406
|
json: t.get({
|
|
374
407
|
handler: async ({ ctx }) => {
|
|
375
|
-
|
|
408
|
+
await ctx.reportInfo.refresh();
|
|
409
|
+
return ctx.reportInfo.report;
|
|
376
410
|
}
|
|
377
411
|
})
|
|
378
412
|
}
|
|
@@ -387,17 +421,6 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
387
421
|
this._authToken = _authToken;
|
|
388
422
|
}
|
|
389
423
|
static async create(options) {
|
|
390
|
-
const report = JSON.parse(await fs5.promises.readFile(options.reportPath, "utf-8"));
|
|
391
|
-
const attachmentsDir = options.attachmentsFolder;
|
|
392
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
393
|
-
if (missingAttachments.length) {
|
|
394
|
-
const first = missingAttachments.slice(0, 3);
|
|
395
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
396
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
397
|
-
if (missingAttachments.length > 3)
|
|
398
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
399
|
-
}
|
|
400
|
-
const commits = await listLocalCommits(process.cwd(), report.commitId, 100);
|
|
401
424
|
const app = express();
|
|
402
425
|
app.set("etag", false);
|
|
403
426
|
const authToken = randomUUIDBase62();
|
|
@@ -420,9 +443,10 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
420
443
|
});
|
|
421
444
|
next();
|
|
422
445
|
});
|
|
446
|
+
const reportInfo = new ReportInfo(options);
|
|
423
447
|
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
424
448
|
router: localReportRouter,
|
|
425
|
-
createRootContext: async ({ req, res, input }) => ({
|
|
449
|
+
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
426
450
|
}));
|
|
427
451
|
app.use((err, req, res, next) => {
|
|
428
452
|
if (err instanceof TypedHTTP3.HttpError)
|
|
@@ -456,7 +480,7 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
456
480
|
|
|
457
481
|
// src/cli/cmd-show-report.ts
|
|
458
482
|
async function cmdShowReport(reportFolder) {
|
|
459
|
-
const reportPath =
|
|
483
|
+
const reportPath = path5.join(reportFolder, "report.json");
|
|
460
484
|
const session = await FlakinessSession.load();
|
|
461
485
|
const config = await FlakinessConfig.load();
|
|
462
486
|
const projectPublicId = config.projectPublicId();
|
package/lib/junit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/junit.ts
|
|
2
|
-
import { ReportUtils as ReportUtils2
|
|
2
|
+
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
3
3
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
4
4
|
import assert from "assert";
|
|
5
5
|
import fs2 from "fs";
|
|
@@ -191,7 +191,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
191
191
|
file,
|
|
192
192
|
line,
|
|
193
193
|
column: 1
|
|
194
|
-
} :
|
|
194
|
+
} : void 0,
|
|
195
195
|
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
196
196
|
suites: [],
|
|
197
197
|
tests: []
|
|
@@ -253,7 +253,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
253
253
|
file,
|
|
254
254
|
line,
|
|
255
255
|
column: 1
|
|
256
|
-
} :
|
|
256
|
+
} : void 0,
|
|
257
257
|
attempts: [{
|
|
258
258
|
environmentIdx: currentEnvIndex,
|
|
259
259
|
expectedStatus,
|
package/lib/localReportApi.js
CHANGED
|
@@ -1,7 +1,207 @@
|
|
|
1
1
|
// src/localReportApi.ts
|
|
2
2
|
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
3
|
-
import
|
|
3
|
+
import fs2 from "fs";
|
|
4
|
+
import path2 from "path";
|
|
4
5
|
import { z } from "zod/v4";
|
|
6
|
+
|
|
7
|
+
// src/localGit.ts
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
var execAsync = promisify(exec);
|
|
11
|
+
async function listLocalCommits(gitRoot, head, count) {
|
|
12
|
+
const FIELD_SEPARATOR = "|~|";
|
|
13
|
+
const RECORD_SEPARATOR = "\0";
|
|
14
|
+
const prettyFormat = [
|
|
15
|
+
"%H",
|
|
16
|
+
// %H: Full commit hash
|
|
17
|
+
"%at",
|
|
18
|
+
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
19
|
+
"%an",
|
|
20
|
+
// %an: Author name
|
|
21
|
+
"%s"
|
|
22
|
+
// %s: Subject (the first line of the commit message)
|
|
23
|
+
].join(FIELD_SEPARATOR);
|
|
24
|
+
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
25
|
+
try {
|
|
26
|
+
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
27
|
+
if (!stdout) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
31
|
+
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
32
|
+
return {
|
|
33
|
+
commitId,
|
|
34
|
+
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
35
|
+
// Convert timestamp from seconds to milliseconds
|
|
36
|
+
author,
|
|
37
|
+
message,
|
|
38
|
+
walkIndex: 0
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/utils.ts
|
|
48
|
+
import { ReportUtils } from "@flakiness/report";
|
|
49
|
+
import fs from "fs";
|
|
50
|
+
import http from "http";
|
|
51
|
+
import https from "https";
|
|
52
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
53
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
54
|
+
function errorText(error) {
|
|
55
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
56
|
+
}
|
|
57
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
58
|
+
for (const timeout of backoff) {
|
|
59
|
+
try {
|
|
60
|
+
return await job();
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (e instanceof AggregateError)
|
|
63
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
64
|
+
else if (e instanceof Error)
|
|
65
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
66
|
+
else
|
|
67
|
+
console.error(`[flakiness.io err]`, e);
|
|
68
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return await job();
|
|
72
|
+
}
|
|
73
|
+
var httpUtils;
|
|
74
|
+
((httpUtils2) => {
|
|
75
|
+
function createRequest({ url, method = "get", headers = {} }) {
|
|
76
|
+
let resolve;
|
|
77
|
+
let reject;
|
|
78
|
+
const responseDataPromise = new Promise((a, b) => {
|
|
79
|
+
resolve = a;
|
|
80
|
+
reject = b;
|
|
81
|
+
});
|
|
82
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
83
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
84
|
+
const request = protocol.request(url, { method, headers }, (res) => {
|
|
85
|
+
const chunks = [];
|
|
86
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
87
|
+
res.on("end", () => {
|
|
88
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
89
|
+
resolve(Buffer.concat(chunks));
|
|
90
|
+
else
|
|
91
|
+
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
92
|
+
});
|
|
93
|
+
res.on("error", (error) => reject(error));
|
|
94
|
+
});
|
|
95
|
+
request.on("error", reject);
|
|
96
|
+
return { request, responseDataPromise };
|
|
97
|
+
}
|
|
98
|
+
httpUtils2.createRequest = createRequest;
|
|
99
|
+
async function getBuffer(url, backoff) {
|
|
100
|
+
return await retryWithBackoff(async () => {
|
|
101
|
+
const { request, responseDataPromise } = createRequest({ url });
|
|
102
|
+
request.end();
|
|
103
|
+
return await responseDataPromise;
|
|
104
|
+
}, backoff);
|
|
105
|
+
}
|
|
106
|
+
httpUtils2.getBuffer = getBuffer;
|
|
107
|
+
async function getText(url, backoff) {
|
|
108
|
+
const buffer = await getBuffer(url, backoff);
|
|
109
|
+
return buffer.toString("utf-8");
|
|
110
|
+
}
|
|
111
|
+
httpUtils2.getText = getText;
|
|
112
|
+
async function getJSON(url) {
|
|
113
|
+
return JSON.parse(await getText(url));
|
|
114
|
+
}
|
|
115
|
+
httpUtils2.getJSON = getJSON;
|
|
116
|
+
async function postText(url, text, backoff) {
|
|
117
|
+
const headers = {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"Content-Length": Buffer.byteLength(text) + ""
|
|
120
|
+
};
|
|
121
|
+
return await retryWithBackoff(async () => {
|
|
122
|
+
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
123
|
+
request.write(text);
|
|
124
|
+
request.end();
|
|
125
|
+
return await responseDataPromise;
|
|
126
|
+
}, backoff);
|
|
127
|
+
}
|
|
128
|
+
httpUtils2.postText = postText;
|
|
129
|
+
async function postJSON(url, json, backoff) {
|
|
130
|
+
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
131
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
132
|
+
}
|
|
133
|
+
httpUtils2.postJSON = postJSON;
|
|
134
|
+
})(httpUtils || (httpUtils = {}));
|
|
135
|
+
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");
|
|
136
|
+
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
137
|
+
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
138
|
+
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
139
|
+
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
140
|
+
const missingAttachments = /* @__PURE__ */ new Set();
|
|
141
|
+
ReportUtils.visitTests(report, (test) => {
|
|
142
|
+
for (const attempt of test.attempts) {
|
|
143
|
+
for (const attachment of attempt.attachments ?? []) {
|
|
144
|
+
const attachmentPath = filenameToPath.get(attachment.id);
|
|
145
|
+
if (!attachmentPath) {
|
|
146
|
+
missingAttachments.add(attachment.id);
|
|
147
|
+
} else {
|
|
148
|
+
attachmentIdToPath.set(attachment.id, {
|
|
149
|
+
contentType: attachment.contentType,
|
|
150
|
+
id: attachment.id,
|
|
151
|
+
path: attachmentPath
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
158
|
+
}
|
|
159
|
+
async function listFilesRecursively(dir, result = []) {
|
|
160
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const fullPath = path.join(dir, entry.name);
|
|
163
|
+
if (entry.isDirectory())
|
|
164
|
+
await listFilesRecursively(fullPath, result);
|
|
165
|
+
else
|
|
166
|
+
result.push(fullPath);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
171
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
172
|
+
|
|
173
|
+
// src/localReportApi.ts
|
|
174
|
+
var ReportInfo = class {
|
|
175
|
+
constructor(_options) {
|
|
176
|
+
this._options = _options;
|
|
177
|
+
}
|
|
178
|
+
report;
|
|
179
|
+
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
180
|
+
commits = [];
|
|
181
|
+
async refresh() {
|
|
182
|
+
const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
183
|
+
if (!report) {
|
|
184
|
+
this.report = void 0;
|
|
185
|
+
this.commits = [];
|
|
186
|
+
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
190
|
+
return;
|
|
191
|
+
this.report = report;
|
|
192
|
+
this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
|
|
193
|
+
const attachmentsDir = this._options.attachmentsFolder;
|
|
194
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
195
|
+
if (missingAttachments.length) {
|
|
196
|
+
const first = missingAttachments.slice(0, 3);
|
|
197
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
198
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
199
|
+
if (missingAttachments.length > 3)
|
|
200
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
201
|
+
}
|
|
202
|
+
this.attachmentIdToPath = attachmentIdToPath;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
5
205
|
var t = TypedHTTP.Router.create();
|
|
6
206
|
var localReportRouter = {
|
|
7
207
|
ping: t.get({
|
|
@@ -11,7 +211,7 @@ var localReportRouter = {
|
|
|
11
211
|
}),
|
|
12
212
|
lastCommits: t.get({
|
|
13
213
|
handler: async ({ ctx }) => {
|
|
14
|
-
return ctx.commits;
|
|
214
|
+
return ctx.reportInfo.commits;
|
|
15
215
|
}
|
|
16
216
|
}),
|
|
17
217
|
report: {
|
|
@@ -20,21 +220,23 @@ var localReportRouter = {
|
|
|
20
220
|
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
21
221
|
}),
|
|
22
222
|
handler: async ({ ctx, input }) => {
|
|
23
|
-
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
223
|
+
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
24
224
|
if (!idx)
|
|
25
225
|
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
26
|
-
const buffer = await
|
|
226
|
+
const buffer = await fs2.promises.readFile(idx.path);
|
|
27
227
|
return TypedHTTP.ok(buffer, idx.contentType);
|
|
28
228
|
}
|
|
29
229
|
}),
|
|
30
230
|
json: t.get({
|
|
31
231
|
handler: async ({ ctx }) => {
|
|
32
|
-
|
|
232
|
+
await ctx.reportInfo.refresh();
|
|
233
|
+
return ctx.reportInfo.report;
|
|
33
234
|
}
|
|
34
235
|
})
|
|
35
236
|
}
|
|
36
237
|
};
|
|
37
238
|
export {
|
|
239
|
+
ReportInfo,
|
|
38
240
|
localReportRouter
|
|
39
241
|
};
|
|
40
242
|
//# sourceMappingURL=localReportApi.js.map
|