@flakiness/sdk 0.129.4 → 0.130.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 +109 -44
- package/lib/cli/cmd-convert.js +3 -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 +70 -46
- package/package.json +10 -5
- package/types/tsconfig.tsbuildinfo +1 -1
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
|
package/lib/localReportServer.js
CHANGED
|
@@ -7,9 +7,14 @@ import compression from "compression";
|
|
|
7
7
|
import debug from "debug";
|
|
8
8
|
import express from "express";
|
|
9
9
|
import "express-async-errors";
|
|
10
|
-
import fs3 from "fs";
|
|
11
10
|
import http2 from "http";
|
|
12
11
|
|
|
12
|
+
// src/localReportApi.ts
|
|
13
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
14
|
+
import fs2 from "fs";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import { z } from "zod/v4";
|
|
17
|
+
|
|
13
18
|
// src/localGit.ts
|
|
14
19
|
import { exec } from "child_process";
|
|
15
20
|
import { promisify } from "util";
|
|
@@ -50,46 +55,9 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
// src/localReportApi.ts
|
|
54
|
-
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
55
|
-
import fs from "fs";
|
|
56
|
-
import { z } from "zod/v4";
|
|
57
|
-
var t = TypedHTTP.Router.create();
|
|
58
|
-
var localReportRouter = {
|
|
59
|
-
ping: t.get({
|
|
60
|
-
handler: async () => {
|
|
61
|
-
return "pong";
|
|
62
|
-
}
|
|
63
|
-
}),
|
|
64
|
-
lastCommits: t.get({
|
|
65
|
-
handler: async ({ ctx }) => {
|
|
66
|
-
return ctx.commits;
|
|
67
|
-
}
|
|
68
|
-
}),
|
|
69
|
-
report: {
|
|
70
|
-
attachment: t.rawMethod("GET", {
|
|
71
|
-
input: z.object({
|
|
72
|
-
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
73
|
-
}),
|
|
74
|
-
handler: async ({ ctx, input }) => {
|
|
75
|
-
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
76
|
-
if (!idx)
|
|
77
|
-
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
78
|
-
const buffer = await fs.promises.readFile(idx.path);
|
|
79
|
-
return TypedHTTP.ok(buffer, idx.contentType);
|
|
80
|
-
}
|
|
81
|
-
}),
|
|
82
|
-
json: t.get({
|
|
83
|
-
handler: async ({ ctx }) => {
|
|
84
|
-
return ctx.report;
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
58
|
// src/utils.ts
|
|
91
59
|
import { ReportUtils } from "@flakiness/report";
|
|
92
|
-
import
|
|
60
|
+
import fs from "fs";
|
|
93
61
|
import http from "http";
|
|
94
62
|
import https from "https";
|
|
95
63
|
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
@@ -200,7 +168,7 @@ async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
|
200
168
|
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
201
169
|
}
|
|
202
170
|
async function listFilesRecursively(dir, result = []) {
|
|
203
|
-
const entries = await
|
|
171
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
204
172
|
for (const entry of entries) {
|
|
205
173
|
const fullPath = path.join(dir, entry.name);
|
|
206
174
|
if (entry.isDirectory())
|
|
@@ -213,17 +181,27 @@ async function listFilesRecursively(dir, result = []) {
|
|
|
213
181
|
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
214
182
|
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
215
183
|
|
|
216
|
-
// src/
|
|
217
|
-
var
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this._server = _server;
|
|
221
|
-
this._port = _port;
|
|
222
|
-
this._authToken = _authToken;
|
|
184
|
+
// src/localReportApi.ts
|
|
185
|
+
var ReportInfo = class {
|
|
186
|
+
constructor(_options) {
|
|
187
|
+
this._options = _options;
|
|
223
188
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
189
|
+
report;
|
|
190
|
+
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
191
|
+
commits = [];
|
|
192
|
+
async refresh() {
|
|
193
|
+
const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
194
|
+
if (!report) {
|
|
195
|
+
this.report = void 0;
|
|
196
|
+
this.commits = [];
|
|
197
|
+
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
201
|
+
return;
|
|
202
|
+
this.report = report;
|
|
203
|
+
this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
|
|
204
|
+
const attachmentsDir = this._options.attachmentsFolder;
|
|
227
205
|
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
228
206
|
if (missingAttachments.length) {
|
|
229
207
|
const first = missingAttachments.slice(0, 3);
|
|
@@ -232,7 +210,52 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
232
210
|
if (missingAttachments.length > 3)
|
|
233
211
|
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
234
212
|
}
|
|
235
|
-
|
|
213
|
+
this.attachmentIdToPath = attachmentIdToPath;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var t = TypedHTTP.Router.create();
|
|
217
|
+
var localReportRouter = {
|
|
218
|
+
ping: t.get({
|
|
219
|
+
handler: async () => {
|
|
220
|
+
return "pong";
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
lastCommits: t.get({
|
|
224
|
+
handler: async ({ ctx }) => {
|
|
225
|
+
return ctx.reportInfo.commits;
|
|
226
|
+
}
|
|
227
|
+
}),
|
|
228
|
+
report: {
|
|
229
|
+
attachment: t.rawMethod("GET", {
|
|
230
|
+
input: z.object({
|
|
231
|
+
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
232
|
+
}),
|
|
233
|
+
handler: async ({ ctx, input }) => {
|
|
234
|
+
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
235
|
+
if (!idx)
|
|
236
|
+
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
237
|
+
const buffer = await fs2.promises.readFile(idx.path);
|
|
238
|
+
return TypedHTTP.ok(buffer, idx.contentType);
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
json: t.get({
|
|
242
|
+
handler: async ({ ctx }) => {
|
|
243
|
+
await ctx.reportInfo.refresh();
|
|
244
|
+
return ctx.reportInfo.report;
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/localReportServer.ts
|
|
251
|
+
var logHTTPServer = debug("fk:http");
|
|
252
|
+
var LocalReportServer = class _LocalReportServer {
|
|
253
|
+
constructor(_server, _port, _authToken) {
|
|
254
|
+
this._server = _server;
|
|
255
|
+
this._port = _port;
|
|
256
|
+
this._authToken = _authToken;
|
|
257
|
+
}
|
|
258
|
+
static async create(options) {
|
|
236
259
|
const app = express();
|
|
237
260
|
app.set("etag", false);
|
|
238
261
|
const authToken = randomUUIDBase62();
|
|
@@ -255,9 +278,10 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
255
278
|
});
|
|
256
279
|
next();
|
|
257
280
|
});
|
|
281
|
+
const reportInfo = new ReportInfo(options);
|
|
258
282
|
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
259
283
|
router: localReportRouter,
|
|
260
|
-
createRootContext: async ({ req, res, input }) => ({
|
|
284
|
+
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
261
285
|
}));
|
|
262
286
|
app.use((err, req, res, next) => {
|
|
263
287
|
if (err instanceof TypedHTTP2.HttpError)
|
package/lib/playwright-test.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
3
3
|
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
4
4
|
import chalk2 from "chalk";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import fs7 from "fs";
|
|
6
|
+
import path6 from "path";
|
|
7
7
|
|
|
8
8
|
// src/cli/cmd-show-report.ts
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import open from "open";
|
|
11
|
-
import
|
|
11
|
+
import path5 from "path";
|
|
12
12
|
|
|
13
13
|
// src/flakinessConfig.ts
|
|
14
14
|
import fs2 from "fs";
|
|
@@ -429,9 +429,14 @@ import compression from "compression";
|
|
|
429
429
|
import debug from "debug";
|
|
430
430
|
import express from "express";
|
|
431
431
|
import "express-async-errors";
|
|
432
|
-
import fs5 from "fs";
|
|
433
432
|
import http2 from "http";
|
|
434
433
|
|
|
434
|
+
// src/localReportApi.ts
|
|
435
|
+
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
436
|
+
import fs4 from "fs";
|
|
437
|
+
import path4 from "path";
|
|
438
|
+
import { z } from "zod/v4";
|
|
439
|
+
|
|
435
440
|
// src/localGit.ts
|
|
436
441
|
import { exec } from "child_process";
|
|
437
442
|
import { promisify } from "util";
|
|
@@ -473,9 +478,37 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
473
478
|
}
|
|
474
479
|
|
|
475
480
|
// src/localReportApi.ts
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
481
|
+
var ReportInfo = class {
|
|
482
|
+
constructor(_options) {
|
|
483
|
+
this._options = _options;
|
|
484
|
+
}
|
|
485
|
+
report;
|
|
486
|
+
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
487
|
+
commits = [];
|
|
488
|
+
async refresh() {
|
|
489
|
+
const report = await fs4.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
490
|
+
if (!report) {
|
|
491
|
+
this.report = void 0;
|
|
492
|
+
this.commits = [];
|
|
493
|
+
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
497
|
+
return;
|
|
498
|
+
this.report = report;
|
|
499
|
+
this.commits = await listLocalCommits(path4.dirname(this._options.reportPath), report.commitId, 100);
|
|
500
|
+
const attachmentsDir = this._options.attachmentsFolder;
|
|
501
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
502
|
+
if (missingAttachments.length) {
|
|
503
|
+
const first = missingAttachments.slice(0, 3);
|
|
504
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
505
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
506
|
+
if (missingAttachments.length > 3)
|
|
507
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
508
|
+
}
|
|
509
|
+
this.attachmentIdToPath = attachmentIdToPath;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
479
512
|
var t = TypedHTTP2.Router.create();
|
|
480
513
|
var localReportRouter = {
|
|
481
514
|
ping: t.get({
|
|
@@ -485,7 +518,7 @@ var localReportRouter = {
|
|
|
485
518
|
}),
|
|
486
519
|
lastCommits: t.get({
|
|
487
520
|
handler: async ({ ctx }) => {
|
|
488
|
-
return ctx.commits;
|
|
521
|
+
return ctx.reportInfo.commits;
|
|
489
522
|
}
|
|
490
523
|
}),
|
|
491
524
|
report: {
|
|
@@ -494,7 +527,7 @@ var localReportRouter = {
|
|
|
494
527
|
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
495
528
|
}),
|
|
496
529
|
handler: async ({ ctx, input }) => {
|
|
497
|
-
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
530
|
+
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
498
531
|
if (!idx)
|
|
499
532
|
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
500
533
|
const buffer = await fs4.promises.readFile(idx.path);
|
|
@@ -503,7 +536,8 @@ var localReportRouter = {
|
|
|
503
536
|
}),
|
|
504
537
|
json: t.get({
|
|
505
538
|
handler: async ({ ctx }) => {
|
|
506
|
-
|
|
539
|
+
await ctx.reportInfo.refresh();
|
|
540
|
+
return ctx.reportInfo.report;
|
|
507
541
|
}
|
|
508
542
|
})
|
|
509
543
|
}
|
|
@@ -518,17 +552,6 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
518
552
|
this._authToken = _authToken;
|
|
519
553
|
}
|
|
520
554
|
static async create(options) {
|
|
521
|
-
const report = JSON.parse(await fs5.promises.readFile(options.reportPath, "utf-8"));
|
|
522
|
-
const attachmentsDir = options.attachmentsFolder;
|
|
523
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
524
|
-
if (missingAttachments.length) {
|
|
525
|
-
const first = missingAttachments.slice(0, 3);
|
|
526
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
527
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
528
|
-
if (missingAttachments.length > 3)
|
|
529
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
530
|
-
}
|
|
531
|
-
const commits = await listLocalCommits(process.cwd(), report.commitId, 100);
|
|
532
555
|
const app = express();
|
|
533
556
|
app.set("etag", false);
|
|
534
557
|
const authToken = randomUUIDBase62();
|
|
@@ -551,9 +574,10 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
551
574
|
});
|
|
552
575
|
next();
|
|
553
576
|
});
|
|
577
|
+
const reportInfo = new ReportInfo(options);
|
|
554
578
|
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
555
579
|
router: localReportRouter,
|
|
556
|
-
createRootContext: async ({ req, res, input }) => ({
|
|
580
|
+
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
557
581
|
}));
|
|
558
582
|
app.use((err2, req, res, next) => {
|
|
559
583
|
if (err2 instanceof TypedHTTP3.HttpError)
|
|
@@ -587,7 +611,7 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
587
611
|
|
|
588
612
|
// src/cli/cmd-show-report.ts
|
|
589
613
|
async function cmdShowReport(reportFolder) {
|
|
590
|
-
const reportPath =
|
|
614
|
+
const reportPath = path5.join(reportFolder, "report.json");
|
|
591
615
|
const session = await FlakinessSession.load();
|
|
592
616
|
const config = await FlakinessConfig.load();
|
|
593
617
|
const projectPublicId = config.projectPublicId();
|
|
@@ -610,12 +634,12 @@ async function cmdShowReport(reportFolder) {
|
|
|
610
634
|
|
|
611
635
|
// src/createTestStepSnippets.ts
|
|
612
636
|
import { codeFrameColumns } from "@babel/code-frame";
|
|
613
|
-
import
|
|
637
|
+
import fs5 from "fs";
|
|
614
638
|
function createTestStepSnippets(filepathToSteps) {
|
|
615
639
|
for (const [filepath, steps] of filepathToSteps) {
|
|
616
640
|
let source;
|
|
617
641
|
try {
|
|
618
|
-
source =
|
|
642
|
+
source = fs5.readFileSync(filepath, "utf-8");
|
|
619
643
|
} catch (e) {
|
|
620
644
|
continue;
|
|
621
645
|
}
|
|
@@ -640,7 +664,7 @@ function createTestStepSnippets(filepathToSteps) {
|
|
|
640
664
|
// src/reportUploader.ts
|
|
641
665
|
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
642
666
|
import assert2 from "assert";
|
|
643
|
-
import
|
|
667
|
+
import fs6 from "fs";
|
|
644
668
|
import { URL } from "url";
|
|
645
669
|
var ReportUploader = class _ReportUploader {
|
|
646
670
|
static optionsFromEnv(overrides) {
|
|
@@ -738,16 +762,16 @@ var ReportUpload = class {
|
|
|
738
762
|
url: uploadUrl,
|
|
739
763
|
headers: {
|
|
740
764
|
"Content-Type": attachment.contentType,
|
|
741
|
-
"Content-Length": (await
|
|
765
|
+
"Content-Length": (await fs6.promises.stat(attachmentPath)).size + ""
|
|
742
766
|
},
|
|
743
767
|
method: "put"
|
|
744
768
|
});
|
|
745
|
-
|
|
769
|
+
fs6.createReadStream(attachmentPath).pipe(request);
|
|
746
770
|
await responseDataPromise;
|
|
747
771
|
}, HTTP_BACKOFF);
|
|
748
772
|
return;
|
|
749
773
|
}
|
|
750
|
-
let buffer = attachment.body ? attachment.body : attachment.path ? await
|
|
774
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs6.promises.readFile(attachment.path) : void 0;
|
|
751
775
|
assert2(buffer);
|
|
752
776
|
const encoding = compressable ? "br" : void 0;
|
|
753
777
|
if (compressable)
|
|
@@ -844,7 +868,7 @@ var err = (txt) => console.error(chalk2.red(`[flakiness.io] ${txt}`));
|
|
|
844
868
|
var FlakinessReporter = class {
|
|
845
869
|
constructor(_options = {}) {
|
|
846
870
|
this._options = _options;
|
|
847
|
-
this._outputFolder =
|
|
871
|
+
this._outputFolder = path6.join(process.cwd(), this._options.outputFolder ?? process.env.FLAKINESS_OUTPUT_DIR ?? "flakiness-report");
|
|
848
872
|
}
|
|
849
873
|
_config;
|
|
850
874
|
_rootSuite;
|
|
@@ -947,7 +971,7 @@ var FlakinessReporter = class {
|
|
|
947
971
|
location: pwStep.location ? this._createLocation(context, pwStep.location) : void 0
|
|
948
972
|
};
|
|
949
973
|
if (pwStep.location) {
|
|
950
|
-
const resolvedPath =
|
|
974
|
+
const resolvedPath = path6.resolve(pwStep.location.file);
|
|
951
975
|
this._filepathToSteps.set(resolvedPath, step);
|
|
952
976
|
}
|
|
953
977
|
if (pwStep.error)
|
|
@@ -996,10 +1020,10 @@ var FlakinessReporter = class {
|
|
|
996
1020
|
const environmentsMap = createEnvironments(this._config.projects);
|
|
997
1021
|
if (this._options.collectBrowserVersions) {
|
|
998
1022
|
try {
|
|
999
|
-
let playwrightPath =
|
|
1000
|
-
while (
|
|
1001
|
-
playwrightPath =
|
|
1002
|
-
const module = await import(
|
|
1023
|
+
let playwrightPath = fs7.realpathSync(process.argv[1]);
|
|
1024
|
+
while (path6.basename(playwrightPath) !== "test")
|
|
1025
|
+
playwrightPath = path6.dirname(playwrightPath);
|
|
1026
|
+
const module = await import(path6.join(playwrightPath, "index.js"));
|
|
1003
1027
|
for (const [project, env] of environmentsMap) {
|
|
1004
1028
|
const { browserName = "chromium", channel, headless } = project.use;
|
|
1005
1029
|
let browserType;
|
|
@@ -1048,19 +1072,19 @@ var FlakinessReporter = class {
|
|
|
1048
1072
|
for (const unaccessibleAttachment of context.unaccessibleAttachmentPaths)
|
|
1049
1073
|
warn(`cannot access attachment ${unaccessibleAttachment}`);
|
|
1050
1074
|
this._report = report;
|
|
1051
|
-
const reportPath =
|
|
1052
|
-
const attachmentsFolder =
|
|
1053
|
-
await
|
|
1054
|
-
await
|
|
1055
|
-
await
|
|
1075
|
+
const reportPath = path6.join(this._outputFolder, "report.json");
|
|
1076
|
+
const attachmentsFolder = path6.join(this._outputFolder, "attachments");
|
|
1077
|
+
await fs7.promises.rm(this._outputFolder, { recursive: true, force: true });
|
|
1078
|
+
await fs7.promises.mkdir(this._outputFolder, { recursive: true });
|
|
1079
|
+
await fs7.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
1056
1080
|
if (context.attachments.size)
|
|
1057
|
-
await
|
|
1081
|
+
await fs7.promises.mkdir(attachmentsFolder);
|
|
1058
1082
|
for (const attachment of context.attachments.values()) {
|
|
1059
|
-
const attachmentPath =
|
|
1083
|
+
const attachmentPath = path6.join(attachmentsFolder, attachment.id);
|
|
1060
1084
|
if (attachment.path)
|
|
1061
|
-
await
|
|
1085
|
+
await fs7.promises.cp(attachment.path, attachmentPath);
|
|
1062
1086
|
else if (attachment.body)
|
|
1063
|
-
await
|
|
1087
|
+
await fs7.promises.writeFile(attachmentPath, attachment.body);
|
|
1064
1088
|
this._attachments.push({
|
|
1065
1089
|
contentType: attachment.contentType,
|
|
1066
1090
|
id: attachment.id,
|
|
@@ -1084,8 +1108,8 @@ var FlakinessReporter = class {
|
|
|
1084
1108
|
if (shouldOpen) {
|
|
1085
1109
|
await cmdShowReport(this._outputFolder);
|
|
1086
1110
|
} else {
|
|
1087
|
-
const defaultOutputFolder =
|
|
1088
|
-
const folder = defaultOutputFolder === this._outputFolder ? "" :
|
|
1111
|
+
const defaultOutputFolder = path6.join(process.cwd(), "flakiness-report");
|
|
1112
|
+
const folder = defaultOutputFolder === this._outputFolder ? "" : path6.relative(process.cwd(), this._outputFolder);
|
|
1089
1113
|
console.log(`
|
|
1090
1114
|
To open last Flakiness report run:
|
|
1091
1115
|
|