@flakiness/sdk 0.145.0 → 0.146.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/{flakinessConfig.js → flakinessProjectConfig.js} +10 -17
- package/lib/playwright-test.js +243 -294
- package/lib/{cli/cmd-show-report.js → showReport.js} +37 -106
- package/package.json +14 -17
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cli.js +0 -2382
- package/lib/cli/cmd-convert.js +0 -421
- package/lib/cli/cmd-download.js +0 -42
- package/lib/cli/cmd-link.js +0 -207
- package/lib/cli/cmd-login.js +0 -223
- package/lib/cli/cmd-logout.js +0 -170
- package/lib/cli/cmd-status.js +0 -273
- package/lib/cli/cmd-unlink.js +0 -199
- package/lib/cli/cmd-upload-playwright-json.js +0 -614
- package/lib/cli/cmd-upload.js +0 -321
- package/lib/cli/cmd-whoami.js +0 -173
- package/lib/flakinessSession.js +0 -159
- package/lib/junit.js +0 -310
- package/lib/playwrightJSONReport.js +0 -430
package/lib/playwright-test.js
CHANGED
|
@@ -2,30 +2,59 @@
|
|
|
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 path6 from "path";
|
|
7
|
-
|
|
8
|
-
// src/cli/cmd-show-report.ts
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import open from "open";
|
|
5
|
+
import fs6 from "fs";
|
|
11
6
|
import path5 from "path";
|
|
12
7
|
|
|
13
|
-
// src/
|
|
14
|
-
import
|
|
15
|
-
import
|
|
8
|
+
// src/createTestStepSnippets.ts
|
|
9
|
+
import { codeFrameColumns } from "@babel/code-frame";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
function createTestStepSnippets(filepathToSteps) {
|
|
12
|
+
for (const [filepath, steps] of filepathToSteps) {
|
|
13
|
+
let source;
|
|
14
|
+
try {
|
|
15
|
+
source = fs.readFileSync(filepath, "utf-8");
|
|
16
|
+
} catch (e) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const lines = source.split("\n").length;
|
|
20
|
+
const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
|
|
21
|
+
const highlightedLines = highlighted.split("\n");
|
|
22
|
+
const lineWithArrow = highlightedLines[highlightedLines.length - 1];
|
|
23
|
+
for (const step of steps) {
|
|
24
|
+
if (!step.location)
|
|
25
|
+
continue;
|
|
26
|
+
if (step.location.line < 2 || step.location.line >= lines)
|
|
27
|
+
continue;
|
|
28
|
+
const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
|
|
29
|
+
const index = lineWithArrow.indexOf("^");
|
|
30
|
+
const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
|
|
31
|
+
snippetLines.splice(2, 0, shiftedArrow);
|
|
32
|
+
step.snippet = snippetLines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/reportUploader.ts
|
|
38
|
+
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
39
|
+
import assert2 from "assert";
|
|
40
|
+
import fs3 from "fs";
|
|
41
|
+
import { URL as URL2 } from "url";
|
|
42
|
+
|
|
43
|
+
// src/serverapi.ts
|
|
44
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
16
45
|
|
|
17
46
|
// src/utils.ts
|
|
18
47
|
import { ReportUtils } from "@flakiness/report";
|
|
19
48
|
import assert from "assert";
|
|
20
49
|
import { spawnSync } from "child_process";
|
|
21
50
|
import crypto from "crypto";
|
|
22
|
-
import
|
|
51
|
+
import fs2 from "fs";
|
|
23
52
|
import http from "http";
|
|
24
53
|
import https from "https";
|
|
25
54
|
import os from "os";
|
|
26
55
|
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
27
56
|
async function existsAsync(aPath) {
|
|
28
|
-
return
|
|
57
|
+
return fs2.promises.stat(aPath).then(() => true).catch((e) => false);
|
|
29
58
|
}
|
|
30
59
|
function extractEnvConfiguration() {
|
|
31
60
|
const ENV_PREFIX = "FK_ENV_";
|
|
@@ -36,7 +65,7 @@ function extractEnvConfiguration() {
|
|
|
36
65
|
function sha1File(filePath) {
|
|
37
66
|
return new Promise((resolve, reject) => {
|
|
38
67
|
const hash = crypto.createHash("sha1");
|
|
39
|
-
const stream =
|
|
68
|
+
const stream = fs2.createReadStream(filePath);
|
|
40
69
|
stream.on("data", (chunk) => {
|
|
41
70
|
hash.update(chunk);
|
|
42
71
|
});
|
|
@@ -142,18 +171,18 @@ function stripAnsi(str) {
|
|
|
142
171
|
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
143
172
|
const reportPath = path.join(outputFolder, "report.json");
|
|
144
173
|
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
145
|
-
await
|
|
146
|
-
await
|
|
147
|
-
await
|
|
174
|
+
await fs2.promises.rm(outputFolder, { recursive: true, force: true });
|
|
175
|
+
await fs2.promises.mkdir(outputFolder, { recursive: true });
|
|
176
|
+
await fs2.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
148
177
|
if (attachments.length)
|
|
149
|
-
await
|
|
178
|
+
await fs2.promises.mkdir(attachmentsFolder);
|
|
150
179
|
const movedAttachments = [];
|
|
151
180
|
for (const attachment of attachments) {
|
|
152
181
|
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
153
182
|
if (attachment.path)
|
|
154
|
-
await
|
|
183
|
+
await fs2.promises.cp(attachment.path, attachmentPath);
|
|
155
184
|
else if (attachment.body)
|
|
156
|
-
await
|
|
185
|
+
await fs2.promises.writeFile(attachmentPath, attachment.body);
|
|
157
186
|
movedAttachments.push({
|
|
158
187
|
contentType: attachment.contentType,
|
|
159
188
|
id: attachment.id,
|
|
@@ -175,7 +204,7 @@ function shell(command, args, options) {
|
|
|
175
204
|
}
|
|
176
205
|
}
|
|
177
206
|
function readLinuxOSRelease() {
|
|
178
|
-
const osReleaseText =
|
|
207
|
+
const osReleaseText = fs2.readFileSync("/etc/os-release", "utf-8");
|
|
179
208
|
return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
180
209
|
line = line.trim();
|
|
181
210
|
let [key, value] = line.split("=");
|
|
@@ -247,7 +276,7 @@ async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
|
247
276
|
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
248
277
|
}
|
|
249
278
|
async function listFilesRecursively(dir, result = []) {
|
|
250
|
-
const entries = await
|
|
279
|
+
const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
251
280
|
for (const entry of entries) {
|
|
252
281
|
const fullPath = path.join(dir, entry.name);
|
|
253
282
|
if (entry.isDirectory())
|
|
@@ -317,7 +346,158 @@ function createEnvironments(projects) {
|
|
|
317
346
|
return result;
|
|
318
347
|
}
|
|
319
348
|
|
|
320
|
-
// src/
|
|
349
|
+
// src/serverapi.ts
|
|
350
|
+
function createServerAPI(endpoint, options) {
|
|
351
|
+
endpoint += "/api/";
|
|
352
|
+
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
353
|
+
...init,
|
|
354
|
+
headers: {
|
|
355
|
+
...init.headers,
|
|
356
|
+
"Authorization": `Bearer ${options.auth}`
|
|
357
|
+
}
|
|
358
|
+
}) : fetch;
|
|
359
|
+
if (options?.retries)
|
|
360
|
+
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
361
|
+
return TypedHTTP.createClient(endpoint, fetcher);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/reportUploader.ts
|
|
365
|
+
var ReportUploader = class _ReportUploader {
|
|
366
|
+
static optionsFromEnv(overrides) {
|
|
367
|
+
const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
368
|
+
if (!flakinessAccessToken)
|
|
369
|
+
return void 0;
|
|
370
|
+
const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
|
|
371
|
+
return { flakinessAccessToken, flakinessEndpoint };
|
|
372
|
+
}
|
|
373
|
+
static async upload(options) {
|
|
374
|
+
const uploaderOptions = _ReportUploader.optionsFromEnv(options);
|
|
375
|
+
if (!uploaderOptions) {
|
|
376
|
+
if (process.env.CI)
|
|
377
|
+
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
378
|
+
return void 0;
|
|
379
|
+
}
|
|
380
|
+
const uploader = new _ReportUploader(uploaderOptions);
|
|
381
|
+
const upload = uploader.createUpload(options.report, options.attachments);
|
|
382
|
+
const uploadResult = await upload.upload();
|
|
383
|
+
if (!uploadResult.success) {
|
|
384
|
+
options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
|
|
385
|
+
return { errorMessage: uploadResult.message };
|
|
386
|
+
}
|
|
387
|
+
options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
|
|
388
|
+
if (uploadResult.reportUrl)
|
|
389
|
+
options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
|
|
390
|
+
}
|
|
391
|
+
_options;
|
|
392
|
+
constructor(options) {
|
|
393
|
+
this._options = options;
|
|
394
|
+
}
|
|
395
|
+
createUpload(report, attachments) {
|
|
396
|
+
const upload = new ReportUpload(this._options, report, attachments);
|
|
397
|
+
return upload;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
|
|
401
|
+
var ReportUpload = class {
|
|
402
|
+
_report;
|
|
403
|
+
_attachments;
|
|
404
|
+
_options;
|
|
405
|
+
_api;
|
|
406
|
+
constructor(options, report, attachments) {
|
|
407
|
+
this._options = options;
|
|
408
|
+
this._report = report;
|
|
409
|
+
this._attachments = attachments;
|
|
410
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
411
|
+
}
|
|
412
|
+
async upload(options) {
|
|
413
|
+
const response = await this._api.run.startUpload.POST({
|
|
414
|
+
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
415
|
+
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
416
|
+
if (response?.error || !response.result)
|
|
417
|
+
return { success: false, message: `flakiness.io returned error: ${response.error.message}` };
|
|
418
|
+
await Promise.all([
|
|
419
|
+
this._uploadReport(JSON.stringify(this._report), response.result.report_upload_url, options?.syncCompression ?? false),
|
|
420
|
+
...this._attachments.map((attachment) => {
|
|
421
|
+
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
422
|
+
if (!uploadURL)
|
|
423
|
+
throw new Error("Internal error: missing upload URL for attachment!");
|
|
424
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
425
|
+
})
|
|
426
|
+
]);
|
|
427
|
+
const response2 = await this._api.run.completeUpload.POST({
|
|
428
|
+
upload_token: response.result.upload_token
|
|
429
|
+
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
430
|
+
const url = response2?.result?.report_url ? new URL2(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
431
|
+
return { success: true, reportUrl: url };
|
|
432
|
+
}
|
|
433
|
+
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
434
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
435
|
+
const headers = {
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
438
|
+
"Content-Encoding": "br"
|
|
439
|
+
};
|
|
440
|
+
await retryWithBackoff(async () => {
|
|
441
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
442
|
+
url: uploadUrl,
|
|
443
|
+
headers,
|
|
444
|
+
method: "put"
|
|
445
|
+
});
|
|
446
|
+
request.write(compressed);
|
|
447
|
+
request.end();
|
|
448
|
+
await responseDataPromise;
|
|
449
|
+
}, HTTP_BACKOFF);
|
|
450
|
+
}
|
|
451
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
452
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
453
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
454
|
+
if (!compressable && attachment.path) {
|
|
455
|
+
const attachmentPath = attachment.path;
|
|
456
|
+
await retryWithBackoff(async () => {
|
|
457
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
458
|
+
url: uploadUrl,
|
|
459
|
+
headers: {
|
|
460
|
+
"Content-Type": attachment.contentType,
|
|
461
|
+
"Content-Length": (await fs3.promises.stat(attachmentPath)).size + ""
|
|
462
|
+
},
|
|
463
|
+
method: "put"
|
|
464
|
+
});
|
|
465
|
+
fs3.createReadStream(attachmentPath).pipe(request);
|
|
466
|
+
await responseDataPromise;
|
|
467
|
+
}, HTTP_BACKOFF);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs3.promises.readFile(attachment.path) : void 0;
|
|
471
|
+
assert2(buffer);
|
|
472
|
+
const encoding = compressable ? "br" : void 0;
|
|
473
|
+
if (compressable)
|
|
474
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
475
|
+
const headers = {
|
|
476
|
+
"Content-Type": attachment.contentType,
|
|
477
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
478
|
+
"Content-Encoding": encoding
|
|
479
|
+
};
|
|
480
|
+
await retryWithBackoff(async () => {
|
|
481
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
482
|
+
url: uploadUrl,
|
|
483
|
+
headers,
|
|
484
|
+
method: "put"
|
|
485
|
+
});
|
|
486
|
+
request.write(buffer);
|
|
487
|
+
request.end();
|
|
488
|
+
await responseDataPromise;
|
|
489
|
+
}, HTTP_BACKOFF);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/showReport.ts
|
|
494
|
+
import chalk from "chalk";
|
|
495
|
+
import open from "open";
|
|
496
|
+
import path4 from "path";
|
|
497
|
+
|
|
498
|
+
// src/flakinessProjectConfig.ts
|
|
499
|
+
import fs4 from "fs";
|
|
500
|
+
import path2 from "path";
|
|
321
501
|
function createConfigPath(dir) {
|
|
322
502
|
return path2.join(dir, ".flakiness", "config.json");
|
|
323
503
|
}
|
|
@@ -330,7 +510,7 @@ function ensureConfigPath() {
|
|
|
330
510
|
function computeConfigPath() {
|
|
331
511
|
for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
|
|
332
512
|
const configPath = createConfigPath(p);
|
|
333
|
-
if (
|
|
513
|
+
if (fs4.existsSync(configPath))
|
|
334
514
|
return configPath;
|
|
335
515
|
}
|
|
336
516
|
try {
|
|
@@ -340,29 +520,19 @@ function computeConfigPath() {
|
|
|
340
520
|
return createConfigPath(process.cwd());
|
|
341
521
|
}
|
|
342
522
|
}
|
|
343
|
-
var
|
|
523
|
+
var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
344
524
|
constructor(_configPath, _config) {
|
|
345
525
|
this._configPath = _configPath;
|
|
346
526
|
this._config = _config;
|
|
347
527
|
}
|
|
348
528
|
static async load() {
|
|
349
529
|
const configPath = ensureConfigPath();
|
|
350
|
-
const data = await
|
|
530
|
+
const data = await fs4.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
351
531
|
const json = data ? JSON.parse(data) : {};
|
|
352
|
-
return new
|
|
353
|
-
}
|
|
354
|
-
static async projectOrDie(session) {
|
|
355
|
-
const config = await _FlakinessConfig.load();
|
|
356
|
-
const projectPublicId = config.projectPublicId();
|
|
357
|
-
if (!projectPublicId)
|
|
358
|
-
throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
|
|
359
|
-
const project = await session.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
|
|
360
|
-
if (!project)
|
|
361
|
-
throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
|
|
362
|
-
return project;
|
|
532
|
+
return new _FlakinessProjectConfig(configPath, json);
|
|
363
533
|
}
|
|
364
534
|
static createEmpty() {
|
|
365
|
-
return new
|
|
535
|
+
return new _FlakinessProjectConfig(ensureConfigPath(), {});
|
|
366
536
|
}
|
|
367
537
|
path() {
|
|
368
538
|
return this._configPath;
|
|
@@ -370,76 +540,15 @@ var FlakinessConfig = class _FlakinessConfig {
|
|
|
370
540
|
projectPublicId() {
|
|
371
541
|
return this._config.projectPublicId;
|
|
372
542
|
}
|
|
543
|
+
reportViewerEndpoint() {
|
|
544
|
+
return this._config.customReportViewerEndpoint ?? "https://report.flakiness.io";
|
|
545
|
+
}
|
|
373
546
|
setProjectPublicId(projectId) {
|
|
374
547
|
this._config.projectPublicId = projectId;
|
|
375
548
|
}
|
|
376
549
|
async save() {
|
|
377
|
-
await
|
|
378
|
-
await
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// src/flakinessSession.ts
|
|
383
|
-
import fs3 from "fs/promises";
|
|
384
|
-
import os2 from "os";
|
|
385
|
-
import path3 from "path";
|
|
386
|
-
|
|
387
|
-
// src/serverapi.ts
|
|
388
|
-
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
389
|
-
function createServerAPI(endpoint, options) {
|
|
390
|
-
endpoint += "/api/";
|
|
391
|
-
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
392
|
-
...init,
|
|
393
|
-
headers: {
|
|
394
|
-
...init.headers,
|
|
395
|
-
"Authorization": `Bearer ${options.auth}`
|
|
396
|
-
}
|
|
397
|
-
}) : fetch;
|
|
398
|
-
if (options?.retries)
|
|
399
|
-
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
400
|
-
return TypedHTTP.createClient(endpoint, fetcher);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// src/flakinessSession.ts
|
|
404
|
-
var CONFIG_DIR = (() => {
|
|
405
|
-
const configDir = process.platform === "darwin" ? path3.join(os2.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path3.join(os2.homedir(), "AppData", "Roaming", "flakiness") : path3.join(os2.homedir(), ".config", "flakiness");
|
|
406
|
-
return configDir;
|
|
407
|
-
})();
|
|
408
|
-
var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
|
|
409
|
-
var FlakinessSession = class _FlakinessSession {
|
|
410
|
-
constructor(_config) {
|
|
411
|
-
this._config = _config;
|
|
412
|
-
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
413
|
-
}
|
|
414
|
-
static async loadOrDie() {
|
|
415
|
-
const session = await _FlakinessSession.load();
|
|
416
|
-
if (!session)
|
|
417
|
-
throw new Error(`Please login first with 'npx flakiness login'`);
|
|
418
|
-
return session;
|
|
419
|
-
}
|
|
420
|
-
static async load() {
|
|
421
|
-
const data = await fs3.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
422
|
-
if (!data)
|
|
423
|
-
return void 0;
|
|
424
|
-
const json = JSON.parse(data);
|
|
425
|
-
return new _FlakinessSession(json);
|
|
426
|
-
}
|
|
427
|
-
static async remove() {
|
|
428
|
-
await fs3.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
429
|
-
}
|
|
430
|
-
api;
|
|
431
|
-
endpoint() {
|
|
432
|
-
return this._config.endpoint;
|
|
433
|
-
}
|
|
434
|
-
path() {
|
|
435
|
-
return CONFIG_PATH;
|
|
436
|
-
}
|
|
437
|
-
sessionToken() {
|
|
438
|
-
return this._config.token;
|
|
439
|
-
}
|
|
440
|
-
async save() {
|
|
441
|
-
await fs3.mkdir(CONFIG_DIR, { recursive: true });
|
|
442
|
-
await fs3.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
550
|
+
await fs4.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
|
|
551
|
+
await fs4.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
443
552
|
}
|
|
444
553
|
};
|
|
445
554
|
|
|
@@ -456,8 +565,8 @@ import http2 from "http";
|
|
|
456
565
|
|
|
457
566
|
// src/localReportApi.ts
|
|
458
567
|
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
459
|
-
import
|
|
460
|
-
import
|
|
568
|
+
import fs5 from "fs";
|
|
569
|
+
import path3 from "path";
|
|
461
570
|
import { z } from "zod/v4";
|
|
462
571
|
|
|
463
572
|
// src/localGit.ts
|
|
@@ -512,7 +621,7 @@ var ReportInfo = class {
|
|
|
512
621
|
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
513
622
|
commits = [];
|
|
514
623
|
async refresh() {
|
|
515
|
-
const report = await
|
|
624
|
+
const report = await fs5.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
516
625
|
if (!report) {
|
|
517
626
|
this.report = void 0;
|
|
518
627
|
this.commits = [];
|
|
@@ -522,7 +631,7 @@ var ReportInfo = class {
|
|
|
522
631
|
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
523
632
|
return;
|
|
524
633
|
this.report = report;
|
|
525
|
-
this.commits = await listLocalCommits(
|
|
634
|
+
this.commits = await listLocalCommits(path3.dirname(this._options.reportPath), report.commitId, 100);
|
|
526
635
|
const attachmentsDir = this._options.attachmentsFolder;
|
|
527
636
|
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
528
637
|
if (missingAttachments.length) {
|
|
@@ -556,7 +665,7 @@ var localReportRouter = {
|
|
|
556
665
|
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
557
666
|
if (!idx)
|
|
558
667
|
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
559
|
-
const buffer = await
|
|
668
|
+
const buffer = await fs5.promises.readFile(idx.path);
|
|
560
669
|
return TypedHTTP2.ok(buffer, idx.contentType);
|
|
561
670
|
}
|
|
562
671
|
}),
|
|
@@ -635,194 +744,34 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
635
744
|
}
|
|
636
745
|
};
|
|
637
746
|
|
|
638
|
-
// src/
|
|
639
|
-
async function
|
|
640
|
-
const reportPath =
|
|
641
|
-
const
|
|
642
|
-
const config = await FlakinessConfig.load();
|
|
747
|
+
// src/showReport.ts
|
|
748
|
+
async function showReport(reportFolder) {
|
|
749
|
+
const reportPath = path4.join(reportFolder, "report.json");
|
|
750
|
+
const config = await FlakinessProjectConfig.load();
|
|
643
751
|
const projectPublicId = config.projectPublicId();
|
|
644
|
-
const
|
|
645
|
-
const endpoint = session?.endpoint() ?? "https://flakiness.io";
|
|
752
|
+
const reportViewerEndpoint = config.reportViewerEndpoint();
|
|
646
753
|
const server = await LocalReportServer.create({
|
|
647
|
-
endpoint,
|
|
754
|
+
endpoint: reportViewerEndpoint,
|
|
648
755
|
port: 9373,
|
|
649
756
|
reportPath,
|
|
650
757
|
attachmentsFolder: reportFolder
|
|
651
758
|
});
|
|
652
|
-
const
|
|
759
|
+
const url = new URL(reportViewerEndpoint);
|
|
760
|
+
url.searchParams.set("port", String(server.port()));
|
|
761
|
+
url.searchParams.set("token", server.authToken());
|
|
762
|
+
if (projectPublicId)
|
|
763
|
+
url.searchParams.set("ppid", projectPublicId);
|
|
653
764
|
console.log(chalk.cyan(`
|
|
654
|
-
Serving Flakiness report at ${
|
|
765
|
+
Serving Flakiness report at ${url.toString()}
|
|
655
766
|
Press Ctrl+C to quit.`));
|
|
656
|
-
await open(
|
|
767
|
+
await open(url.toString());
|
|
657
768
|
await new Promise(() => {
|
|
658
769
|
});
|
|
659
770
|
}
|
|
660
771
|
|
|
661
|
-
// src/createTestStepSnippets.ts
|
|
662
|
-
import { codeFrameColumns } from "@babel/code-frame";
|
|
663
|
-
import fs5 from "fs";
|
|
664
|
-
function createTestStepSnippets(filepathToSteps) {
|
|
665
|
-
for (const [filepath, steps] of filepathToSteps) {
|
|
666
|
-
let source;
|
|
667
|
-
try {
|
|
668
|
-
source = fs5.readFileSync(filepath, "utf-8");
|
|
669
|
-
} catch (e) {
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
const lines = source.split("\n").length;
|
|
673
|
-
const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
|
|
674
|
-
const highlightedLines = highlighted.split("\n");
|
|
675
|
-
const lineWithArrow = highlightedLines[highlightedLines.length - 1];
|
|
676
|
-
for (const step of steps) {
|
|
677
|
-
if (!step.location)
|
|
678
|
-
continue;
|
|
679
|
-
if (step.location.line < 2 || step.location.line >= lines)
|
|
680
|
-
continue;
|
|
681
|
-
const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
|
|
682
|
-
const index = lineWithArrow.indexOf("^");
|
|
683
|
-
const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
|
|
684
|
-
snippetLines.splice(2, 0, shiftedArrow);
|
|
685
|
-
step.snippet = snippetLines.join("\n");
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// src/reportUploader.ts
|
|
691
|
-
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
692
|
-
import assert2 from "assert";
|
|
693
|
-
import fs6 from "fs";
|
|
694
|
-
import { URL } from "url";
|
|
695
|
-
var ReportUploader = class _ReportUploader {
|
|
696
|
-
static optionsFromEnv(overrides) {
|
|
697
|
-
const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
698
|
-
if (!flakinessAccessToken)
|
|
699
|
-
return void 0;
|
|
700
|
-
const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
|
|
701
|
-
return { flakinessAccessToken, flakinessEndpoint };
|
|
702
|
-
}
|
|
703
|
-
static async upload(options) {
|
|
704
|
-
const uploaderOptions = _ReportUploader.optionsFromEnv(options);
|
|
705
|
-
if (!uploaderOptions) {
|
|
706
|
-
if (process.env.CI)
|
|
707
|
-
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
708
|
-
return void 0;
|
|
709
|
-
}
|
|
710
|
-
const uploader = new _ReportUploader(uploaderOptions);
|
|
711
|
-
const upload = uploader.createUpload(options.report, options.attachments);
|
|
712
|
-
const uploadResult = await upload.upload();
|
|
713
|
-
if (!uploadResult.success) {
|
|
714
|
-
options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
|
|
715
|
-
return { errorMessage: uploadResult.message };
|
|
716
|
-
}
|
|
717
|
-
options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
|
|
718
|
-
if (uploadResult.reportUrl)
|
|
719
|
-
options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
|
|
720
|
-
}
|
|
721
|
-
_options;
|
|
722
|
-
constructor(options) {
|
|
723
|
-
this._options = options;
|
|
724
|
-
}
|
|
725
|
-
createUpload(report, attachments) {
|
|
726
|
-
const upload = new ReportUpload(this._options, report, attachments);
|
|
727
|
-
return upload;
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
|
|
731
|
-
var ReportUpload = class {
|
|
732
|
-
_report;
|
|
733
|
-
_attachments;
|
|
734
|
-
_options;
|
|
735
|
-
_api;
|
|
736
|
-
constructor(options, report, attachments) {
|
|
737
|
-
this._options = options;
|
|
738
|
-
this._report = report;
|
|
739
|
-
this._attachments = attachments;
|
|
740
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
741
|
-
}
|
|
742
|
-
async upload(options) {
|
|
743
|
-
const response = await this._api.run.startUpload.POST({
|
|
744
|
-
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
745
|
-
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
746
|
-
if (response?.error || !response.result)
|
|
747
|
-
return { success: false, message: `flakiness.io returned error: ${response.error.message}` };
|
|
748
|
-
await Promise.all([
|
|
749
|
-
this._uploadReport(JSON.stringify(this._report), response.result.report_upload_url, options?.syncCompression ?? false),
|
|
750
|
-
...this._attachments.map((attachment) => {
|
|
751
|
-
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
752
|
-
if (!uploadURL)
|
|
753
|
-
throw new Error("Internal error: missing upload URL for attachment!");
|
|
754
|
-
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
755
|
-
})
|
|
756
|
-
]);
|
|
757
|
-
const response2 = await this._api.run.completeUpload.POST({
|
|
758
|
-
upload_token: response.result.upload_token
|
|
759
|
-
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
760
|
-
const url = response2?.result?.report_url ? new URL(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
761
|
-
return { success: true, reportUrl: url };
|
|
762
|
-
}
|
|
763
|
-
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
764
|
-
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
765
|
-
const headers = {
|
|
766
|
-
"Content-Type": "application/json",
|
|
767
|
-
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
768
|
-
"Content-Encoding": "br"
|
|
769
|
-
};
|
|
770
|
-
await retryWithBackoff(async () => {
|
|
771
|
-
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
772
|
-
url: uploadUrl,
|
|
773
|
-
headers,
|
|
774
|
-
method: "put"
|
|
775
|
-
});
|
|
776
|
-
request.write(compressed);
|
|
777
|
-
request.end();
|
|
778
|
-
await responseDataPromise;
|
|
779
|
-
}, HTTP_BACKOFF);
|
|
780
|
-
}
|
|
781
|
-
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
782
|
-
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
783
|
-
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
784
|
-
if (!compressable && attachment.path) {
|
|
785
|
-
const attachmentPath = attachment.path;
|
|
786
|
-
await retryWithBackoff(async () => {
|
|
787
|
-
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
788
|
-
url: uploadUrl,
|
|
789
|
-
headers: {
|
|
790
|
-
"Content-Type": attachment.contentType,
|
|
791
|
-
"Content-Length": (await fs6.promises.stat(attachmentPath)).size + ""
|
|
792
|
-
},
|
|
793
|
-
method: "put"
|
|
794
|
-
});
|
|
795
|
-
fs6.createReadStream(attachmentPath).pipe(request);
|
|
796
|
-
await responseDataPromise;
|
|
797
|
-
}, HTTP_BACKOFF);
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
let buffer = attachment.body ? attachment.body : attachment.path ? await fs6.promises.readFile(attachment.path) : void 0;
|
|
801
|
-
assert2(buffer);
|
|
802
|
-
const encoding = compressable ? "br" : void 0;
|
|
803
|
-
if (compressable)
|
|
804
|
-
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
805
|
-
const headers = {
|
|
806
|
-
"Content-Type": attachment.contentType,
|
|
807
|
-
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
808
|
-
"Content-Encoding": encoding
|
|
809
|
-
};
|
|
810
|
-
await retryWithBackoff(async () => {
|
|
811
|
-
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
812
|
-
url: uploadUrl,
|
|
813
|
-
headers,
|
|
814
|
-
method: "put"
|
|
815
|
-
});
|
|
816
|
-
request.write(buffer);
|
|
817
|
-
request.end();
|
|
818
|
-
await responseDataPromise;
|
|
819
|
-
}, HTTP_BACKOFF);
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
|
|
823
772
|
// src/systemUtilizationSampler.ts
|
|
824
773
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
825
|
-
import
|
|
774
|
+
import os2 from "os";
|
|
826
775
|
function getAvailableMemMacOS() {
|
|
827
776
|
const lines = spawnSync2("vm_stat", { encoding: "utf8" }).stdout.trim().split("\n");
|
|
828
777
|
const pageSize = parseInt(lines[0].match(/page size of (\d+) bytes/)[1], 10);
|
|
@@ -843,7 +792,7 @@ function getAvailableMemMacOS() {
|
|
|
843
792
|
function getSystemUtilization() {
|
|
844
793
|
let idleTicks = 0;
|
|
845
794
|
let totalTicks = 0;
|
|
846
|
-
for (const cpu of
|
|
795
|
+
for (const cpu of os2.cpus()) {
|
|
847
796
|
totalTicks += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
|
|
848
797
|
idleTicks += cpu.times.idle;
|
|
849
798
|
}
|
|
@@ -851,14 +800,14 @@ function getSystemUtilization() {
|
|
|
851
800
|
idleTicks,
|
|
852
801
|
totalTicks,
|
|
853
802
|
timestamp: Date.now(),
|
|
854
|
-
freeBytes:
|
|
803
|
+
freeBytes: os2.platform() === "darwin" ? getAvailableMemMacOS() : os2.freemem()
|
|
855
804
|
};
|
|
856
805
|
}
|
|
857
806
|
function toFKUtilization(sample, previous) {
|
|
858
807
|
const idleTicks = sample.idleTicks - previous.idleTicks;
|
|
859
808
|
const totalTicks = sample.totalTicks - previous.totalTicks;
|
|
860
809
|
const cpuUtilization = Math.floor((1 - idleTicks / totalTicks) * 1e4) / 100;
|
|
861
|
-
const memoryUtilization = Math.floor((1 - sample.freeBytes /
|
|
810
|
+
const memoryUtilization = Math.floor((1 - sample.freeBytes / os2.totalmem()) * 1e4) / 100;
|
|
862
811
|
return {
|
|
863
812
|
cpuUtilization,
|
|
864
813
|
memoryUtilization,
|
|
@@ -873,7 +822,7 @@ var SystemUtilizationSampler = class {
|
|
|
873
822
|
this.result = {
|
|
874
823
|
samples: [],
|
|
875
824
|
startTimestamp: this._lastSample.timestamp,
|
|
876
|
-
totalMemoryBytes:
|
|
825
|
+
totalMemoryBytes: os2.totalmem()
|
|
877
826
|
};
|
|
878
827
|
this._timer = setTimeout(this._addSample.bind(this), 50);
|
|
879
828
|
}
|
|
@@ -894,7 +843,7 @@ var err = (txt) => console.error(chalk2.red(`[flakiness.io] ${txt}`));
|
|
|
894
843
|
var FlakinessReporter = class {
|
|
895
844
|
constructor(_options = {}) {
|
|
896
845
|
this._options = _options;
|
|
897
|
-
this._outputFolder =
|
|
846
|
+
this._outputFolder = path5.join(process.cwd(), this._options.outputFolder ?? process.env.FLAKINESS_OUTPUT_DIR ?? "flakiness-report");
|
|
898
847
|
}
|
|
899
848
|
_config;
|
|
900
849
|
_rootSuite;
|
|
@@ -997,7 +946,7 @@ var FlakinessReporter = class {
|
|
|
997
946
|
location: pwStep.location ? this._createLocation(context, pwStep.location) : void 0
|
|
998
947
|
};
|
|
999
948
|
if (pwStep.location) {
|
|
1000
|
-
const resolvedPath =
|
|
949
|
+
const resolvedPath = path5.resolve(pwStep.location.file);
|
|
1001
950
|
this._filepathToSteps.set(resolvedPath, step);
|
|
1002
951
|
}
|
|
1003
952
|
if (pwStep.error)
|
|
@@ -1046,10 +995,10 @@ var FlakinessReporter = class {
|
|
|
1046
995
|
const environmentsMap = createEnvironments(this._config.projects);
|
|
1047
996
|
if (this._options.collectBrowserVersions) {
|
|
1048
997
|
try {
|
|
1049
|
-
let playwrightPath =
|
|
1050
|
-
while (
|
|
1051
|
-
playwrightPath =
|
|
1052
|
-
const module = await import(
|
|
998
|
+
let playwrightPath = fs6.realpathSync(process.argv[1]);
|
|
999
|
+
while (path5.basename(playwrightPath) !== "test")
|
|
1000
|
+
playwrightPath = path5.dirname(playwrightPath);
|
|
1001
|
+
const module = await import(path5.join(playwrightPath, "index.js"));
|
|
1053
1002
|
for (const [project, env] of environmentsMap) {
|
|
1054
1003
|
const { browserName = "chromium", channel, headless } = project.use;
|
|
1055
1004
|
let browserType;
|
|
@@ -1114,10 +1063,10 @@ var FlakinessReporter = class {
|
|
|
1114
1063
|
const openMode = this._options.open ?? "on-failure";
|
|
1115
1064
|
const shouldOpen = process.stdin.isTTY && !process.env.CI && (openMode === "always" || openMode === "on-failure" && this._result?.status === "failed");
|
|
1116
1065
|
if (shouldOpen) {
|
|
1117
|
-
await
|
|
1066
|
+
await showReport(this._outputFolder);
|
|
1118
1067
|
} else {
|
|
1119
|
-
const defaultOutputFolder =
|
|
1120
|
-
const folder = defaultOutputFolder === this._outputFolder ? "" :
|
|
1068
|
+
const defaultOutputFolder = path5.join(process.cwd(), "flakiness-report");
|
|
1069
|
+
const folder = defaultOutputFolder === this._outputFolder ? "" : path5.relative(process.cwd(), this._outputFolder);
|
|
1121
1070
|
console.log(`
|
|
1122
1071
|
To open last Flakiness report, install Flakiness CLI tool and run:
|
|
1123
1072
|
|