@flakiness/sdk 0.147.0 → 0.148.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/flakinessProjectConfig.js +8 -2
- package/lib/index.js +392 -467
- package/lib/showReport.js +134 -334
- package/lib/staticServer.js +149 -0
- package/package.json +3 -12
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/localReportApi.js +0 -275
- package/lib/localReportServer.js +0 -351
package/lib/index.js
CHANGED
|
@@ -5,65 +5,14 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
import chalk from "chalk";
|
|
9
8
|
import fs6 from "fs";
|
|
10
|
-
import open from "open";
|
|
11
9
|
import path3 from "path";
|
|
10
|
+
import { FlakinessReport } from "@flakiness/flakiness-report";
|
|
12
11
|
|
|
13
|
-
// src/
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
import path from "path";
|
|
16
|
-
|
|
17
|
-
// src/git.ts
|
|
18
|
-
import assert from "assert";
|
|
19
|
-
|
|
20
|
-
// src/pathutils.ts
|
|
21
|
-
var pathutils_exports = {};
|
|
22
|
-
__export(pathutils_exports, {
|
|
23
|
-
gitFilePath: () => gitFilePath,
|
|
24
|
-
normalizePath: () => normalizePath
|
|
25
|
-
});
|
|
26
|
-
import { posix as posixPath, win32 as win32Path } from "path";
|
|
27
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
28
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
29
|
-
function normalizePath(aPath) {
|
|
30
|
-
if (IS_WIN32_PATH.test(aPath)) {
|
|
31
|
-
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
32
|
-
}
|
|
33
|
-
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
34
|
-
return "/" + aPath[0] + aPath.substring(2);
|
|
35
|
-
return aPath;
|
|
36
|
-
}
|
|
37
|
-
function gitFilePath(gitRoot, absolutePath) {
|
|
38
|
-
return posixPath.relative(gitRoot, absolutePath);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/utils.ts
|
|
12
|
+
// src/createEnvironment.ts
|
|
42
13
|
import { spawnSync } from "child_process";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
46
|
-
}
|
|
47
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
48
|
-
for (const timeout of backoff) {
|
|
49
|
-
try {
|
|
50
|
-
return await job();
|
|
51
|
-
} catch (e) {
|
|
52
|
-
if (e instanceof AggregateError)
|
|
53
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
54
|
-
else if (e instanceof Error)
|
|
55
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
56
|
-
else
|
|
57
|
-
console.error(`[flakiness.io err]`, e);
|
|
58
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return await job();
|
|
62
|
-
}
|
|
63
|
-
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");
|
|
64
|
-
function stripAnsi(str) {
|
|
65
|
-
return str.replace(ansiRegex, "");
|
|
66
|
-
}
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import os from "os";
|
|
67
16
|
function shell(command, args, options) {
|
|
68
17
|
try {
|
|
69
18
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -76,141 +25,69 @@ function shell(command, args, options) {
|
|
|
76
25
|
return void 0;
|
|
77
26
|
}
|
|
78
27
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
28
|
+
function readLinuxOSRelease() {
|
|
29
|
+
const osReleaseText = fs.readFileSync("/etc/os-release", "utf-8");
|
|
30
|
+
return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
31
|
+
line = line.trim();
|
|
32
|
+
let [key, value] = line.split("=");
|
|
33
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
34
|
+
value = value.substring(1, value.length - 1);
|
|
35
|
+
return [key, value];
|
|
36
|
+
}));
|
|
88
37
|
}
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return normalizePath(root);
|
|
38
|
+
function osLinuxInfo() {
|
|
39
|
+
const arch = shell(`uname`, [`-m`]);
|
|
40
|
+
const osReleaseMap = readLinuxOSRelease();
|
|
41
|
+
const name = osReleaseMap.get("name") ?? shell(`uname`);
|
|
42
|
+
const version = osReleaseMap.get("version_id");
|
|
43
|
+
return { name, arch, version };
|
|
96
44
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
45
|
+
function osDarwinInfo() {
|
|
46
|
+
const name = "macos";
|
|
47
|
+
const arch = shell(`uname`, [`-m`]);
|
|
48
|
+
const version = shell(`sw_vers`, [`-productVersion`]);
|
|
49
|
+
return { name, arch, version };
|
|
101
50
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
51
|
+
function osWinInfo() {
|
|
52
|
+
const name = "win";
|
|
53
|
+
const arch = process.arch;
|
|
54
|
+
const version = os.release();
|
|
55
|
+
return { name, arch, version };
|
|
107
56
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
57
|
+
function getOSInfo() {
|
|
58
|
+
if (process.platform === "darwin")
|
|
59
|
+
return osDarwinInfo();
|
|
60
|
+
if (process.platform === "win32")
|
|
61
|
+
return osWinInfo();
|
|
62
|
+
return osLinuxInfo();
|
|
63
|
+
}
|
|
64
|
+
function extractEnvConfiguration() {
|
|
65
|
+
const ENV_PREFIX = "FK_ENV_";
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function createEnvironment(options) {
|
|
71
|
+
const osInfo = getOSInfo();
|
|
72
|
+
return {
|
|
73
|
+
name: options.name,
|
|
74
|
+
systemData: {
|
|
75
|
+
osArch: osInfo.arch,
|
|
76
|
+
osName: osInfo.name,
|
|
77
|
+
osVersion: osInfo.version
|
|
78
|
+
},
|
|
79
|
+
userSuppliedData: {
|
|
80
|
+
...extractEnvConfiguration(),
|
|
81
|
+
...options.userSuppliedData ?? {}
|
|
82
|
+
},
|
|
83
|
+
opaqueData: options.opaqueData
|
|
84
|
+
};
|
|
120
85
|
}
|
|
121
|
-
var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
122
|
-
constructor(_configPath, _config) {
|
|
123
|
-
this._configPath = _configPath;
|
|
124
|
-
this._config = _config;
|
|
125
|
-
}
|
|
126
|
-
static async load() {
|
|
127
|
-
const configPath = ensureConfigPath();
|
|
128
|
-
const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
129
|
-
const json = data ? JSON.parse(data) : {};
|
|
130
|
-
return new _FlakinessProjectConfig(configPath, json);
|
|
131
|
-
}
|
|
132
|
-
static createEmpty() {
|
|
133
|
-
return new _FlakinessProjectConfig(ensureConfigPath(), {});
|
|
134
|
-
}
|
|
135
|
-
path() {
|
|
136
|
-
return this._configPath;
|
|
137
|
-
}
|
|
138
|
-
projectPublicId() {
|
|
139
|
-
return this._config.projectPublicId;
|
|
140
|
-
}
|
|
141
|
-
reportViewerEndpoint() {
|
|
142
|
-
return this._config.customReportViewerEndpoint ?? "https://report.flakiness.io";
|
|
143
|
-
}
|
|
144
|
-
setProjectPublicId(projectId) {
|
|
145
|
-
this._config.projectPublicId = projectId;
|
|
146
|
-
}
|
|
147
|
-
async save() {
|
|
148
|
-
await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
|
|
149
|
-
await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// src/localReportServer.ts
|
|
154
|
-
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
155
|
-
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
156
|
-
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
157
|
-
import bodyParser from "body-parser";
|
|
158
|
-
import compression from "compression";
|
|
159
|
-
import debug2 from "debug";
|
|
160
|
-
import express from "express";
|
|
161
|
-
import "express-async-errors";
|
|
162
|
-
import http from "http";
|
|
163
86
|
|
|
164
|
-
// src/
|
|
165
|
-
import {
|
|
87
|
+
// src/createTestStepSnippets.ts
|
|
88
|
+
import { codeFrameColumns } from "@babel/code-frame";
|
|
166
89
|
import fs2 from "fs";
|
|
167
|
-
import
|
|
168
|
-
import { z } from "zod/v4";
|
|
169
|
-
|
|
170
|
-
// src/localGit.ts
|
|
171
|
-
import { exec } from "child_process";
|
|
172
|
-
import debug from "debug";
|
|
173
|
-
import { promisify } from "util";
|
|
174
|
-
var log = debug("fk:git");
|
|
175
|
-
var execAsync = promisify(exec);
|
|
176
|
-
async function listLocalCommits(gitRoot, head, count) {
|
|
177
|
-
const FIELD_SEPARATOR = "|~|";
|
|
178
|
-
const RECORD_SEPARATOR = "\0";
|
|
179
|
-
const prettyFormat = [
|
|
180
|
-
"%H",
|
|
181
|
-
// %H: Full commit hash
|
|
182
|
-
"%at",
|
|
183
|
-
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
184
|
-
"%an",
|
|
185
|
-
// %an: Author name
|
|
186
|
-
"%s",
|
|
187
|
-
// %s: Subject (the first line of the commit message)
|
|
188
|
-
"%P"
|
|
189
|
-
// %P: Parent hashes (space-separated)
|
|
190
|
-
].join(FIELD_SEPARATOR);
|
|
191
|
-
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
192
|
-
try {
|
|
193
|
-
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
194
|
-
if (!stdout) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
198
|
-
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
199
|
-
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
200
|
-
return {
|
|
201
|
-
commitId,
|
|
202
|
-
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
203
|
-
author,
|
|
204
|
-
message,
|
|
205
|
-
parents,
|
|
206
|
-
walkIndex: 0
|
|
207
|
-
};
|
|
208
|
-
});
|
|
209
|
-
} catch (error) {
|
|
210
|
-
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
90
|
+
import { posix as posixPath } from "path";
|
|
214
91
|
|
|
215
92
|
// src/reportUtils.ts
|
|
216
93
|
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
@@ -279,7 +156,7 @@ var ReportUtils;
|
|
|
279
156
|
location: tests2[0].location,
|
|
280
157
|
title: tests2[0].title,
|
|
281
158
|
tags: tags.length ? tags : void 0,
|
|
282
|
-
attempts: tests2.map((
|
|
159
|
+
attempts: tests2.map((t) => t.attempts).flat().map((attempt) => ({
|
|
283
160
|
...attempt,
|
|
284
161
|
environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
|
|
285
162
|
}))
|
|
@@ -332,254 +209,7 @@ var ReportUtils;
|
|
|
332
209
|
}
|
|
333
210
|
})(ReportUtils || (ReportUtils = {}));
|
|
334
211
|
|
|
335
|
-
// src/localReportApi.ts
|
|
336
|
-
var ReportInfo = class {
|
|
337
|
-
constructor(_options) {
|
|
338
|
-
this._options = _options;
|
|
339
|
-
}
|
|
340
|
-
report;
|
|
341
|
-
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
342
|
-
commits = [];
|
|
343
|
-
async refresh() {
|
|
344
|
-
const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
345
|
-
if (!report) {
|
|
346
|
-
this.report = void 0;
|
|
347
|
-
this.commits = [];
|
|
348
|
-
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
352
|
-
return;
|
|
353
|
-
this.report = report;
|
|
354
|
-
this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
|
|
355
|
-
const attachmentsDir = this._options.attachmentsFolder;
|
|
356
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
357
|
-
if (missingAttachments.length) {
|
|
358
|
-
const first = missingAttachments.slice(0, 3);
|
|
359
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
360
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
361
|
-
if (missingAttachments.length > 3)
|
|
362
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
363
|
-
}
|
|
364
|
-
this.attachmentIdToPath = attachmentIdToPath;
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
var t = TypedHTTP.Router.create();
|
|
368
|
-
var localReportRouter = {
|
|
369
|
-
ping: t.get({
|
|
370
|
-
handler: async () => {
|
|
371
|
-
return "pong";
|
|
372
|
-
}
|
|
373
|
-
}),
|
|
374
|
-
lastCommits: t.get({
|
|
375
|
-
handler: async ({ ctx }) => {
|
|
376
|
-
return ctx.reportInfo.commits;
|
|
377
|
-
}
|
|
378
|
-
}),
|
|
379
|
-
report: {
|
|
380
|
-
attachment: t.rawMethod("GET", {
|
|
381
|
-
input: z.object({
|
|
382
|
-
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
383
|
-
}),
|
|
384
|
-
handler: async ({ ctx, input }) => {
|
|
385
|
-
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
386
|
-
if (!idx)
|
|
387
|
-
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
388
|
-
const buffer = await fs2.promises.readFile(idx.path);
|
|
389
|
-
return TypedHTTP.ok(buffer, idx.contentType);
|
|
390
|
-
}
|
|
391
|
-
}),
|
|
392
|
-
json: t.get({
|
|
393
|
-
handler: async ({ ctx }) => {
|
|
394
|
-
await ctx.reportInfo.refresh();
|
|
395
|
-
return ctx.reportInfo.report;
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
401
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
402
|
-
const filenameToPath = new Map(attachmentFiles.map((file) => [path2.basename(file), file]));
|
|
403
|
-
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
404
|
-
const missingAttachments = /* @__PURE__ */ new Set();
|
|
405
|
-
ReportUtils.visitTests(report, (test) => {
|
|
406
|
-
for (const attempt of test.attempts) {
|
|
407
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
408
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
409
|
-
if (!attachmentPath) {
|
|
410
|
-
missingAttachments.add(attachment.id);
|
|
411
|
-
} else {
|
|
412
|
-
attachmentIdToPath.set(attachment.id, {
|
|
413
|
-
contentType: attachment.contentType,
|
|
414
|
-
id: attachment.id,
|
|
415
|
-
path: attachmentPath
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
422
|
-
}
|
|
423
|
-
async function listFilesRecursively(dir, result = []) {
|
|
424
|
-
const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
425
|
-
for (const entry of entries) {
|
|
426
|
-
const fullPath = path2.join(dir, entry.name);
|
|
427
|
-
if (entry.isDirectory())
|
|
428
|
-
await listFilesRecursively(fullPath, result);
|
|
429
|
-
else
|
|
430
|
-
result.push(fullPath);
|
|
431
|
-
}
|
|
432
|
-
return result;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/localReportServer.ts
|
|
436
|
-
var logHTTPServer = debug2("fk:http");
|
|
437
|
-
var LocalReportServer = class _LocalReportServer {
|
|
438
|
-
constructor(_server, _port, _authToken) {
|
|
439
|
-
this._server = _server;
|
|
440
|
-
this._port = _port;
|
|
441
|
-
this._authToken = _authToken;
|
|
442
|
-
}
|
|
443
|
-
static async create(options) {
|
|
444
|
-
const app = express();
|
|
445
|
-
app.set("etag", false);
|
|
446
|
-
const authToken = randomUUIDBase62();
|
|
447
|
-
app.use(compression());
|
|
448
|
-
app.use(bodyParser.json({ limit: 256 * 1024 }));
|
|
449
|
-
app.use((req, res, next) => {
|
|
450
|
-
if (!req.path.startsWith("/" + authToken))
|
|
451
|
-
throw TypedHTTP2.HttpError.withCode("UNAUTHORIZED");
|
|
452
|
-
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
453
|
-
res.setHeader("Access-Control-Allow-Origin", options.endpoint);
|
|
454
|
-
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
455
|
-
if (req.method === "OPTIONS") {
|
|
456
|
-
res.writeHead(204);
|
|
457
|
-
res.end();
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
|
|
461
|
-
res.on("close", () => {
|
|
462
|
-
if (!res.headersSent) logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
|
|
463
|
-
});
|
|
464
|
-
next();
|
|
465
|
-
});
|
|
466
|
-
const reportInfo = new ReportInfo(options);
|
|
467
|
-
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
468
|
-
router: localReportRouter,
|
|
469
|
-
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
470
|
-
}));
|
|
471
|
-
app.use((err, req, res, next) => {
|
|
472
|
-
if (err instanceof TypedHTTP2.HttpError)
|
|
473
|
-
return res.status(err.status).send({ error: err.message });
|
|
474
|
-
logHTTPServer(err);
|
|
475
|
-
res.status(500).send({ error: "Internal Server Error" });
|
|
476
|
-
});
|
|
477
|
-
const server = http.createServer(app);
|
|
478
|
-
server.on("error", (err) => {
|
|
479
|
-
if (err.code === "ECONNRESET") {
|
|
480
|
-
logHTTPServer("Client connection reset. Ignoring.");
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
throw err;
|
|
484
|
-
});
|
|
485
|
-
const port = await new Promise((resolve) => server.listen(options.port, () => {
|
|
486
|
-
resolve(server.address().port);
|
|
487
|
-
}));
|
|
488
|
-
return new _LocalReportServer(server, port, authToken);
|
|
489
|
-
}
|
|
490
|
-
authToken() {
|
|
491
|
-
return this._authToken;
|
|
492
|
-
}
|
|
493
|
-
port() {
|
|
494
|
-
return this._port;
|
|
495
|
-
}
|
|
496
|
-
async dispose() {
|
|
497
|
-
await new Promise((x) => this._server.close(x));
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// src/index.ts
|
|
502
|
-
import { FlakinessReport } from "@flakiness/flakiness-report";
|
|
503
|
-
|
|
504
|
-
// src/createEnvironment.ts
|
|
505
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
506
|
-
import fs3 from "fs";
|
|
507
|
-
import os from "os";
|
|
508
|
-
function shell2(command, args, options) {
|
|
509
|
-
try {
|
|
510
|
-
const result = spawnSync2(command, args, { encoding: "utf-8", ...options });
|
|
511
|
-
if (result.status !== 0) {
|
|
512
|
-
return void 0;
|
|
513
|
-
}
|
|
514
|
-
return result.stdout.trim();
|
|
515
|
-
} catch (e) {
|
|
516
|
-
console.error(e);
|
|
517
|
-
return void 0;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
function readLinuxOSRelease() {
|
|
521
|
-
const osReleaseText = fs3.readFileSync("/etc/os-release", "utf-8");
|
|
522
|
-
return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
523
|
-
line = line.trim();
|
|
524
|
-
let [key, value] = line.split("=");
|
|
525
|
-
if (value.startsWith('"') && value.endsWith('"'))
|
|
526
|
-
value = value.substring(1, value.length - 1);
|
|
527
|
-
return [key, value];
|
|
528
|
-
}));
|
|
529
|
-
}
|
|
530
|
-
function osLinuxInfo() {
|
|
531
|
-
const arch = shell2(`uname`, [`-m`]);
|
|
532
|
-
const osReleaseMap = readLinuxOSRelease();
|
|
533
|
-
const name = osReleaseMap.get("name") ?? shell2(`uname`);
|
|
534
|
-
const version = osReleaseMap.get("version_id");
|
|
535
|
-
return { name, arch, version };
|
|
536
|
-
}
|
|
537
|
-
function osDarwinInfo() {
|
|
538
|
-
const name = "macos";
|
|
539
|
-
const arch = shell2(`uname`, [`-m`]);
|
|
540
|
-
const version = shell2(`sw_vers`, [`-productVersion`]);
|
|
541
|
-
return { name, arch, version };
|
|
542
|
-
}
|
|
543
|
-
function osWinInfo() {
|
|
544
|
-
const name = "win";
|
|
545
|
-
const arch = process.arch;
|
|
546
|
-
const version = os.release();
|
|
547
|
-
return { name, arch, version };
|
|
548
|
-
}
|
|
549
|
-
function getOSInfo() {
|
|
550
|
-
if (process.platform === "darwin")
|
|
551
|
-
return osDarwinInfo();
|
|
552
|
-
if (process.platform === "win32")
|
|
553
|
-
return osWinInfo();
|
|
554
|
-
return osLinuxInfo();
|
|
555
|
-
}
|
|
556
|
-
function extractEnvConfiguration() {
|
|
557
|
-
const ENV_PREFIX = "FK_ENV_";
|
|
558
|
-
return Object.fromEntries(
|
|
559
|
-
Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
function createEnvironment(options) {
|
|
563
|
-
const osInfo = getOSInfo();
|
|
564
|
-
return {
|
|
565
|
-
name: options.name,
|
|
566
|
-
systemData: {
|
|
567
|
-
osArch: osInfo.arch,
|
|
568
|
-
osName: osInfo.name,
|
|
569
|
-
osVersion: osInfo.version
|
|
570
|
-
},
|
|
571
|
-
userSuppliedData: {
|
|
572
|
-
...extractEnvConfiguration(),
|
|
573
|
-
...options.userSuppliedData ?? {}
|
|
574
|
-
},
|
|
575
|
-
opaqueData: options.opaqueData
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
212
|
// src/createTestStepSnippets.ts
|
|
580
|
-
import { codeFrameColumns } from "@babel/code-frame";
|
|
581
|
-
import fs4 from "fs";
|
|
582
|
-
import { posix as posixPath2 } from "path";
|
|
583
213
|
function createTestStepSnippetsInplace(report, gitRoot) {
|
|
584
214
|
const allSteps = /* @__PURE__ */ new Map();
|
|
585
215
|
ReportUtils.visitTests(report, (test) => {
|
|
@@ -599,7 +229,7 @@ function createTestStepSnippetsInplace(report, gitRoot) {
|
|
|
599
229
|
for (const [gitFilePath2, steps] of allSteps) {
|
|
600
230
|
let source;
|
|
601
231
|
try {
|
|
602
|
-
source =
|
|
232
|
+
source = fs2.readFileSync(posixPath.join(gitRoot, gitFilePath2), "utf-8");
|
|
603
233
|
} catch (e) {
|
|
604
234
|
continue;
|
|
605
235
|
}
|
|
@@ -621,34 +251,180 @@ function createTestStepSnippetsInplace(report, gitRoot) {
|
|
|
621
251
|
}
|
|
622
252
|
}
|
|
623
253
|
|
|
254
|
+
// src/flakinessProjectConfig.ts
|
|
255
|
+
import fs3 from "fs";
|
|
256
|
+
import path from "path";
|
|
257
|
+
|
|
258
|
+
// src/git.ts
|
|
259
|
+
import assert from "assert";
|
|
260
|
+
|
|
261
|
+
// src/pathutils.ts
|
|
262
|
+
var pathutils_exports = {};
|
|
263
|
+
__export(pathutils_exports, {
|
|
264
|
+
gitFilePath: () => gitFilePath,
|
|
265
|
+
normalizePath: () => normalizePath
|
|
266
|
+
});
|
|
267
|
+
import { posix as posixPath2, win32 as win32Path } from "path";
|
|
268
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
269
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
270
|
+
function normalizePath(aPath) {
|
|
271
|
+
if (IS_WIN32_PATH.test(aPath)) {
|
|
272
|
+
aPath = aPath.split(win32Path.sep).join(posixPath2.sep);
|
|
273
|
+
}
|
|
274
|
+
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
275
|
+
return "/" + aPath[0] + aPath.substring(2);
|
|
276
|
+
return aPath;
|
|
277
|
+
}
|
|
278
|
+
function gitFilePath(gitRoot, absolutePath) {
|
|
279
|
+
return posixPath2.relative(gitRoot, absolutePath);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/utils.ts
|
|
283
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
284
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
285
|
+
function errorText(error) {
|
|
286
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
287
|
+
}
|
|
288
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
289
|
+
for (const timeout of backoff) {
|
|
290
|
+
try {
|
|
291
|
+
return await job();
|
|
292
|
+
} catch (e) {
|
|
293
|
+
if (e instanceof AggregateError)
|
|
294
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
295
|
+
else if (e instanceof Error)
|
|
296
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
297
|
+
else
|
|
298
|
+
console.error(`[flakiness.io err]`, e);
|
|
299
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return await job();
|
|
303
|
+
}
|
|
304
|
+
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");
|
|
305
|
+
function stripAnsi(str) {
|
|
306
|
+
return str.replace(ansiRegex, "");
|
|
307
|
+
}
|
|
308
|
+
function shell2(command, args, options) {
|
|
309
|
+
try {
|
|
310
|
+
const result = spawnSync2(command, args, { encoding: "utf-8", ...options });
|
|
311
|
+
if (result.status !== 0) {
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
return result.stdout.trim();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
console.error(e);
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/git.ts
|
|
322
|
+
function gitCommitInfo(gitRepo) {
|
|
323
|
+
const sha = shell2(`git`, ["rev-parse", "HEAD"], {
|
|
324
|
+
cwd: gitRepo,
|
|
325
|
+
encoding: "utf-8"
|
|
326
|
+
});
|
|
327
|
+
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
328
|
+
return sha.trim();
|
|
329
|
+
}
|
|
330
|
+
function computeGitRoot(somePathInsideGitRepo) {
|
|
331
|
+
const root = shell2(`git`, ["rev-parse", "--show-toplevel"], {
|
|
332
|
+
cwd: somePathInsideGitRepo,
|
|
333
|
+
encoding: "utf-8"
|
|
334
|
+
});
|
|
335
|
+
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
336
|
+
return normalizePath(root);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/flakinessProjectConfig.ts
|
|
340
|
+
function createConfigPath(dir) {
|
|
341
|
+
return path.join(dir, ".flakiness", "config.json");
|
|
342
|
+
}
|
|
343
|
+
var gConfigPath;
|
|
344
|
+
function ensureConfigPath() {
|
|
345
|
+
if (!gConfigPath)
|
|
346
|
+
gConfigPath = computeConfigPath();
|
|
347
|
+
return gConfigPath;
|
|
348
|
+
}
|
|
349
|
+
function computeConfigPath() {
|
|
350
|
+
for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
|
|
351
|
+
const configPath = createConfigPath(p);
|
|
352
|
+
if (fs3.existsSync(configPath))
|
|
353
|
+
return configPath;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const gitRoot = computeGitRoot(process.cwd());
|
|
357
|
+
return createConfigPath(gitRoot);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
return createConfigPath(process.cwd());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
363
|
+
constructor(_configPath, _config) {
|
|
364
|
+
this._configPath = _configPath;
|
|
365
|
+
this._config = _config;
|
|
366
|
+
}
|
|
367
|
+
static async load() {
|
|
368
|
+
const configPath = ensureConfigPath();
|
|
369
|
+
const data = await fs3.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
370
|
+
const json = data ? JSON.parse(data) : {};
|
|
371
|
+
return new _FlakinessProjectConfig(configPath, json);
|
|
372
|
+
}
|
|
373
|
+
static createEmpty() {
|
|
374
|
+
return new _FlakinessProjectConfig(ensureConfigPath(), {});
|
|
375
|
+
}
|
|
376
|
+
path() {
|
|
377
|
+
return this._configPath;
|
|
378
|
+
}
|
|
379
|
+
projectPublicId() {
|
|
380
|
+
return this._config.projectPublicId;
|
|
381
|
+
}
|
|
382
|
+
reportViewerUrl() {
|
|
383
|
+
return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
|
|
384
|
+
}
|
|
385
|
+
setCustomReportViewerUrl(url) {
|
|
386
|
+
if (url)
|
|
387
|
+
this._config.customReportViewerUrl = url;
|
|
388
|
+
else
|
|
389
|
+
delete this._config.customReportViewerUrl;
|
|
390
|
+
}
|
|
391
|
+
setProjectPublicId(projectId) {
|
|
392
|
+
this._config.projectPublicId = projectId;
|
|
393
|
+
}
|
|
394
|
+
async save() {
|
|
395
|
+
await fs3.promises.mkdir(path.dirname(this._configPath), { recursive: true });
|
|
396
|
+
await fs3.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
624
400
|
// src/reportUploader.ts
|
|
625
401
|
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
626
402
|
import assert2 from "assert";
|
|
627
403
|
import crypto from "crypto";
|
|
628
|
-
import
|
|
404
|
+
import fs4 from "fs";
|
|
629
405
|
import { URL as URL2 } from "url";
|
|
630
406
|
|
|
631
407
|
// src/httpUtils.ts
|
|
632
|
-
import
|
|
408
|
+
import http from "http";
|
|
633
409
|
import https from "https";
|
|
634
410
|
var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
|
|
635
411
|
var httpUtils;
|
|
636
412
|
((httpUtils2) => {
|
|
637
413
|
function createRequest({ url, method = "get", headers = {} }) {
|
|
638
|
-
let
|
|
414
|
+
let resolve2;
|
|
639
415
|
let reject;
|
|
640
416
|
const responseDataPromise = new Promise((a, b) => {
|
|
641
|
-
|
|
417
|
+
resolve2 = a;
|
|
642
418
|
reject = b;
|
|
643
419
|
});
|
|
644
|
-
const protocol = url.startsWith("https") ? https :
|
|
420
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
645
421
|
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
646
422
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
647
423
|
const chunks = [];
|
|
648
424
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
649
425
|
res.on("end", () => {
|
|
650
426
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
651
|
-
|
|
427
|
+
resolve2(Buffer.concat(chunks));
|
|
652
428
|
else
|
|
653
429
|
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
654
430
|
});
|
|
@@ -697,14 +473,14 @@ var httpUtils;
|
|
|
697
473
|
|
|
698
474
|
// src/reportUploader.ts
|
|
699
475
|
function sha1File(filePath) {
|
|
700
|
-
return new Promise((
|
|
476
|
+
return new Promise((resolve2, reject) => {
|
|
701
477
|
const hash = crypto.createHash("sha1");
|
|
702
|
-
const stream =
|
|
478
|
+
const stream = fs4.createReadStream(filePath);
|
|
703
479
|
stream.on("data", (chunk) => {
|
|
704
480
|
hash.update(chunk);
|
|
705
481
|
});
|
|
706
482
|
stream.on("end", () => {
|
|
707
|
-
|
|
483
|
+
resolve2(hash.digest("hex"));
|
|
708
484
|
});
|
|
709
485
|
stream.on("error", (err) => {
|
|
710
486
|
reject(err);
|
|
@@ -845,16 +621,16 @@ var ReportUpload = class {
|
|
|
845
621
|
url: uploadUrl,
|
|
846
622
|
headers: {
|
|
847
623
|
"Content-Type": attachment.contentType,
|
|
848
|
-
"Content-Length": (await
|
|
624
|
+
"Content-Length": (await fs4.promises.stat(attachmentPath)).size + ""
|
|
849
625
|
},
|
|
850
626
|
method: "put"
|
|
851
627
|
});
|
|
852
|
-
|
|
628
|
+
fs4.createReadStream(attachmentPath).pipe(request);
|
|
853
629
|
await responseDataPromise;
|
|
854
630
|
}, HTTP_BACKOFF);
|
|
855
631
|
return;
|
|
856
632
|
}
|
|
857
|
-
let buffer = attachment.body ? attachment.body : attachment.path ? await
|
|
633
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs4.promises.readFile(attachment.path) : void 0;
|
|
858
634
|
assert2(buffer);
|
|
859
635
|
const encoding = compressable ? "br" : void 0;
|
|
860
636
|
if (compressable)
|
|
@@ -877,6 +653,178 @@ var ReportUpload = class {
|
|
|
877
653
|
}
|
|
878
654
|
};
|
|
879
655
|
|
|
656
|
+
// src/showReport.ts
|
|
657
|
+
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
658
|
+
import chalk from "chalk";
|
|
659
|
+
import open from "open";
|
|
660
|
+
|
|
661
|
+
// src/staticServer.ts
|
|
662
|
+
import debug from "debug";
|
|
663
|
+
import * as fs5 from "fs";
|
|
664
|
+
import * as http2 from "http";
|
|
665
|
+
import * as path2 from "path";
|
|
666
|
+
var log = debug("fk:static_server");
|
|
667
|
+
var StaticServer = class {
|
|
668
|
+
_server;
|
|
669
|
+
_absoluteFolderPath;
|
|
670
|
+
_pathPrefix;
|
|
671
|
+
_cors;
|
|
672
|
+
_mimeTypes = {
|
|
673
|
+
".html": "text/html",
|
|
674
|
+
".js": "text/javascript",
|
|
675
|
+
".css": "text/css",
|
|
676
|
+
".json": "application/json",
|
|
677
|
+
".png": "image/png",
|
|
678
|
+
".jpg": "image/jpeg",
|
|
679
|
+
".gif": "image/gif",
|
|
680
|
+
".svg": "image/svg+xml",
|
|
681
|
+
".ico": "image/x-icon",
|
|
682
|
+
".txt": "text/plain"
|
|
683
|
+
};
|
|
684
|
+
constructor(pathPrefix, folderPath, cors) {
|
|
685
|
+
this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
|
|
686
|
+
this._absoluteFolderPath = path2.resolve(folderPath);
|
|
687
|
+
this._cors = cors;
|
|
688
|
+
this._server = http2.createServer((req, res) => this._handleRequest(req, res));
|
|
689
|
+
}
|
|
690
|
+
port() {
|
|
691
|
+
const address = this._server.address();
|
|
692
|
+
if (!address)
|
|
693
|
+
return void 0;
|
|
694
|
+
return address.port;
|
|
695
|
+
}
|
|
696
|
+
address() {
|
|
697
|
+
const address = this._server.address();
|
|
698
|
+
if (!address)
|
|
699
|
+
return void 0;
|
|
700
|
+
const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
701
|
+
return `http://${displayHost}:${address.port}${this._pathPrefix}`;
|
|
702
|
+
}
|
|
703
|
+
async _startServer(port, host) {
|
|
704
|
+
let okListener;
|
|
705
|
+
let errListener;
|
|
706
|
+
const result = new Promise((resolve2, reject) => {
|
|
707
|
+
okListener = resolve2;
|
|
708
|
+
errListener = reject;
|
|
709
|
+
}).finally(() => {
|
|
710
|
+
this._server.removeListener("listening", okListener);
|
|
711
|
+
this._server.removeListener("error", errListener);
|
|
712
|
+
});
|
|
713
|
+
this._server.once("listening", okListener);
|
|
714
|
+
this._server.once("error", errListener);
|
|
715
|
+
this._server.listen(port, host);
|
|
716
|
+
await result;
|
|
717
|
+
log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
|
|
718
|
+
}
|
|
719
|
+
async start(port, host = "127.0.0.1") {
|
|
720
|
+
if (port === 0) {
|
|
721
|
+
await this._startServer(port, host);
|
|
722
|
+
return this.address();
|
|
723
|
+
}
|
|
724
|
+
for (let i = 0; i < 20; ++i) {
|
|
725
|
+
const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
|
|
726
|
+
if (!err)
|
|
727
|
+
return this.address();
|
|
728
|
+
if (err.code !== "EADDRINUSE")
|
|
729
|
+
throw err;
|
|
730
|
+
log("Port %d is busy (EADDRINUSE). Trying next port...", port);
|
|
731
|
+
port = port + 1;
|
|
732
|
+
if (port > 65535)
|
|
733
|
+
port = 4e3;
|
|
734
|
+
}
|
|
735
|
+
log("All sequential ports busy. Falling back to random port.");
|
|
736
|
+
await this._startServer(0, host);
|
|
737
|
+
return this.address();
|
|
738
|
+
}
|
|
739
|
+
stop() {
|
|
740
|
+
return new Promise((resolve2, reject) => {
|
|
741
|
+
this._server.close((err) => {
|
|
742
|
+
if (err) {
|
|
743
|
+
log("Error stopping server: %o", err);
|
|
744
|
+
reject(err);
|
|
745
|
+
} else {
|
|
746
|
+
log("Server stopped.");
|
|
747
|
+
resolve2();
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
_errorResponse(req, res, code, text) {
|
|
753
|
+
res.writeHead(code, { "Content-Type": "text/plain" });
|
|
754
|
+
res.end(text);
|
|
755
|
+
log(`[${code}] ${req.method} ${req.url}`);
|
|
756
|
+
}
|
|
757
|
+
_handleRequest(req, res) {
|
|
758
|
+
const { url, method } = req;
|
|
759
|
+
if (this._cors) {
|
|
760
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
761
|
+
res.setHeader("Access-Control-Allow-Origin", this._cors);
|
|
762
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
763
|
+
if (req.method === "OPTIONS") {
|
|
764
|
+
res.writeHead(204);
|
|
765
|
+
res.end();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (method !== "GET") {
|
|
770
|
+
this._errorResponse(req, res, 405, "Method Not Allowed");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
|
|
774
|
+
res.on("close", () => {
|
|
775
|
+
if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
|
|
776
|
+
});
|
|
777
|
+
if (!url || !url.startsWith(this._pathPrefix)) {
|
|
778
|
+
this._errorResponse(req, res, 404, "Not Found");
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const relativePath = url.slice(this._pathPrefix.length);
|
|
782
|
+
const safeSuffix = path2.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
|
|
783
|
+
const filePath = path2.join(this._absoluteFolderPath, safeSuffix);
|
|
784
|
+
if (!filePath.startsWith(this._absoluteFolderPath)) {
|
|
785
|
+
this._errorResponse(req, res, 403, "Forbidden");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
fs5.stat(filePath, (err, stats) => {
|
|
789
|
+
if (err || !stats.isFile()) {
|
|
790
|
+
this._errorResponse(req, res, 404, "File Not Found");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
794
|
+
const contentType = this._mimeTypes[ext] || "application/octet-stream";
|
|
795
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
796
|
+
log(`[200] ${req.method} ${req.url} -> ${filePath}`);
|
|
797
|
+
const readStream = fs5.createReadStream(filePath);
|
|
798
|
+
readStream.pipe(res);
|
|
799
|
+
readStream.on("error", (err2) => {
|
|
800
|
+
log("Stream error: %o", err2);
|
|
801
|
+
res.end();
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/showReport.ts
|
|
808
|
+
async function showReport(reportFolder) {
|
|
809
|
+
const config = await FlakinessProjectConfig.load();
|
|
810
|
+
const projectPublicId = config.projectPublicId();
|
|
811
|
+
const reportViewerEndpoint = config.reportViewerUrl();
|
|
812
|
+
const token = randomUUIDBase62();
|
|
813
|
+
const server = new StaticServer(token, reportFolder, reportViewerEndpoint);
|
|
814
|
+
await server.start(9373, "127.0.0.1");
|
|
815
|
+
const url = new URL(reportViewerEndpoint);
|
|
816
|
+
url.searchParams.set("port", String(server.port()));
|
|
817
|
+
url.searchParams.set("token", token);
|
|
818
|
+
if (projectPublicId)
|
|
819
|
+
url.searchParams.set("ppid", projectPublicId);
|
|
820
|
+
console.log(chalk.cyan(`
|
|
821
|
+
Serving Flakiness report at ${url.toString()}
|
|
822
|
+
Press Ctrl+C to quit.`));
|
|
823
|
+
await open(url.toString());
|
|
824
|
+
await new Promise(() => {
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
880
828
|
// src/systemUtilizationSampler.ts
|
|
881
829
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
882
830
|
import os2 from "os";
|
|
@@ -951,29 +899,6 @@ function inferRunUrl() {
|
|
|
951
899
|
return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
952
900
|
return void 0;
|
|
953
901
|
}
|
|
954
|
-
async function showReport(reportFolder) {
|
|
955
|
-
const reportPath = path3.join(reportFolder, "report.json");
|
|
956
|
-
const config = await FlakinessProjectConfig.load();
|
|
957
|
-
const projectPublicId = config.projectPublicId();
|
|
958
|
-
const reportViewerEndpoint = config.reportViewerEndpoint();
|
|
959
|
-
const server = await LocalReportServer.create({
|
|
960
|
-
endpoint: reportViewerEndpoint,
|
|
961
|
-
port: 9373,
|
|
962
|
-
reportPath,
|
|
963
|
-
attachmentsFolder: reportFolder
|
|
964
|
-
});
|
|
965
|
-
const url = new URL(reportViewerEndpoint);
|
|
966
|
-
url.searchParams.set("port", String(server.port()));
|
|
967
|
-
url.searchParams.set("token", server.authToken());
|
|
968
|
-
if (projectPublicId)
|
|
969
|
-
url.searchParams.set("ppid", projectPublicId);
|
|
970
|
-
console.log(chalk.cyan(`
|
|
971
|
-
Serving Flakiness report at ${url.toString()}
|
|
972
|
-
Press Ctrl+C to quit.`));
|
|
973
|
-
await open(url.toString());
|
|
974
|
-
await new Promise(() => {
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
902
|
async function saveReport(report, attachments, outputFolder) {
|
|
978
903
|
const reportPath = path3.join(outputFolder, "report.json");
|
|
979
904
|
const attachmentsFolder = path3.join(outputFolder, "attachments");
|