@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/showReport.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/showReport.ts
|
|
2
|
+
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
2
3
|
import chalk from "chalk";
|
|
3
4
|
import open from "open";
|
|
4
|
-
import path3 from "path";
|
|
5
5
|
|
|
6
6
|
// src/flakinessProjectConfig.ts
|
|
7
7
|
import fs from "fs";
|
|
@@ -93,8 +93,14 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
93
93
|
projectPublicId() {
|
|
94
94
|
return this._config.projectPublicId;
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
return this._config.
|
|
96
|
+
reportViewerUrl() {
|
|
97
|
+
return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
|
|
98
|
+
}
|
|
99
|
+
setCustomReportViewerUrl(url) {
|
|
100
|
+
if (url)
|
|
101
|
+
this._config.customReportViewerUrl = url;
|
|
102
|
+
else
|
|
103
|
+
delete this._config.customReportViewerUrl;
|
|
98
104
|
}
|
|
99
105
|
setProjectPublicId(projectId) {
|
|
100
106
|
this._config.projectPublicId = projectId;
|
|
@@ -105,369 +111,163 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
105
111
|
}
|
|
106
112
|
};
|
|
107
113
|
|
|
108
|
-
// src/
|
|
109
|
-
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
110
|
-
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
111
|
-
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
112
|
-
import bodyParser from "body-parser";
|
|
113
|
-
import compression from "compression";
|
|
114
|
-
import debug2 from "debug";
|
|
115
|
-
import express from "express";
|
|
116
|
-
import "express-async-errors";
|
|
117
|
-
import http from "http";
|
|
118
|
-
|
|
119
|
-
// src/localReportApi.ts
|
|
120
|
-
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
121
|
-
import fs2 from "fs";
|
|
122
|
-
import path2 from "path";
|
|
123
|
-
import { z } from "zod/v4";
|
|
124
|
-
|
|
125
|
-
// src/localGit.ts
|
|
126
|
-
import { exec } from "child_process";
|
|
114
|
+
// src/staticServer.ts
|
|
127
115
|
import debug from "debug";
|
|
128
|
-
import
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
155
|
-
return {
|
|
156
|
-
commitId,
|
|
157
|
-
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
158
|
-
author,
|
|
159
|
-
message,
|
|
160
|
-
parents,
|
|
161
|
-
walkIndex: 0
|
|
162
|
-
};
|
|
163
|
-
});
|
|
164
|
-
} catch (error) {
|
|
165
|
-
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/reportUtils.ts
|
|
171
|
-
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
172
|
-
import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
|
|
173
|
-
var ReportUtils;
|
|
174
|
-
((ReportUtils2) => {
|
|
175
|
-
function visitTests(report, testVisitor) {
|
|
176
|
-
function visitSuite(suite, parents) {
|
|
177
|
-
parents.push(suite);
|
|
178
|
-
for (const test of suite.tests ?? [])
|
|
179
|
-
testVisitor(test, parents);
|
|
180
|
-
for (const childSuite of suite.suites ?? [])
|
|
181
|
-
visitSuite(childSuite, parents);
|
|
182
|
-
parents.pop();
|
|
183
|
-
}
|
|
184
|
-
for (const test of report.tests ?? [])
|
|
185
|
-
testVisitor(test, []);
|
|
186
|
-
for (const suite of report.suites)
|
|
187
|
-
visitSuite(suite, []);
|
|
188
|
-
}
|
|
189
|
-
ReportUtils2.visitTests = visitTests;
|
|
190
|
-
function normalizeReport(report) {
|
|
191
|
-
const gEnvs = /* @__PURE__ */ new Map();
|
|
192
|
-
const gSuites = /* @__PURE__ */ new Map();
|
|
193
|
-
const gTests = new Multimap();
|
|
194
|
-
const gSuiteIds = /* @__PURE__ */ new Map();
|
|
195
|
-
const gTestIds = /* @__PURE__ */ new Map();
|
|
196
|
-
const gEnvIds = /* @__PURE__ */ new Map();
|
|
197
|
-
const gSuiteChildren = new Multimap();
|
|
198
|
-
const gSuiteTests = new Multimap();
|
|
199
|
-
for (const env of report.environments) {
|
|
200
|
-
const envId = computeEnvId(env);
|
|
201
|
-
gEnvs.set(envId, env);
|
|
202
|
-
gEnvIds.set(env, envId);
|
|
203
|
-
}
|
|
204
|
-
const usedEnvIds = /* @__PURE__ */ new Set();
|
|
205
|
-
function visitTests2(tests, suiteId) {
|
|
206
|
-
for (const test of tests ?? []) {
|
|
207
|
-
const testId = computeTestId(test, suiteId);
|
|
208
|
-
gTests.set(testId, test);
|
|
209
|
-
gTestIds.set(test, testId);
|
|
210
|
-
gSuiteTests.set(suiteId, test);
|
|
211
|
-
for (const attempt of test.attempts) {
|
|
212
|
-
const env = report.environments[attempt.environmentIdx];
|
|
213
|
-
const envId = gEnvIds.get(env);
|
|
214
|
-
usedEnvIds.add(envId);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function visitSuite(suite, parentSuiteId) {
|
|
219
|
-
const suiteId = computeSuiteId(suite, parentSuiteId);
|
|
220
|
-
gSuites.set(suiteId, suite);
|
|
221
|
-
gSuiteIds.set(suite, suiteId);
|
|
222
|
-
for (const childSuite of suite.suites ?? []) {
|
|
223
|
-
visitSuite(childSuite, suiteId);
|
|
224
|
-
gSuiteChildren.set(suiteId, childSuite);
|
|
225
|
-
}
|
|
226
|
-
visitTests2(suite.tests ?? [], suiteId);
|
|
227
|
-
}
|
|
228
|
-
function transformTests(tests) {
|
|
229
|
-
const testIds = new Set(tests.map((test) => gTestIds.get(test)));
|
|
230
|
-
return [...testIds].map((testId) => {
|
|
231
|
-
const tests2 = gTests.getAll(testId);
|
|
232
|
-
const tags = tests2.map((test) => test.tags ?? []).flat();
|
|
233
|
-
return {
|
|
234
|
-
location: tests2[0].location,
|
|
235
|
-
title: tests2[0].title,
|
|
236
|
-
tags: tags.length ? tags : void 0,
|
|
237
|
-
attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
|
|
238
|
-
...attempt,
|
|
239
|
-
environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
|
|
240
|
-
}))
|
|
241
|
-
};
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
function transformSuites(suites) {
|
|
245
|
-
const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
|
|
246
|
-
return [...suiteIds].map((suiteId) => {
|
|
247
|
-
const suite = gSuites.get(suiteId);
|
|
248
|
-
return {
|
|
249
|
-
location: suite.location,
|
|
250
|
-
title: suite.title,
|
|
251
|
-
type: suite.type,
|
|
252
|
-
suites: transformSuites(gSuiteChildren.getAll(suiteId)),
|
|
253
|
-
tests: transformTests(gSuiteTests.getAll(suiteId))
|
|
254
|
-
};
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
visitTests2(report.tests ?? [], "suiteless");
|
|
258
|
-
for (const suite of report.suites)
|
|
259
|
-
visitSuite(suite);
|
|
260
|
-
const newEnvironments = [...usedEnvIds];
|
|
261
|
-
const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
|
|
262
|
-
return {
|
|
263
|
-
...report,
|
|
264
|
-
environments: newEnvironments.map((envId) => gEnvs.get(envId)),
|
|
265
|
-
suites: transformSuites(report.suites),
|
|
266
|
-
tests: transformTests(report.tests ?? [])
|
|
267
|
-
};
|
|
116
|
+
import * as fs2 from "fs";
|
|
117
|
+
import * as http from "http";
|
|
118
|
+
import * as path2 from "path";
|
|
119
|
+
var log = debug("fk:static_server");
|
|
120
|
+
var StaticServer = class {
|
|
121
|
+
_server;
|
|
122
|
+
_absoluteFolderPath;
|
|
123
|
+
_pathPrefix;
|
|
124
|
+
_cors;
|
|
125
|
+
_mimeTypes = {
|
|
126
|
+
".html": "text/html",
|
|
127
|
+
".js": "text/javascript",
|
|
128
|
+
".css": "text/css",
|
|
129
|
+
".json": "application/json",
|
|
130
|
+
".png": "image/png",
|
|
131
|
+
".jpg": "image/jpeg",
|
|
132
|
+
".gif": "image/gif",
|
|
133
|
+
".svg": "image/svg+xml",
|
|
134
|
+
".ico": "image/x-icon",
|
|
135
|
+
".txt": "text/plain"
|
|
136
|
+
};
|
|
137
|
+
constructor(pathPrefix, folderPath, cors) {
|
|
138
|
+
this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
|
|
139
|
+
this._absoluteFolderPath = path2.resolve(folderPath);
|
|
140
|
+
this._cors = cors;
|
|
141
|
+
this._server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
268
142
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return xxHash([
|
|
275
|
-
parentSuiteId ?? "",
|
|
276
|
-
suite.type,
|
|
277
|
-
suite.location?.file ?? "",
|
|
278
|
-
suite.title
|
|
279
|
-
]);
|
|
280
|
-
}
|
|
281
|
-
function computeTestId(test, suiteId) {
|
|
282
|
-
return xxHash([
|
|
283
|
-
suiteId,
|
|
284
|
-
test.location?.file ?? "",
|
|
285
|
-
test.title
|
|
286
|
-
]);
|
|
143
|
+
port() {
|
|
144
|
+
const address = this._server.address();
|
|
145
|
+
if (!address)
|
|
146
|
+
return void 0;
|
|
147
|
+
return address.port;
|
|
287
148
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.
|
|
149
|
+
address() {
|
|
150
|
+
const address = this._server.address();
|
|
151
|
+
if (!address)
|
|
152
|
+
return void 0;
|
|
153
|
+
const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
154
|
+
return `http://${displayHost}:${address.port}${this._pathPrefix}`;
|
|
294
155
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
this.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const attachmentsDir = this._options.attachmentsFolder;
|
|
311
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
312
|
-
if (missingAttachments.length) {
|
|
313
|
-
const first = missingAttachments.slice(0, 3);
|
|
314
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
315
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
316
|
-
if (missingAttachments.length > 3)
|
|
317
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
318
|
-
}
|
|
319
|
-
this.attachmentIdToPath = attachmentIdToPath;
|
|
156
|
+
async _startServer(port, host) {
|
|
157
|
+
let okListener;
|
|
158
|
+
let errListener;
|
|
159
|
+
const result = new Promise((resolve2, reject) => {
|
|
160
|
+
okListener = resolve2;
|
|
161
|
+
errListener = reject;
|
|
162
|
+
}).finally(() => {
|
|
163
|
+
this._server.removeListener("listening", okListener);
|
|
164
|
+
this._server.removeListener("error", errListener);
|
|
165
|
+
});
|
|
166
|
+
this._server.once("listening", okListener);
|
|
167
|
+
this._server.once("error", errListener);
|
|
168
|
+
this._server.listen(port, host);
|
|
169
|
+
await result;
|
|
170
|
+
log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
|
|
320
171
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
handler: async () => {
|
|
326
|
-
return "pong";
|
|
172
|
+
async start(port, host = "127.0.0.1") {
|
|
173
|
+
if (port === 0) {
|
|
174
|
+
await this._startServer(port, host);
|
|
175
|
+
return this.address();
|
|
327
176
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
177
|
+
for (let i = 0; i < 20; ++i) {
|
|
178
|
+
const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
|
|
179
|
+
if (!err)
|
|
180
|
+
return this.address();
|
|
181
|
+
if (err.code !== "EADDRINUSE")
|
|
182
|
+
throw err;
|
|
183
|
+
log("Port %d is busy (EADDRINUSE). Trying next port...", port);
|
|
184
|
+
port = port + 1;
|
|
185
|
+
if (port > 65535)
|
|
186
|
+
port = 4e3;
|
|
332
187
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
input: z.object({
|
|
337
|
-
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
338
|
-
}),
|
|
339
|
-
handler: async ({ ctx, input }) => {
|
|
340
|
-
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
341
|
-
if (!idx)
|
|
342
|
-
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
343
|
-
const buffer = await fs2.promises.readFile(idx.path);
|
|
344
|
-
return TypedHTTP.ok(buffer, idx.contentType);
|
|
345
|
-
}
|
|
346
|
-
}),
|
|
347
|
-
json: t.get({
|
|
348
|
-
handler: async ({ ctx }) => {
|
|
349
|
-
await ctx.reportInfo.refresh();
|
|
350
|
-
return ctx.reportInfo.report;
|
|
351
|
-
}
|
|
352
|
-
})
|
|
188
|
+
log("All sequential ports busy. Falling back to random port.");
|
|
189
|
+
await this._startServer(0, host);
|
|
190
|
+
return this.address();
|
|
353
191
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
ReportUtils.visitTests(report, (test) => {
|
|
361
|
-
for (const attempt of test.attempts) {
|
|
362
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
363
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
364
|
-
if (!attachmentPath) {
|
|
365
|
-
missingAttachments.add(attachment.id);
|
|
192
|
+
stop() {
|
|
193
|
+
return new Promise((resolve2, reject) => {
|
|
194
|
+
this._server.close((err) => {
|
|
195
|
+
if (err) {
|
|
196
|
+
log("Error stopping server: %o", err);
|
|
197
|
+
reject(err);
|
|
366
198
|
} else {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
id: attachment.id,
|
|
370
|
-
path: attachmentPath
|
|
371
|
-
});
|
|
199
|
+
log("Server stopped.");
|
|
200
|
+
resolve2();
|
|
372
201
|
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
377
|
-
}
|
|
378
|
-
async function listFilesRecursively(dir, result = []) {
|
|
379
|
-
const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
380
|
-
for (const entry of entries) {
|
|
381
|
-
const fullPath = path2.join(dir, entry.name);
|
|
382
|
-
if (entry.isDirectory())
|
|
383
|
-
await listFilesRecursively(fullPath, result);
|
|
384
|
-
else
|
|
385
|
-
result.push(fullPath);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
386
204
|
}
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
var logHTTPServer = debug2("fk:http");
|
|
392
|
-
var LocalReportServer = class _LocalReportServer {
|
|
393
|
-
constructor(_server, _port, _authToken) {
|
|
394
|
-
this._server = _server;
|
|
395
|
-
this._port = _port;
|
|
396
|
-
this._authToken = _authToken;
|
|
205
|
+
_errorResponse(req, res, code, text) {
|
|
206
|
+
res.writeHead(code, { "Content-Type": "text/plain" });
|
|
207
|
+
res.end(text);
|
|
208
|
+
log(`[${code}] ${req.method} ${req.url}`);
|
|
397
209
|
}
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
const authToken = randomUUIDBase62();
|
|
402
|
-
app.use(compression());
|
|
403
|
-
app.use(bodyParser.json({ limit: 256 * 1024 }));
|
|
404
|
-
app.use((req, res, next) => {
|
|
405
|
-
if (!req.path.startsWith("/" + authToken))
|
|
406
|
-
throw TypedHTTP2.HttpError.withCode("UNAUTHORIZED");
|
|
210
|
+
_handleRequest(req, res) {
|
|
211
|
+
const { url, method } = req;
|
|
212
|
+
if (this._cors) {
|
|
407
213
|
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
408
|
-
res.setHeader("Access-Control-Allow-Origin",
|
|
214
|
+
res.setHeader("Access-Control-Allow-Origin", this._cors);
|
|
409
215
|
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
410
216
|
if (req.method === "OPTIONS") {
|
|
411
217
|
res.writeHead(204);
|
|
412
218
|
res.end();
|
|
413
219
|
return;
|
|
414
220
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
router: localReportRouter,
|
|
424
|
-
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
425
|
-
}));
|
|
426
|
-
app.use((err, req, res, next) => {
|
|
427
|
-
if (err instanceof TypedHTTP2.HttpError)
|
|
428
|
-
return res.status(err.status).send({ error: err.message });
|
|
429
|
-
logHTTPServer(err);
|
|
430
|
-
res.status(500).send({ error: "Internal Server Error" });
|
|
221
|
+
}
|
|
222
|
+
if (method !== "GET") {
|
|
223
|
+
this._errorResponse(req, res, 405, "Method Not Allowed");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
|
|
227
|
+
res.on("close", () => {
|
|
228
|
+
if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
|
|
431
229
|
});
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
230
|
+
if (!url || !url.startsWith(this._pathPrefix)) {
|
|
231
|
+
this._errorResponse(req, res, 404, "Not Found");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const relativePath = url.slice(this._pathPrefix.length);
|
|
235
|
+
const safeSuffix = path2.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
|
|
236
|
+
const filePath = path2.join(this._absoluteFolderPath, safeSuffix);
|
|
237
|
+
if (!filePath.startsWith(this._absoluteFolderPath)) {
|
|
238
|
+
this._errorResponse(req, res, 403, "Forbidden");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
fs2.stat(filePath, (err, stats) => {
|
|
242
|
+
if (err || !stats.isFile()) {
|
|
243
|
+
this._errorResponse(req, res, 404, "File Not Found");
|
|
436
244
|
return;
|
|
437
245
|
}
|
|
438
|
-
|
|
246
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
247
|
+
const contentType = this._mimeTypes[ext] || "application/octet-stream";
|
|
248
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
249
|
+
log(`[200] ${req.method} ${req.url} -> ${filePath}`);
|
|
250
|
+
const readStream = fs2.createReadStream(filePath);
|
|
251
|
+
readStream.pipe(res);
|
|
252
|
+
readStream.on("error", (err2) => {
|
|
253
|
+
log("Stream error: %o", err2);
|
|
254
|
+
res.end();
|
|
255
|
+
});
|
|
439
256
|
});
|
|
440
|
-
const port = await new Promise((resolve) => server.listen(options.port, () => {
|
|
441
|
-
resolve(server.address().port);
|
|
442
|
-
}));
|
|
443
|
-
return new _LocalReportServer(server, port, authToken);
|
|
444
|
-
}
|
|
445
|
-
authToken() {
|
|
446
|
-
return this._authToken;
|
|
447
|
-
}
|
|
448
|
-
port() {
|
|
449
|
-
return this._port;
|
|
450
|
-
}
|
|
451
|
-
async dispose() {
|
|
452
|
-
await new Promise((x) => this._server.close(x));
|
|
453
257
|
}
|
|
454
258
|
};
|
|
455
259
|
|
|
456
260
|
// src/showReport.ts
|
|
457
261
|
async function showReport(reportFolder) {
|
|
458
|
-
const reportPath = path3.join(reportFolder, "report.json");
|
|
459
262
|
const config = await FlakinessProjectConfig.load();
|
|
460
263
|
const projectPublicId = config.projectPublicId();
|
|
461
|
-
const reportViewerEndpoint = config.
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
reportPath,
|
|
466
|
-
attachmentsFolder: reportFolder
|
|
467
|
-
});
|
|
264
|
+
const reportViewerEndpoint = config.reportViewerUrl();
|
|
265
|
+
const token = randomUUIDBase62();
|
|
266
|
+
const server = new StaticServer(token, reportFolder, reportViewerEndpoint);
|
|
267
|
+
await server.start(9373, "127.0.0.1");
|
|
468
268
|
const url = new URL(reportViewerEndpoint);
|
|
469
269
|
url.searchParams.set("port", String(server.port()));
|
|
470
|
-
url.searchParams.set("token",
|
|
270
|
+
url.searchParams.set("token", token);
|
|
471
271
|
if (projectPublicId)
|
|
472
272
|
url.searchParams.set("ppid", projectPublicId);
|
|
473
273
|
console.log(chalk.cyan(`
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/staticServer.ts
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as http from "http";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
var log = debug("fk:static_server");
|
|
7
|
+
var StaticServer = class {
|
|
8
|
+
_server;
|
|
9
|
+
_absoluteFolderPath;
|
|
10
|
+
_pathPrefix;
|
|
11
|
+
_cors;
|
|
12
|
+
_mimeTypes = {
|
|
13
|
+
".html": "text/html",
|
|
14
|
+
".js": "text/javascript",
|
|
15
|
+
".css": "text/css",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".gif": "image/gif",
|
|
20
|
+
".svg": "image/svg+xml",
|
|
21
|
+
".ico": "image/x-icon",
|
|
22
|
+
".txt": "text/plain"
|
|
23
|
+
};
|
|
24
|
+
constructor(pathPrefix, folderPath, cors) {
|
|
25
|
+
this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
|
|
26
|
+
this._absoluteFolderPath = path.resolve(folderPath);
|
|
27
|
+
this._cors = cors;
|
|
28
|
+
this._server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
29
|
+
}
|
|
30
|
+
port() {
|
|
31
|
+
const address = this._server.address();
|
|
32
|
+
if (!address)
|
|
33
|
+
return void 0;
|
|
34
|
+
return address.port;
|
|
35
|
+
}
|
|
36
|
+
address() {
|
|
37
|
+
const address = this._server.address();
|
|
38
|
+
if (!address)
|
|
39
|
+
return void 0;
|
|
40
|
+
const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
41
|
+
return `http://${displayHost}:${address.port}${this._pathPrefix}`;
|
|
42
|
+
}
|
|
43
|
+
async _startServer(port, host) {
|
|
44
|
+
let okListener;
|
|
45
|
+
let errListener;
|
|
46
|
+
const result = new Promise((resolve2, reject) => {
|
|
47
|
+
okListener = resolve2;
|
|
48
|
+
errListener = reject;
|
|
49
|
+
}).finally(() => {
|
|
50
|
+
this._server.removeListener("listening", okListener);
|
|
51
|
+
this._server.removeListener("error", errListener);
|
|
52
|
+
});
|
|
53
|
+
this._server.once("listening", okListener);
|
|
54
|
+
this._server.once("error", errListener);
|
|
55
|
+
this._server.listen(port, host);
|
|
56
|
+
await result;
|
|
57
|
+
log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
|
|
58
|
+
}
|
|
59
|
+
async start(port, host = "127.0.0.1") {
|
|
60
|
+
if (port === 0) {
|
|
61
|
+
await this._startServer(port, host);
|
|
62
|
+
return this.address();
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < 20; ++i) {
|
|
65
|
+
const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
|
|
66
|
+
if (!err)
|
|
67
|
+
return this.address();
|
|
68
|
+
if (err.code !== "EADDRINUSE")
|
|
69
|
+
throw err;
|
|
70
|
+
log("Port %d is busy (EADDRINUSE). Trying next port...", port);
|
|
71
|
+
port = port + 1;
|
|
72
|
+
if (port > 65535)
|
|
73
|
+
port = 4e3;
|
|
74
|
+
}
|
|
75
|
+
log("All sequential ports busy. Falling back to random port.");
|
|
76
|
+
await this._startServer(0, host);
|
|
77
|
+
return this.address();
|
|
78
|
+
}
|
|
79
|
+
stop() {
|
|
80
|
+
return new Promise((resolve2, reject) => {
|
|
81
|
+
this._server.close((err) => {
|
|
82
|
+
if (err) {
|
|
83
|
+
log("Error stopping server: %o", err);
|
|
84
|
+
reject(err);
|
|
85
|
+
} else {
|
|
86
|
+
log("Server stopped.");
|
|
87
|
+
resolve2();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
_errorResponse(req, res, code, text) {
|
|
93
|
+
res.writeHead(code, { "Content-Type": "text/plain" });
|
|
94
|
+
res.end(text);
|
|
95
|
+
log(`[${code}] ${req.method} ${req.url}`);
|
|
96
|
+
}
|
|
97
|
+
_handleRequest(req, res) {
|
|
98
|
+
const { url, method } = req;
|
|
99
|
+
if (this._cors) {
|
|
100
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
101
|
+
res.setHeader("Access-Control-Allow-Origin", this._cors);
|
|
102
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
103
|
+
if (req.method === "OPTIONS") {
|
|
104
|
+
res.writeHead(204);
|
|
105
|
+
res.end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (method !== "GET") {
|
|
110
|
+
this._errorResponse(req, res, 405, "Method Not Allowed");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
|
|
114
|
+
res.on("close", () => {
|
|
115
|
+
if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
|
|
116
|
+
});
|
|
117
|
+
if (!url || !url.startsWith(this._pathPrefix)) {
|
|
118
|
+
this._errorResponse(req, res, 404, "Not Found");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const relativePath = url.slice(this._pathPrefix.length);
|
|
122
|
+
const safeSuffix = path.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
|
|
123
|
+
const filePath = path.join(this._absoluteFolderPath, safeSuffix);
|
|
124
|
+
if (!filePath.startsWith(this._absoluteFolderPath)) {
|
|
125
|
+
this._errorResponse(req, res, 403, "Forbidden");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
fs.stat(filePath, (err, stats) => {
|
|
129
|
+
if (err || !stats.isFile()) {
|
|
130
|
+
this._errorResponse(req, res, 404, "File Not Found");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
134
|
+
const contentType = this._mimeTypes[ext] || "application/octet-stream";
|
|
135
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
136
|
+
log(`[200] ${req.method} ${req.url} -> ${filePath}`);
|
|
137
|
+
const readStream = fs.createReadStream(filePath);
|
|
138
|
+
readStream.pipe(res);
|
|
139
|
+
readStream.on("error", (err2) => {
|
|
140
|
+
log("Stream error: %o", err2);
|
|
141
|
+
res.end();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
export {
|
|
147
|
+
StaticServer
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=staticServer.js.map
|