@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.
@@ -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 fs7 from "fs";
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/flakinessConfig.ts
14
- import fs2 from "fs";
15
- import path2 from "path";
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 fs from "fs";
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 fs.promises.stat(aPath).then(() => true).catch((e) => false);
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 = fs.createReadStream(filePath);
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 fs.promises.rm(outputFolder, { recursive: true, force: true });
146
- await fs.promises.mkdir(outputFolder, { recursive: true });
147
- await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
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 fs.promises.mkdir(attachmentsFolder);
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 fs.promises.cp(attachment.path, attachmentPath);
183
+ await fs2.promises.cp(attachment.path, attachmentPath);
155
184
  else if (attachment.body)
156
- await fs.promises.writeFile(attachmentPath, attachment.body);
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 = fs.readFileSync("/etc/os-release", "utf-8");
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 fs.promises.readdir(dir, { withFileTypes: true });
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/flakinessConfig.ts
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 (fs2.existsSync(configPath))
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 FlakinessConfig = class _FlakinessConfig {
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 fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
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 _FlakinessConfig(configPath, json);
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 _FlakinessConfig(ensureConfigPath(), {});
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 fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
378
- await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
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 fs4 from "fs";
460
- import path4 from "path";
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 fs4.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
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(path4.dirname(this._options.reportPath), report.commitId, 100);
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 fs4.promises.readFile(idx.path);
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/cli/cmd-show-report.ts
639
- async function cmdShowReport(reportFolder) {
640
- const reportPath = path5.join(reportFolder, "report.json");
641
- const session = await FlakinessSession.load();
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 project = projectPublicId && session ? await session.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0) : void 0;
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 reportEndpoint = project ? `${endpoint}/localreport/${project.org.orgSlug}/${project.projectSlug}?port=${server.port()}&token=${server.authToken()}` : `${endpoint}/localreport?port=${server.port()}&token=${server.authToken()}`;
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 ${reportEndpoint}
765
+ Serving Flakiness report at ${url.toString()}
655
766
  Press Ctrl+C to quit.`));
656
- await open(reportEndpoint);
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 os3 from "os";
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 os3.cpus()) {
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: os3.platform() === "darwin" ? getAvailableMemMacOS() : os3.freemem()
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 / os3.totalmem()) * 1e4) / 100;
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: os3.totalmem()
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 = path6.join(process.cwd(), this._options.outputFolder ?? process.env.FLAKINESS_OUTPUT_DIR ?? "flakiness-report");
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 = path6.resolve(pwStep.location.file);
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 = fs7.realpathSync(process.argv[1]);
1050
- while (path6.basename(playwrightPath) !== "test")
1051
- playwrightPath = path6.dirname(playwrightPath);
1052
- const module = await import(path6.join(playwrightPath, "index.js"));
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 cmdShowReport(this._outputFolder);
1066
+ await showReport(this._outputFolder);
1118
1067
  } else {
1119
- const defaultOutputFolder = path6.join(process.cwd(), "flakiness-report");
1120
- const folder = defaultOutputFolder === this._outputFolder ? "" : path6.relative(process.cwd(), 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