@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/localReportServer.js
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
// src/localReportServer.ts
|
|
2
|
-
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
3
|
-
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
4
|
-
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
5
|
-
import bodyParser from "body-parser";
|
|
6
|
-
import compression from "compression";
|
|
7
|
-
import debug2 from "debug";
|
|
8
|
-
import express from "express";
|
|
9
|
-
import "express-async-errors";
|
|
10
|
-
import http from "http";
|
|
11
|
-
|
|
12
|
-
// src/localReportApi.ts
|
|
13
|
-
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
import path from "path";
|
|
16
|
-
import { z } from "zod/v4";
|
|
17
|
-
|
|
18
|
-
// src/localGit.ts
|
|
19
|
-
import { exec } from "child_process";
|
|
20
|
-
import debug from "debug";
|
|
21
|
-
import { promisify } from "util";
|
|
22
|
-
var log = debug("fk:git");
|
|
23
|
-
var execAsync = promisify(exec);
|
|
24
|
-
async function listLocalCommits(gitRoot, head, count) {
|
|
25
|
-
const FIELD_SEPARATOR = "|~|";
|
|
26
|
-
const RECORD_SEPARATOR = "\0";
|
|
27
|
-
const prettyFormat = [
|
|
28
|
-
"%H",
|
|
29
|
-
// %H: Full commit hash
|
|
30
|
-
"%at",
|
|
31
|
-
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
32
|
-
"%an",
|
|
33
|
-
// %an: Author name
|
|
34
|
-
"%s",
|
|
35
|
-
// %s: Subject (the first line of the commit message)
|
|
36
|
-
"%P"
|
|
37
|
-
// %P: Parent hashes (space-separated)
|
|
38
|
-
].join(FIELD_SEPARATOR);
|
|
39
|
-
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
40
|
-
try {
|
|
41
|
-
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
42
|
-
if (!stdout) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
46
|
-
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
47
|
-
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
48
|
-
return {
|
|
49
|
-
commitId,
|
|
50
|
-
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
51
|
-
author,
|
|
52
|
-
message,
|
|
53
|
-
parents,
|
|
54
|
-
walkIndex: 0
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
} catch (error) {
|
|
58
|
-
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/reportUtils.ts
|
|
64
|
-
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
65
|
-
import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
|
|
66
|
-
var ReportUtils;
|
|
67
|
-
((ReportUtils2) => {
|
|
68
|
-
function visitTests(report, testVisitor) {
|
|
69
|
-
function visitSuite(suite, parents) {
|
|
70
|
-
parents.push(suite);
|
|
71
|
-
for (const test of suite.tests ?? [])
|
|
72
|
-
testVisitor(test, parents);
|
|
73
|
-
for (const childSuite of suite.suites ?? [])
|
|
74
|
-
visitSuite(childSuite, parents);
|
|
75
|
-
parents.pop();
|
|
76
|
-
}
|
|
77
|
-
for (const test of report.tests ?? [])
|
|
78
|
-
testVisitor(test, []);
|
|
79
|
-
for (const suite of report.suites)
|
|
80
|
-
visitSuite(suite, []);
|
|
81
|
-
}
|
|
82
|
-
ReportUtils2.visitTests = visitTests;
|
|
83
|
-
function normalizeReport(report) {
|
|
84
|
-
const gEnvs = /* @__PURE__ */ new Map();
|
|
85
|
-
const gSuites = /* @__PURE__ */ new Map();
|
|
86
|
-
const gTests = new Multimap();
|
|
87
|
-
const gSuiteIds = /* @__PURE__ */ new Map();
|
|
88
|
-
const gTestIds = /* @__PURE__ */ new Map();
|
|
89
|
-
const gEnvIds = /* @__PURE__ */ new Map();
|
|
90
|
-
const gSuiteChildren = new Multimap();
|
|
91
|
-
const gSuiteTests = new Multimap();
|
|
92
|
-
for (const env of report.environments) {
|
|
93
|
-
const envId = computeEnvId(env);
|
|
94
|
-
gEnvs.set(envId, env);
|
|
95
|
-
gEnvIds.set(env, envId);
|
|
96
|
-
}
|
|
97
|
-
const usedEnvIds = /* @__PURE__ */ new Set();
|
|
98
|
-
function visitTests2(tests, suiteId) {
|
|
99
|
-
for (const test of tests ?? []) {
|
|
100
|
-
const testId = computeTestId(test, suiteId);
|
|
101
|
-
gTests.set(testId, test);
|
|
102
|
-
gTestIds.set(test, testId);
|
|
103
|
-
gSuiteTests.set(suiteId, test);
|
|
104
|
-
for (const attempt of test.attempts) {
|
|
105
|
-
const env = report.environments[attempt.environmentIdx];
|
|
106
|
-
const envId = gEnvIds.get(env);
|
|
107
|
-
usedEnvIds.add(envId);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function visitSuite(suite, parentSuiteId) {
|
|
112
|
-
const suiteId = computeSuiteId(suite, parentSuiteId);
|
|
113
|
-
gSuites.set(suiteId, suite);
|
|
114
|
-
gSuiteIds.set(suite, suiteId);
|
|
115
|
-
for (const childSuite of suite.suites ?? []) {
|
|
116
|
-
visitSuite(childSuite, suiteId);
|
|
117
|
-
gSuiteChildren.set(suiteId, childSuite);
|
|
118
|
-
}
|
|
119
|
-
visitTests2(suite.tests ?? [], suiteId);
|
|
120
|
-
}
|
|
121
|
-
function transformTests(tests) {
|
|
122
|
-
const testIds = new Set(tests.map((test) => gTestIds.get(test)));
|
|
123
|
-
return [...testIds].map((testId) => {
|
|
124
|
-
const tests2 = gTests.getAll(testId);
|
|
125
|
-
const tags = tests2.map((test) => test.tags ?? []).flat();
|
|
126
|
-
return {
|
|
127
|
-
location: tests2[0].location,
|
|
128
|
-
title: tests2[0].title,
|
|
129
|
-
tags: tags.length ? tags : void 0,
|
|
130
|
-
attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
|
|
131
|
-
...attempt,
|
|
132
|
-
environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
|
|
133
|
-
}))
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
function transformSuites(suites) {
|
|
138
|
-
const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
|
|
139
|
-
return [...suiteIds].map((suiteId) => {
|
|
140
|
-
const suite = gSuites.get(suiteId);
|
|
141
|
-
return {
|
|
142
|
-
location: suite.location,
|
|
143
|
-
title: suite.title,
|
|
144
|
-
type: suite.type,
|
|
145
|
-
suites: transformSuites(gSuiteChildren.getAll(suiteId)),
|
|
146
|
-
tests: transformTests(gSuiteTests.getAll(suiteId))
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
visitTests2(report.tests ?? [], "suiteless");
|
|
151
|
-
for (const suite of report.suites)
|
|
152
|
-
visitSuite(suite);
|
|
153
|
-
const newEnvironments = [...usedEnvIds];
|
|
154
|
-
const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
|
|
155
|
-
return {
|
|
156
|
-
...report,
|
|
157
|
-
environments: newEnvironments.map((envId) => gEnvs.get(envId)),
|
|
158
|
-
suites: transformSuites(report.suites),
|
|
159
|
-
tests: transformTests(report.tests ?? [])
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
ReportUtils2.normalizeReport = normalizeReport;
|
|
163
|
-
function computeEnvId(env) {
|
|
164
|
-
return xxHashObject(env);
|
|
165
|
-
}
|
|
166
|
-
function computeSuiteId(suite, parentSuiteId) {
|
|
167
|
-
return xxHash([
|
|
168
|
-
parentSuiteId ?? "",
|
|
169
|
-
suite.type,
|
|
170
|
-
suite.location?.file ?? "",
|
|
171
|
-
suite.title
|
|
172
|
-
]);
|
|
173
|
-
}
|
|
174
|
-
function computeTestId(test, suiteId) {
|
|
175
|
-
return xxHash([
|
|
176
|
-
suiteId,
|
|
177
|
-
test.location?.file ?? "",
|
|
178
|
-
test.title
|
|
179
|
-
]);
|
|
180
|
-
}
|
|
181
|
-
})(ReportUtils || (ReportUtils = {}));
|
|
182
|
-
|
|
183
|
-
// src/localReportApi.ts
|
|
184
|
-
var ReportInfo = class {
|
|
185
|
-
constructor(_options) {
|
|
186
|
-
this._options = _options;
|
|
187
|
-
}
|
|
188
|
-
report;
|
|
189
|
-
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
190
|
-
commits = [];
|
|
191
|
-
async refresh() {
|
|
192
|
-
const report = await fs.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
193
|
-
if (!report) {
|
|
194
|
-
this.report = void 0;
|
|
195
|
-
this.commits = [];
|
|
196
|
-
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
200
|
-
return;
|
|
201
|
-
this.report = report;
|
|
202
|
-
this.commits = await listLocalCommits(path.dirname(this._options.reportPath), report.commitId, 100);
|
|
203
|
-
const attachmentsDir = this._options.attachmentsFolder;
|
|
204
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
205
|
-
if (missingAttachments.length) {
|
|
206
|
-
const first = missingAttachments.slice(0, 3);
|
|
207
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
208
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
209
|
-
if (missingAttachments.length > 3)
|
|
210
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
211
|
-
}
|
|
212
|
-
this.attachmentIdToPath = attachmentIdToPath;
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
var t = TypedHTTP.Router.create();
|
|
216
|
-
var localReportRouter = {
|
|
217
|
-
ping: t.get({
|
|
218
|
-
handler: async () => {
|
|
219
|
-
return "pong";
|
|
220
|
-
}
|
|
221
|
-
}),
|
|
222
|
-
lastCommits: t.get({
|
|
223
|
-
handler: async ({ ctx }) => {
|
|
224
|
-
return ctx.reportInfo.commits;
|
|
225
|
-
}
|
|
226
|
-
}),
|
|
227
|
-
report: {
|
|
228
|
-
attachment: t.rawMethod("GET", {
|
|
229
|
-
input: z.object({
|
|
230
|
-
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
231
|
-
}),
|
|
232
|
-
handler: async ({ ctx, input }) => {
|
|
233
|
-
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
234
|
-
if (!idx)
|
|
235
|
-
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
236
|
-
const buffer = await fs.promises.readFile(idx.path);
|
|
237
|
-
return TypedHTTP.ok(buffer, idx.contentType);
|
|
238
|
-
}
|
|
239
|
-
}),
|
|
240
|
-
json: t.get({
|
|
241
|
-
handler: async ({ ctx }) => {
|
|
242
|
-
await ctx.reportInfo.refresh();
|
|
243
|
-
return ctx.reportInfo.report;
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
249
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
250
|
-
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
251
|
-
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
252
|
-
const missingAttachments = /* @__PURE__ */ new Set();
|
|
253
|
-
ReportUtils.visitTests(report, (test) => {
|
|
254
|
-
for (const attempt of test.attempts) {
|
|
255
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
256
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
257
|
-
if (!attachmentPath) {
|
|
258
|
-
missingAttachments.add(attachment.id);
|
|
259
|
-
} else {
|
|
260
|
-
attachmentIdToPath.set(attachment.id, {
|
|
261
|
-
contentType: attachment.contentType,
|
|
262
|
-
id: attachment.id,
|
|
263
|
-
path: attachmentPath
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
270
|
-
}
|
|
271
|
-
async function listFilesRecursively(dir, result = []) {
|
|
272
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
273
|
-
for (const entry of entries) {
|
|
274
|
-
const fullPath = path.join(dir, entry.name);
|
|
275
|
-
if (entry.isDirectory())
|
|
276
|
-
await listFilesRecursively(fullPath, result);
|
|
277
|
-
else
|
|
278
|
-
result.push(fullPath);
|
|
279
|
-
}
|
|
280
|
-
return result;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// src/localReportServer.ts
|
|
284
|
-
var logHTTPServer = debug2("fk:http");
|
|
285
|
-
var LocalReportServer = class _LocalReportServer {
|
|
286
|
-
constructor(_server, _port, _authToken) {
|
|
287
|
-
this._server = _server;
|
|
288
|
-
this._port = _port;
|
|
289
|
-
this._authToken = _authToken;
|
|
290
|
-
}
|
|
291
|
-
static async create(options) {
|
|
292
|
-
const app = express();
|
|
293
|
-
app.set("etag", false);
|
|
294
|
-
const authToken = randomUUIDBase62();
|
|
295
|
-
app.use(compression());
|
|
296
|
-
app.use(bodyParser.json({ limit: 256 * 1024 }));
|
|
297
|
-
app.use((req, res, next) => {
|
|
298
|
-
if (!req.path.startsWith("/" + authToken))
|
|
299
|
-
throw TypedHTTP2.HttpError.withCode("UNAUTHORIZED");
|
|
300
|
-
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
301
|
-
res.setHeader("Access-Control-Allow-Origin", options.endpoint);
|
|
302
|
-
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
303
|
-
if (req.method === "OPTIONS") {
|
|
304
|
-
res.writeHead(204);
|
|
305
|
-
res.end();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
|
|
309
|
-
res.on("close", () => {
|
|
310
|
-
if (!res.headersSent) logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
|
|
311
|
-
});
|
|
312
|
-
next();
|
|
313
|
-
});
|
|
314
|
-
const reportInfo = new ReportInfo(options);
|
|
315
|
-
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
316
|
-
router: localReportRouter,
|
|
317
|
-
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
318
|
-
}));
|
|
319
|
-
app.use((err, req, res, next) => {
|
|
320
|
-
if (err instanceof TypedHTTP2.HttpError)
|
|
321
|
-
return res.status(err.status).send({ error: err.message });
|
|
322
|
-
logHTTPServer(err);
|
|
323
|
-
res.status(500).send({ error: "Internal Server Error" });
|
|
324
|
-
});
|
|
325
|
-
const server = http.createServer(app);
|
|
326
|
-
server.on("error", (err) => {
|
|
327
|
-
if (err.code === "ECONNRESET") {
|
|
328
|
-
logHTTPServer("Client connection reset. Ignoring.");
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
throw err;
|
|
332
|
-
});
|
|
333
|
-
const port = await new Promise((resolve) => server.listen(options.port, () => {
|
|
334
|
-
resolve(server.address().port);
|
|
335
|
-
}));
|
|
336
|
-
return new _LocalReportServer(server, port, authToken);
|
|
337
|
-
}
|
|
338
|
-
authToken() {
|
|
339
|
-
return this._authToken;
|
|
340
|
-
}
|
|
341
|
-
port() {
|
|
342
|
-
return this._port;
|
|
343
|
-
}
|
|
344
|
-
async dispose() {
|
|
345
|
-
await new Promise((x) => this._server.close(x));
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
export {
|
|
349
|
-
LocalReportServer
|
|
350
|
-
};
|
|
351
|
-
//# sourceMappingURL=localReportServer.js.map
|