@flakiness/sdk 0.146.0 → 0.147.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.
@@ -1,91 +1,49 @@
1
- // src/playwright-test.ts
2
- import { ReportUtils as ReportUtils2 } from "@flakiness/report";
3
- import { Multimap } from "@flakiness/shared/common/multimap.js";
4
- import chalk2 from "chalk";
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/index.ts
8
+ import chalk from "chalk";
5
9
  import fs6 from "fs";
6
- import path5 from "path";
10
+ import open from "open";
11
+ import path3 from "path";
7
12
 
8
- // src/createTestStepSnippets.ts
9
- import { codeFrameColumns } from "@babel/code-frame";
13
+ // src/flakinessProjectConfig.ts
10
14
  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
- }
15
+ import path from "path";
36
16
 
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";
17
+ // src/git.ts
18
+ import assert from "assert";
42
19
 
43
- // src/serverapi.ts
44
- import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
20
+ // src/pathutils.ts
21
+ var pathutils_exports = {};
22
+ __export(pathutils_exports, {
23
+ gitFilePath: () => gitFilePath,
24
+ normalizePath: () => normalizePath
25
+ });
26
+ import { posix as posixPath, win32 as win32Path } from "path";
27
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
28
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
29
+ function normalizePath(aPath) {
30
+ if (IS_WIN32_PATH.test(aPath)) {
31
+ aPath = aPath.split(win32Path.sep).join(posixPath.sep);
32
+ }
33
+ if (IS_ALMOST_POSIX_PATH.test(aPath))
34
+ return "/" + aPath[0] + aPath.substring(2);
35
+ return aPath;
36
+ }
37
+ function gitFilePath(gitRoot, absolutePath) {
38
+ return posixPath.relative(gitRoot, absolutePath);
39
+ }
45
40
 
46
41
  // src/utils.ts
47
- import { ReportUtils } from "@flakiness/report";
48
- import assert from "assert";
49
42
  import { spawnSync } from "child_process";
50
- import crypto from "crypto";
51
- import fs2 from "fs";
52
- import http from "http";
53
- import https from "https";
54
- import os from "os";
55
- import path, { posix as posixPath, win32 as win32Path } from "path";
56
- async function existsAsync(aPath) {
57
- return fs2.promises.stat(aPath).then(() => true).catch((e) => false);
58
- }
59
- function extractEnvConfiguration() {
60
- const ENV_PREFIX = "FK_ENV_";
61
- return Object.fromEntries(
62
- Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
63
- );
64
- }
65
- function sha1File(filePath) {
66
- return new Promise((resolve, reject) => {
67
- const hash = crypto.createHash("sha1");
68
- const stream = fs2.createReadStream(filePath);
69
- stream.on("data", (chunk) => {
70
- hash.update(chunk);
71
- });
72
- stream.on("end", () => {
73
- resolve(hash.digest("hex"));
74
- });
75
- stream.on("error", (err2) => {
76
- reject(err2);
77
- });
78
- });
79
- }
80
43
  var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
81
44
  function errorText(error) {
82
45
  return FLAKINESS_DBG ? error.stack : error.message;
83
46
  }
84
- function sha1Buffer(data) {
85
- const hash = crypto.createHash("sha1");
86
- hash.update(data);
87
- return hash.digest("hex");
88
- }
89
47
  async function retryWithBackoff(job, backoff = []) {
90
48
  for (const timeout of backoff) {
91
49
  try {
@@ -102,95 +60,10 @@ async function retryWithBackoff(job, backoff = []) {
102
60
  }
103
61
  return await job();
104
62
  }
105
- var httpUtils;
106
- ((httpUtils2) => {
107
- function createRequest({ url, method = "get", headers = {} }) {
108
- let resolve;
109
- let reject;
110
- const responseDataPromise = new Promise((a, b) => {
111
- resolve = a;
112
- reject = b;
113
- });
114
- const protocol = url.startsWith("https") ? https : http;
115
- headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
116
- const request = protocol.request(url, { method, headers }, (res) => {
117
- const chunks = [];
118
- res.on("data", (chunk) => chunks.push(chunk));
119
- res.on("end", () => {
120
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
121
- resolve(Buffer.concat(chunks));
122
- else
123
- reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
124
- });
125
- res.on("error", (error) => reject(error));
126
- });
127
- request.on("error", reject);
128
- return { request, responseDataPromise };
129
- }
130
- httpUtils2.createRequest = createRequest;
131
- async function getBuffer(url, backoff) {
132
- return await retryWithBackoff(async () => {
133
- const { request, responseDataPromise } = createRequest({ url });
134
- request.end();
135
- return await responseDataPromise;
136
- }, backoff);
137
- }
138
- httpUtils2.getBuffer = getBuffer;
139
- async function getText(url, backoff) {
140
- const buffer = await getBuffer(url, backoff);
141
- return buffer.toString("utf-8");
142
- }
143
- httpUtils2.getText = getText;
144
- async function getJSON(url) {
145
- return JSON.parse(await getText(url));
146
- }
147
- httpUtils2.getJSON = getJSON;
148
- async function postText(url, text, backoff) {
149
- const headers = {
150
- "Content-Type": "application/json",
151
- "Content-Length": Buffer.byteLength(text) + ""
152
- };
153
- return await retryWithBackoff(async () => {
154
- const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
155
- request.write(text);
156
- request.end();
157
- return await responseDataPromise;
158
- }, backoff);
159
- }
160
- httpUtils2.postText = postText;
161
- async function postJSON(url, json, backoff) {
162
- const buffer = await postText(url, JSON.stringify(json), backoff);
163
- return JSON.parse(buffer.toString("utf-8"));
164
- }
165
- httpUtils2.postJSON = postJSON;
166
- })(httpUtils || (httpUtils = {}));
167
63
  var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
168
64
  function stripAnsi(str) {
169
65
  return str.replace(ansiRegex, "");
170
66
  }
171
- async function saveReportAndAttachments(report, attachments, outputFolder) {
172
- const reportPath = path.join(outputFolder, "report.json");
173
- const attachmentsFolder = path.join(outputFolder, "attachments");
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");
177
- if (attachments.length)
178
- await fs2.promises.mkdir(attachmentsFolder);
179
- const movedAttachments = [];
180
- for (const attachment of attachments) {
181
- const attachmentPath = path.join(attachmentsFolder, attachment.id);
182
- if (attachment.path)
183
- await fs2.promises.cp(attachment.path, attachmentPath);
184
- else if (attachment.body)
185
- await fs2.promises.writeFile(attachmentPath, attachment.body);
186
- movedAttachments.push({
187
- contentType: attachment.contentType,
188
- id: attachment.id,
189
- path: attachmentPath
190
- });
191
- }
192
- return movedAttachments;
193
- }
194
67
  function shell(command, args, options) {
195
68
  try {
196
69
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
@@ -203,47 +76,8 @@ function shell(command, args, options) {
203
76
  return void 0;
204
77
  }
205
78
  }
206
- function readLinuxOSRelease() {
207
- const osReleaseText = fs2.readFileSync("/etc/os-release", "utf-8");
208
- return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
209
- line = line.trim();
210
- let [key, value] = line.split("=");
211
- if (value.startsWith('"') && value.endsWith('"'))
212
- value = value.substring(1, value.length - 1);
213
- return [key, value];
214
- }));
215
- }
216
- function osLinuxInfo() {
217
- const arch = shell(`uname`, [`-m`]);
218
- const osReleaseMap = readLinuxOSRelease();
219
- const name = osReleaseMap.get("name") ?? shell(`uname`);
220
- const version = osReleaseMap.get("version_id");
221
- return { name, arch, version };
222
- }
223
- function osDarwinInfo() {
224
- const name = "macos";
225
- const arch = shell(`uname`, [`-m`]);
226
- const version = shell(`sw_vers`, [`-productVersion`]);
227
- return { name, arch, version };
228
- }
229
- function osWinInfo() {
230
- const name = "win";
231
- const arch = process.arch;
232
- const version = os.release();
233
- return { name, arch, version };
234
- }
235
- function getOSInfo() {
236
- if (process.platform === "darwin")
237
- return osDarwinInfo();
238
- if (process.platform === "win32")
239
- return osWinInfo();
240
- return osLinuxInfo();
241
- }
242
- function inferRunUrl() {
243
- if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
244
- return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
245
- return void 0;
246
- }
79
+
80
+ // src/git.ts
247
81
  function gitCommitInfo(gitRepo) {
248
82
  const sha = shell(`git`, ["rev-parse", "HEAD"], {
249
83
  cwd: gitRepo,
@@ -252,40 +86,6 @@ function gitCommitInfo(gitRepo) {
252
86
  assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
253
87
  return sha.trim();
254
88
  }
255
- async function resolveAttachmentPaths(report, attachmentsDir) {
256
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
257
- const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
258
- const attachmentIdToPath = /* @__PURE__ */ new Map();
259
- const missingAttachments = /* @__PURE__ */ new Set();
260
- ReportUtils.visitTests(report, (test) => {
261
- for (const attempt of test.attempts) {
262
- for (const attachment of attempt.attachments ?? []) {
263
- const attachmentPath = filenameToPath.get(attachment.id);
264
- if (!attachmentPath) {
265
- missingAttachments.add(attachment.id);
266
- } else {
267
- attachmentIdToPath.set(attachment.id, {
268
- contentType: attachment.contentType,
269
- id: attachment.id,
270
- path: attachmentPath
271
- });
272
- }
273
- }
274
- }
275
- });
276
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
277
- }
278
- async function listFilesRecursively(dir, result = []) {
279
- const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
280
- for (const entry of entries) {
281
- const fullPath = path.join(dir, entry.name);
282
- if (entry.isDirectory())
283
- await listFilesRecursively(fullPath, result);
284
- else
285
- result.push(fullPath);
286
- }
287
- return result;
288
- }
289
89
  function computeGitRoot(somePathInsideGitRepo) {
290
90
  const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
291
91
  cwd: somePathInsideGitRepo,
@@ -294,224 +94,22 @@ function computeGitRoot(somePathInsideGitRepo) {
294
94
  assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
295
95
  return normalizePath(root);
296
96
  }
297
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
298
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
299
- function normalizePath(aPath) {
300
- if (IS_WIN32_PATH.test(aPath)) {
301
- aPath = aPath.split(win32Path.sep).join(posixPath.sep);
302
- }
303
- if (IS_ALMOST_POSIX_PATH.test(aPath))
304
- return "/" + aPath[0] + aPath.substring(2);
305
- return aPath;
306
- }
307
- function gitFilePath(gitRoot, absolutePath) {
308
- return posixPath.relative(gitRoot, absolutePath);
97
+
98
+ // src/flakinessProjectConfig.ts
99
+ function createConfigPath(dir) {
100
+ return path.join(dir, ".flakiness", "config.json");
309
101
  }
310
- function parseDurationMS(value) {
311
- if (isNaN(value))
312
- throw new Error("Duration cannot be NaN");
313
- if (value < 0)
314
- throw new Error(`Duration cannot be less than 0, found ${value}`);
315
- return value | 0;
102
+ var gConfigPath;
103
+ function ensureConfigPath() {
104
+ if (!gConfigPath)
105
+ gConfigPath = computeConfigPath();
106
+ return gConfigPath;
316
107
  }
317
- function createEnvironments(projects) {
318
- const envConfiguration = extractEnvConfiguration();
319
- const osInfo = getOSInfo();
320
- let uniqueNames = /* @__PURE__ */ new Set();
321
- const result = /* @__PURE__ */ new Map();
322
- for (const project of projects) {
323
- let defaultName = project.name;
324
- if (!defaultName.trim())
325
- defaultName = "anonymous";
326
- let name = defaultName;
327
- for (let i = 2; uniqueNames.has(name); ++i)
328
- name = `${defaultName}-${i}`;
329
- uniqueNames.add(defaultName);
330
- result.set(project, {
331
- name,
332
- systemData: {
333
- osArch: osInfo.arch,
334
- osName: osInfo.name,
335
- osVersion: osInfo.version
336
- },
337
- userSuppliedData: {
338
- ...envConfiguration,
339
- ...project.metadata
340
- },
341
- opaqueData: {
342
- project
343
- }
344
- });
345
- }
346
- return result;
347
- }
348
-
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";
501
- function createConfigPath(dir) {
502
- return path2.join(dir, ".flakiness", "config.json");
503
- }
504
- var gConfigPath;
505
- function ensureConfigPath() {
506
- if (!gConfigPath)
507
- gConfigPath = computeConfigPath();
508
- return gConfigPath;
509
- }
510
- function computeConfigPath() {
511
- for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
512
- const configPath = createConfigPath(p);
513
- if (fs4.existsSync(configPath))
514
- return configPath;
108
+ function computeConfigPath() {
109
+ for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
110
+ const configPath = createConfigPath(p);
111
+ if (fs.existsSync(configPath))
112
+ return configPath;
515
113
  }
516
114
  try {
517
115
  const gitRoot = computeGitRoot(process.cwd());
@@ -527,7 +125,7 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
527
125
  }
528
126
  static async load() {
529
127
  const configPath = ensureConfigPath();
530
- const data = await fs4.promises.readFile(configPath, "utf-8").catch((e) => void 0);
128
+ const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
531
129
  const json = data ? JSON.parse(data) : {};
532
130
  return new _FlakinessProjectConfig(configPath, json);
533
131
  }
@@ -547,31 +145,33 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
547
145
  this._config.projectPublicId = projectId;
548
146
  }
549
147
  async save() {
550
- await fs4.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
551
- await fs4.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
148
+ await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
149
+ await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
552
150
  }
553
151
  };
554
152
 
555
153
  // src/localReportServer.ts
556
- import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
154
+ import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
557
155
  import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
558
156
  import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
559
157
  import bodyParser from "body-parser";
560
158
  import compression from "compression";
561
- import debug from "debug";
159
+ import debug2 from "debug";
562
160
  import express from "express";
563
161
  import "express-async-errors";
564
- import http2 from "http";
162
+ import http from "http";
565
163
 
566
164
  // src/localReportApi.ts
567
- import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
568
- import fs5 from "fs";
569
- import path3 from "path";
165
+ import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
166
+ import fs2 from "fs";
167
+ import path2 from "path";
570
168
  import { z } from "zod/v4";
571
169
 
572
170
  // src/localGit.ts
573
171
  import { exec } from "child_process";
172
+ import debug from "debug";
574
173
  import { promisify } from "util";
174
+ var log = debug("fk:git");
575
175
  var execAsync = promisify(exec);
576
176
  async function listLocalCommits(gitRoot, head, count) {
577
177
  const FIELD_SEPARATOR = "|~|";
@@ -607,11 +207,131 @@ async function listLocalCommits(gitRoot, head, count) {
607
207
  };
608
208
  });
609
209
  } catch (error) {
610
- console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
611
- throw error;
210
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
211
+ return [];
612
212
  }
613
213
  }
614
214
 
215
+ // src/reportUtils.ts
216
+ import { Multimap } from "@flakiness/shared/common/multimap.js";
217
+ import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
218
+ var ReportUtils;
219
+ ((ReportUtils2) => {
220
+ function visitTests(report, testVisitor) {
221
+ function visitSuite(suite, parents) {
222
+ parents.push(suite);
223
+ for (const test of suite.tests ?? [])
224
+ testVisitor(test, parents);
225
+ for (const childSuite of suite.suites ?? [])
226
+ visitSuite(childSuite, parents);
227
+ parents.pop();
228
+ }
229
+ for (const test of report.tests ?? [])
230
+ testVisitor(test, []);
231
+ for (const suite of report.suites)
232
+ visitSuite(suite, []);
233
+ }
234
+ ReportUtils2.visitTests = visitTests;
235
+ function normalizeReport(report) {
236
+ const gEnvs = /* @__PURE__ */ new Map();
237
+ const gSuites = /* @__PURE__ */ new Map();
238
+ const gTests = new Multimap();
239
+ const gSuiteIds = /* @__PURE__ */ new Map();
240
+ const gTestIds = /* @__PURE__ */ new Map();
241
+ const gEnvIds = /* @__PURE__ */ new Map();
242
+ const gSuiteChildren = new Multimap();
243
+ const gSuiteTests = new Multimap();
244
+ for (const env of report.environments) {
245
+ const envId = computeEnvId(env);
246
+ gEnvs.set(envId, env);
247
+ gEnvIds.set(env, envId);
248
+ }
249
+ const usedEnvIds = /* @__PURE__ */ new Set();
250
+ function visitTests2(tests, suiteId) {
251
+ for (const test of tests ?? []) {
252
+ const testId = computeTestId(test, suiteId);
253
+ gTests.set(testId, test);
254
+ gTestIds.set(test, testId);
255
+ gSuiteTests.set(suiteId, test);
256
+ for (const attempt of test.attempts) {
257
+ const env = report.environments[attempt.environmentIdx];
258
+ const envId = gEnvIds.get(env);
259
+ usedEnvIds.add(envId);
260
+ }
261
+ }
262
+ }
263
+ function visitSuite(suite, parentSuiteId) {
264
+ const suiteId = computeSuiteId(suite, parentSuiteId);
265
+ gSuites.set(suiteId, suite);
266
+ gSuiteIds.set(suite, suiteId);
267
+ for (const childSuite of suite.suites ?? []) {
268
+ visitSuite(childSuite, suiteId);
269
+ gSuiteChildren.set(suiteId, childSuite);
270
+ }
271
+ visitTests2(suite.tests ?? [], suiteId);
272
+ }
273
+ function transformTests(tests) {
274
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
275
+ return [...testIds].map((testId) => {
276
+ const tests2 = gTests.getAll(testId);
277
+ const tags = tests2.map((test) => test.tags ?? []).flat();
278
+ return {
279
+ location: tests2[0].location,
280
+ title: tests2[0].title,
281
+ tags: tags.length ? tags : void 0,
282
+ attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
283
+ ...attempt,
284
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
285
+ }))
286
+ };
287
+ });
288
+ }
289
+ function transformSuites(suites) {
290
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
291
+ return [...suiteIds].map((suiteId) => {
292
+ const suite = gSuites.get(suiteId);
293
+ return {
294
+ location: suite.location,
295
+ title: suite.title,
296
+ type: suite.type,
297
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
298
+ tests: transformTests(gSuiteTests.getAll(suiteId))
299
+ };
300
+ });
301
+ }
302
+ visitTests2(report.tests ?? [], "suiteless");
303
+ for (const suite of report.suites)
304
+ visitSuite(suite);
305
+ const newEnvironments = [...usedEnvIds];
306
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
307
+ return {
308
+ ...report,
309
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
310
+ suites: transformSuites(report.suites),
311
+ tests: transformTests(report.tests ?? [])
312
+ };
313
+ }
314
+ ReportUtils2.normalizeReport = normalizeReport;
315
+ function computeEnvId(env) {
316
+ return xxHashObject(env);
317
+ }
318
+ function computeSuiteId(suite, parentSuiteId) {
319
+ return xxHash([
320
+ parentSuiteId ?? "",
321
+ suite.type,
322
+ suite.location?.file ?? "",
323
+ suite.title
324
+ ]);
325
+ }
326
+ function computeTestId(test, suiteId) {
327
+ return xxHash([
328
+ suiteId,
329
+ test.location?.file ?? "",
330
+ test.title
331
+ ]);
332
+ }
333
+ })(ReportUtils || (ReportUtils = {}));
334
+
615
335
  // src/localReportApi.ts
616
336
  var ReportInfo = class {
617
337
  constructor(_options) {
@@ -621,7 +341,7 @@ var ReportInfo = class {
621
341
  attachmentIdToPath = /* @__PURE__ */ new Map();
622
342
  commits = [];
623
343
  async refresh() {
624
- const report = await fs5.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
344
+ const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
625
345
  if (!report) {
626
346
  this.report = void 0;
627
347
  this.commits = [];
@@ -631,7 +351,7 @@ var ReportInfo = class {
631
351
  if (JSON.stringify(report) === JSON.stringify(this.report))
632
352
  return;
633
353
  this.report = report;
634
- this.commits = await listLocalCommits(path3.dirname(this._options.reportPath), report.commitId, 100);
354
+ this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
635
355
  const attachmentsDir = this._options.attachmentsFolder;
636
356
  const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
637
357
  if (missingAttachments.length) {
@@ -644,7 +364,7 @@ var ReportInfo = class {
644
364
  this.attachmentIdToPath = attachmentIdToPath;
645
365
  }
646
366
  };
647
- var t = TypedHTTP2.Router.create();
367
+ var t = TypedHTTP.Router.create();
648
368
  var localReportRouter = {
649
369
  ping: t.get({
650
370
  handler: async () => {
@@ -655,125 +375,513 @@ var localReportRouter = {
655
375
  handler: async ({ ctx }) => {
656
376
  return ctx.reportInfo.commits;
657
377
  }
658
- }),
659
- report: {
660
- attachment: t.rawMethod("GET", {
661
- input: z.object({
662
- attachmentId: z.string().min(1).max(100).transform((id) => id)
663
- }),
664
- handler: async ({ ctx, input }) => {
665
- const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
666
- if (!idx)
667
- throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
668
- const buffer = await fs5.promises.readFile(idx.path);
669
- return TypedHTTP2.ok(buffer, idx.contentType);
670
- }
671
- }),
672
- json: t.get({
673
- handler: async ({ ctx }) => {
674
- await ctx.reportInfo.refresh();
675
- return ctx.reportInfo.report;
676
- }
677
- })
378
+ }),
379
+ report: {
380
+ attachment: t.rawMethod("GET", {
381
+ input: z.object({
382
+ attachmentId: z.string().min(1).max(100).transform((id) => id)
383
+ }),
384
+ handler: async ({ ctx, input }) => {
385
+ const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
386
+ if (!idx)
387
+ throw TypedHTTP.HttpError.withCode("NOT_FOUND");
388
+ const buffer = await fs2.promises.readFile(idx.path);
389
+ return TypedHTTP.ok(buffer, idx.contentType);
390
+ }
391
+ }),
392
+ json: t.get({
393
+ handler: async ({ ctx }) => {
394
+ await ctx.reportInfo.refresh();
395
+ return ctx.reportInfo.report;
396
+ }
397
+ })
398
+ }
399
+ };
400
+ async function resolveAttachmentPaths(report, attachmentsDir) {
401
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
402
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path2.basename(file), file]));
403
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
404
+ const missingAttachments = /* @__PURE__ */ new Set();
405
+ ReportUtils.visitTests(report, (test) => {
406
+ for (const attempt of test.attempts) {
407
+ for (const attachment of attempt.attachments ?? []) {
408
+ const attachmentPath = filenameToPath.get(attachment.id);
409
+ if (!attachmentPath) {
410
+ missingAttachments.add(attachment.id);
411
+ } else {
412
+ attachmentIdToPath.set(attachment.id, {
413
+ contentType: attachment.contentType,
414
+ id: attachment.id,
415
+ path: attachmentPath
416
+ });
417
+ }
418
+ }
419
+ }
420
+ });
421
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
422
+ }
423
+ async function listFilesRecursively(dir, result = []) {
424
+ const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
425
+ for (const entry of entries) {
426
+ const fullPath = path2.join(dir, entry.name);
427
+ if (entry.isDirectory())
428
+ await listFilesRecursively(fullPath, result);
429
+ else
430
+ result.push(fullPath);
431
+ }
432
+ return result;
433
+ }
434
+
435
+ // src/localReportServer.ts
436
+ var logHTTPServer = debug2("fk:http");
437
+ var LocalReportServer = class _LocalReportServer {
438
+ constructor(_server, _port, _authToken) {
439
+ this._server = _server;
440
+ this._port = _port;
441
+ this._authToken = _authToken;
442
+ }
443
+ static async create(options) {
444
+ const app = express();
445
+ app.set("etag", false);
446
+ const authToken = randomUUIDBase62();
447
+ app.use(compression());
448
+ app.use(bodyParser.json({ limit: 256 * 1024 }));
449
+ app.use((req, res, next) => {
450
+ if (!req.path.startsWith("/" + authToken))
451
+ throw TypedHTTP2.HttpError.withCode("UNAUTHORIZED");
452
+ res.setHeader("Access-Control-Allow-Headers", "*");
453
+ res.setHeader("Access-Control-Allow-Origin", options.endpoint);
454
+ res.setHeader("Access-Control-Allow-Methods", "*");
455
+ if (req.method === "OPTIONS") {
456
+ res.writeHead(204);
457
+ res.end();
458
+ return;
459
+ }
460
+ req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
461
+ res.on("close", () => {
462
+ if (!res.headersSent) logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
463
+ });
464
+ next();
465
+ });
466
+ const reportInfo = new ReportInfo(options);
467
+ app.use("/" + authToken, createTypedHttpExpressMiddleware({
468
+ router: localReportRouter,
469
+ createRootContext: async ({ req, res, input }) => ({ reportInfo })
470
+ }));
471
+ app.use((err, req, res, next) => {
472
+ if (err instanceof TypedHTTP2.HttpError)
473
+ return res.status(err.status).send({ error: err.message });
474
+ logHTTPServer(err);
475
+ res.status(500).send({ error: "Internal Server Error" });
476
+ });
477
+ const server = http.createServer(app);
478
+ server.on("error", (err) => {
479
+ if (err.code === "ECONNRESET") {
480
+ logHTTPServer("Client connection reset. Ignoring.");
481
+ return;
482
+ }
483
+ throw err;
484
+ });
485
+ const port = await new Promise((resolve) => server.listen(options.port, () => {
486
+ resolve(server.address().port);
487
+ }));
488
+ return new _LocalReportServer(server, port, authToken);
489
+ }
490
+ authToken() {
491
+ return this._authToken;
492
+ }
493
+ port() {
494
+ return this._port;
495
+ }
496
+ async dispose() {
497
+ await new Promise((x) => this._server.close(x));
498
+ }
499
+ };
500
+
501
+ // src/index.ts
502
+ import { FlakinessReport } from "@flakiness/flakiness-report";
503
+
504
+ // src/createEnvironment.ts
505
+ import { spawnSync as spawnSync2 } from "child_process";
506
+ import fs3 from "fs";
507
+ import os from "os";
508
+ function shell2(command, args, options) {
509
+ try {
510
+ const result = spawnSync2(command, args, { encoding: "utf-8", ...options });
511
+ if (result.status !== 0) {
512
+ return void 0;
513
+ }
514
+ return result.stdout.trim();
515
+ } catch (e) {
516
+ console.error(e);
517
+ return void 0;
518
+ }
519
+ }
520
+ function readLinuxOSRelease() {
521
+ const osReleaseText = fs3.readFileSync("/etc/os-release", "utf-8");
522
+ return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
523
+ line = line.trim();
524
+ let [key, value] = line.split("=");
525
+ if (value.startsWith('"') && value.endsWith('"'))
526
+ value = value.substring(1, value.length - 1);
527
+ return [key, value];
528
+ }));
529
+ }
530
+ function osLinuxInfo() {
531
+ const arch = shell2(`uname`, [`-m`]);
532
+ const osReleaseMap = readLinuxOSRelease();
533
+ const name = osReleaseMap.get("name") ?? shell2(`uname`);
534
+ const version = osReleaseMap.get("version_id");
535
+ return { name, arch, version };
536
+ }
537
+ function osDarwinInfo() {
538
+ const name = "macos";
539
+ const arch = shell2(`uname`, [`-m`]);
540
+ const version = shell2(`sw_vers`, [`-productVersion`]);
541
+ return { name, arch, version };
542
+ }
543
+ function osWinInfo() {
544
+ const name = "win";
545
+ const arch = process.arch;
546
+ const version = os.release();
547
+ return { name, arch, version };
548
+ }
549
+ function getOSInfo() {
550
+ if (process.platform === "darwin")
551
+ return osDarwinInfo();
552
+ if (process.platform === "win32")
553
+ return osWinInfo();
554
+ return osLinuxInfo();
555
+ }
556
+ function extractEnvConfiguration() {
557
+ const ENV_PREFIX = "FK_ENV_";
558
+ return Object.fromEntries(
559
+ Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
560
+ );
561
+ }
562
+ function createEnvironment(options) {
563
+ const osInfo = getOSInfo();
564
+ return {
565
+ name: options.name,
566
+ systemData: {
567
+ osArch: osInfo.arch,
568
+ osName: osInfo.name,
569
+ osVersion: osInfo.version
570
+ },
571
+ userSuppliedData: {
572
+ ...extractEnvConfiguration(),
573
+ ...options.userSuppliedData ?? {}
574
+ },
575
+ opaqueData: options.opaqueData
576
+ };
577
+ }
578
+
579
+ // src/createTestStepSnippets.ts
580
+ import { codeFrameColumns } from "@babel/code-frame";
581
+ import fs4 from "fs";
582
+ import { posix as posixPath2 } from "path";
583
+ function createTestStepSnippetsInplace(report, gitRoot) {
584
+ const allSteps = /* @__PURE__ */ new Map();
585
+ ReportUtils.visitTests(report, (test) => {
586
+ for (const attempt of test.attempts) {
587
+ for (const step of attempt.steps ?? []) {
588
+ if (!step.location)
589
+ continue;
590
+ let fileSteps = allSteps.get(step.location.file);
591
+ if (!fileSteps) {
592
+ fileSteps = /* @__PURE__ */ new Set();
593
+ allSteps.set(step.location.file, fileSteps);
594
+ }
595
+ fileSteps.add(step);
596
+ }
597
+ }
598
+ });
599
+ for (const [gitFilePath2, steps] of allSteps) {
600
+ let source;
601
+ try {
602
+ source = fs4.readFileSync(posixPath2.join(gitRoot, gitFilePath2), "utf-8");
603
+ } catch (e) {
604
+ continue;
605
+ }
606
+ const lines = source.split("\n").length;
607
+ const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
608
+ const highlightedLines = highlighted.split("\n");
609
+ const lineWithArrow = highlightedLines[highlightedLines.length - 1];
610
+ for (const step of steps) {
611
+ if (!step.location)
612
+ continue;
613
+ if (step.location.line < 2 || step.location.line >= lines)
614
+ continue;
615
+ const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
616
+ const index = lineWithArrow.indexOf("^");
617
+ const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
618
+ snippetLines.splice(2, 0, shiftedArrow);
619
+ step.snippet = snippetLines.join("\n");
620
+ }
621
+ }
622
+ }
623
+
624
+ // src/reportUploader.ts
625
+ import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
626
+ import assert2 from "assert";
627
+ import crypto from "crypto";
628
+ import fs5 from "fs";
629
+ import { URL as URL2 } from "url";
630
+
631
+ // src/httpUtils.ts
632
+ import http2 from "http";
633
+ import https from "https";
634
+ var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
635
+ var httpUtils;
636
+ ((httpUtils2) => {
637
+ function createRequest({ url, method = "get", headers = {} }) {
638
+ let resolve;
639
+ let reject;
640
+ const responseDataPromise = new Promise((a, b) => {
641
+ resolve = a;
642
+ reject = b;
643
+ });
644
+ const protocol = url.startsWith("https") ? https : http2;
645
+ headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
646
+ const request = protocol.request(url, { method, headers }, (res) => {
647
+ const chunks = [];
648
+ res.on("data", (chunk) => chunks.push(chunk));
649
+ res.on("end", () => {
650
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
651
+ resolve(Buffer.concat(chunks));
652
+ else
653
+ reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
654
+ });
655
+ res.on("error", (error) => reject(error));
656
+ });
657
+ request.on("error", reject);
658
+ return { request, responseDataPromise };
659
+ }
660
+ httpUtils2.createRequest = createRequest;
661
+ async function getBuffer(url, backoff) {
662
+ return await retryWithBackoff(async () => {
663
+ const { request, responseDataPromise } = createRequest({ url });
664
+ request.end();
665
+ return await responseDataPromise;
666
+ }, backoff);
667
+ }
668
+ httpUtils2.getBuffer = getBuffer;
669
+ async function getText(url, backoff) {
670
+ const buffer = await getBuffer(url, backoff);
671
+ return buffer.toString("utf-8");
672
+ }
673
+ httpUtils2.getText = getText;
674
+ async function getJSON(url) {
675
+ return JSON.parse(await getText(url));
676
+ }
677
+ httpUtils2.getJSON = getJSON;
678
+ async function postText(url, text, backoff) {
679
+ const headers = {
680
+ "Content-Type": "application/json",
681
+ "Content-Length": Buffer.byteLength(text) + ""
682
+ };
683
+ return await retryWithBackoff(async () => {
684
+ const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
685
+ request.write(text);
686
+ request.end();
687
+ return await responseDataPromise;
688
+ }, backoff);
689
+ }
690
+ httpUtils2.postText = postText;
691
+ async function postJSON(url, json, backoff) {
692
+ const buffer = await postText(url, JSON.stringify(json), backoff);
693
+ return JSON.parse(buffer.toString("utf-8"));
694
+ }
695
+ httpUtils2.postJSON = postJSON;
696
+ })(httpUtils || (httpUtils = {}));
697
+
698
+ // src/reportUploader.ts
699
+ function sha1File(filePath) {
700
+ return new Promise((resolve, reject) => {
701
+ const hash = crypto.createHash("sha1");
702
+ const stream = fs5.createReadStream(filePath);
703
+ stream.on("data", (chunk) => {
704
+ hash.update(chunk);
705
+ });
706
+ stream.on("end", () => {
707
+ resolve(hash.digest("hex"));
708
+ });
709
+ stream.on("error", (err) => {
710
+ reject(err);
711
+ });
712
+ });
713
+ }
714
+ async function createFileAttachment(contentType, filePath) {
715
+ return {
716
+ contentType,
717
+ id: await sha1File(filePath),
718
+ path: filePath
719
+ };
720
+ }
721
+ async function createDataAttachment(contentType, data) {
722
+ const hash = crypto.createHash("sha1");
723
+ hash.update(data);
724
+ const id = hash.digest("hex");
725
+ return {
726
+ contentType,
727
+ id,
728
+ body: data
729
+ };
730
+ }
731
+ var ReportUploader = class _ReportUploader {
732
+ static optionsFromEnv(overrides) {
733
+ const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
734
+ if (!flakinessAccessToken)
735
+ return void 0;
736
+ const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
737
+ return { flakinessAccessToken, flakinessEndpoint };
738
+ }
739
+ static async upload(options) {
740
+ const uploaderOptions = _ReportUploader.optionsFromEnv(options);
741
+ if (!uploaderOptions) {
742
+ if (process.env.CI)
743
+ options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
744
+ return void 0;
745
+ }
746
+ const uploader = new _ReportUploader(uploaderOptions);
747
+ const upload = uploader.createUpload(options.report, options.attachments);
748
+ const uploadResult = await upload.upload();
749
+ if (!uploadResult.success) {
750
+ options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
751
+ return { errorMessage: uploadResult.message };
752
+ }
753
+ options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
754
+ if (uploadResult.reportUrl)
755
+ options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
756
+ }
757
+ _options;
758
+ constructor(options) {
759
+ this._options = options;
760
+ }
761
+ createUpload(report, attachments) {
762
+ const upload = new ReportUpload(this._options, report, attachments);
763
+ return upload;
678
764
  }
679
765
  };
680
-
681
- // src/localReportServer.ts
682
- var logHTTPServer = debug("fk:http");
683
- var LocalReportServer = class _LocalReportServer {
684
- constructor(_server, _port, _authToken) {
685
- this._server = _server;
686
- this._port = _port;
687
- this._authToken = _authToken;
766
+ var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
767
+ var ReportUpload = class {
768
+ _report;
769
+ _attachments;
770
+ _options;
771
+ constructor(options, report, attachments) {
772
+ this._options = options;
773
+ this._report = report;
774
+ this._attachments = attachments;
688
775
  }
689
- static async create(options) {
690
- const app = express();
691
- app.set("etag", false);
692
- const authToken = randomUUIDBase62();
693
- app.use(compression());
694
- app.use(bodyParser.json({ limit: 256 * 1024 }));
695
- app.use((req, res, next) => {
696
- if (!req.path.startsWith("/" + authToken))
697
- throw TypedHTTP3.HttpError.withCode("UNAUTHORIZED");
698
- res.setHeader("Access-Control-Allow-Headers", "*");
699
- res.setHeader("Access-Control-Allow-Origin", options.endpoint);
700
- res.setHeader("Access-Control-Allow-Methods", "*");
701
- if (req.method === "OPTIONS") {
702
- res.writeHead(204);
703
- res.end();
704
- return;
705
- }
706
- req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
707
- res.on("close", () => {
708
- if (!res.headersSent) logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
709
- });
710
- next();
711
- });
712
- const reportInfo = new ReportInfo(options);
713
- app.use("/" + authToken, createTypedHttpExpressMiddleware({
714
- router: localReportRouter,
715
- createRootContext: async ({ req, res, input }) => ({ reportInfo })
716
- }));
717
- app.use((err2, req, res, next) => {
718
- if (err2 instanceof TypedHTTP3.HttpError)
719
- return res.status(err2.status).send({ error: err2.message });
720
- logHTTPServer(err2);
721
- res.status(500).send({ error: "Internal Server Error" });
722
- });
723
- const server = http2.createServer(app);
724
- server.on("error", (err2) => {
725
- if (err2.code === "ECONNRESET") {
726
- logHTTPServer("Client connection reset. Ignoring.");
727
- return;
728
- }
729
- throw err2;
730
- });
731
- const port = await new Promise((resolve) => server.listen(options.port, () => {
732
- resolve(server.address().port);
776
+ async _api(pathname, token, body) {
777
+ const url = new URL2(this._options.flakinessEndpoint);
778
+ url.pathname = pathname;
779
+ return await fetch(url, {
780
+ method: "POST",
781
+ headers: {
782
+ "Authorization": `Bearer ${token}`,
783
+ "Content-Type": "application/json"
784
+ },
785
+ body: body ? JSON.stringify(body) : void 0
786
+ }).then(async (response) => !response.ok ? {
787
+ result: void 0,
788
+ error: response.status + " " + url.href + " " + await response.text()
789
+ } : {
790
+ result: await response.json(),
791
+ error: void 0
792
+ }).catch((error) => ({
793
+ result: void 0,
794
+ error
733
795
  }));
734
- return new _LocalReportServer(server, port, authToken);
735
796
  }
736
- authToken() {
737
- return this._authToken;
797
+ async upload(options) {
798
+ const response = await this._api("/api/upload/start", this._options.flakinessAccessToken);
799
+ if (response?.error || !response.result)
800
+ return { success: false, message: response.error };
801
+ const webUrl = new URL2(response.result.webUrl, this._options.flakinessEndpoint).toString();
802
+ const attachmentsPresignedUrls = await this._api("/api/upload/attachments", response.result.uploadToken, {
803
+ attachmentIds: this._attachments.map((a) => a.id)
804
+ });
805
+ if (attachmentsPresignedUrls?.error || !attachmentsPresignedUrls.result)
806
+ return { success: false, message: attachmentsPresignedUrls.error };
807
+ const attachments = new Map(attachmentsPresignedUrls.result.map((a) => [a.attachmentId, a.presignedUrl]));
808
+ await Promise.all([
809
+ this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl, options?.syncCompression ?? false),
810
+ ...this._attachments.map((attachment) => {
811
+ const uploadURL = attachments.get(attachment.id);
812
+ if (!uploadURL)
813
+ throw new Error("Internal error: missing upload URL for attachment!");
814
+ return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
815
+ })
816
+ ]);
817
+ await this._api("/api/upload/finish", response.result.uploadToken);
818
+ return { success: true, reportUrl: webUrl };
738
819
  }
739
- port() {
740
- return this._port;
820
+ async _uploadReport(data, uploadUrl, syncCompression) {
821
+ const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
822
+ const headers = {
823
+ "Content-Type": "application/json",
824
+ "Content-Length": Buffer.byteLength(compressed) + "",
825
+ "Content-Encoding": "br"
826
+ };
827
+ await retryWithBackoff(async () => {
828
+ const { request, responseDataPromise } = httpUtils.createRequest({
829
+ url: uploadUrl,
830
+ headers,
831
+ method: "put"
832
+ });
833
+ request.write(compressed);
834
+ request.end();
835
+ await responseDataPromise;
836
+ }, HTTP_BACKOFF);
741
837
  }
742
- async dispose() {
743
- await new Promise((x) => this._server.close(x));
838
+ async _uploadAttachment(attachment, uploadUrl, syncCompression) {
839
+ const mimeType = attachment.contentType.toLocaleLowerCase().trim();
840
+ const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
841
+ if (!compressable && attachment.path) {
842
+ const attachmentPath = attachment.path;
843
+ await retryWithBackoff(async () => {
844
+ const { request, responseDataPromise } = httpUtils.createRequest({
845
+ url: uploadUrl,
846
+ headers: {
847
+ "Content-Type": attachment.contentType,
848
+ "Content-Length": (await fs5.promises.stat(attachmentPath)).size + ""
849
+ },
850
+ method: "put"
851
+ });
852
+ fs5.createReadStream(attachmentPath).pipe(request);
853
+ await responseDataPromise;
854
+ }, HTTP_BACKOFF);
855
+ return;
856
+ }
857
+ let buffer = attachment.body ? attachment.body : attachment.path ? await fs5.promises.readFile(attachment.path) : void 0;
858
+ assert2(buffer);
859
+ const encoding = compressable ? "br" : void 0;
860
+ if (compressable)
861
+ buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
862
+ const headers = {
863
+ "Content-Type": attachment.contentType,
864
+ "Content-Length": Buffer.byteLength(buffer) + "",
865
+ "Content-Encoding": encoding
866
+ };
867
+ await retryWithBackoff(async () => {
868
+ const { request, responseDataPromise } = httpUtils.createRequest({
869
+ url: uploadUrl,
870
+ headers,
871
+ method: "put"
872
+ });
873
+ request.write(buffer);
874
+ request.end();
875
+ await responseDataPromise;
876
+ }, HTTP_BACKOFF);
744
877
  }
745
878
  };
746
879
 
747
- // src/showReport.ts
748
- async function showReport(reportFolder) {
749
- const reportPath = path4.join(reportFolder, "report.json");
750
- const config = await FlakinessProjectConfig.load();
751
- const projectPublicId = config.projectPublicId();
752
- const reportViewerEndpoint = config.reportViewerEndpoint();
753
- const server = await LocalReportServer.create({
754
- endpoint: reportViewerEndpoint,
755
- port: 9373,
756
- reportPath,
757
- attachmentsFolder: reportFolder
758
- });
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);
764
- console.log(chalk.cyan(`
765
- Serving Flakiness report at ${url.toString()}
766
- Press Ctrl+C to quit.`));
767
- await open(url.toString());
768
- await new Promise(() => {
769
- });
770
- }
771
-
772
880
  // src/systemUtilizationSampler.ts
773
- import { spawnSync as spawnSync2 } from "child_process";
881
+ import { spawnSync as spawnSync3 } from "child_process";
774
882
  import os2 from "os";
775
883
  function getAvailableMemMacOS() {
776
- const lines = spawnSync2("vm_stat", { encoding: "utf8" }).stdout.trim().split("\n");
884
+ const lines = spawnSync3("vm_stat", { encoding: "utf8" }).stdout.trim().split("\n");
777
885
  const pageSize = parseInt(lines[0].match(/page size of (\d+) bytes/)[1], 10);
778
886
  if (isNaN(pageSize)) {
779
887
  console.warn("[flakiness.io] Error detecting macos page size");
@@ -837,250 +945,74 @@ var SystemUtilizationSampler = class {
837
945
  }
838
946
  };
839
947
 
840
- // src/playwright-test.ts
841
- var warn = (txt) => console.warn(chalk2.yellow(`[flakiness.io] ${txt}`));
842
- var err = (txt) => console.error(chalk2.red(`[flakiness.io] ${txt}`));
843
- var FlakinessReporter = class {
844
- constructor(_options = {}) {
845
- this._options = _options;
846
- this._outputFolder = path5.join(process.cwd(), this._options.outputFolder ?? process.env.FLAKINESS_OUTPUT_DIR ?? "flakiness-report");
847
- }
848
- _config;
849
- _rootSuite;
850
- _results = new Multimap();
851
- _unattributedErrors = [];
852
- _filepathToSteps = new Multimap();
853
- _systemUtilizationSampler = new SystemUtilizationSampler();
854
- _report;
855
- _attachments = [];
856
- _outputFolder;
857
- _result;
858
- printsToStdio() {
859
- return false;
860
- }
861
- onBegin(config, suite) {
862
- this._config = config;
863
- this._rootSuite = suite;
864
- }
865
- onError(error) {
866
- this._unattributedErrors.push(error);
867
- }
868
- onTestBegin(test) {
869
- }
870
- onTestEnd(test, result) {
871
- this._results.set(test, result);
872
- }
873
- async _toFKSuites(context, pwSuite) {
874
- const location = pwSuite.location;
875
- if (pwSuite.type === "root" || pwSuite.type === "project" || !location)
876
- return (await Promise.all(pwSuite.suites.map((suite) => this._toFKSuites(context, suite)))).flat();
877
- let type = "suite";
878
- if (pwSuite.type === "file")
879
- type = "file";
880
- else if (pwSuite.type === "describe" && !pwSuite.title)
881
- type = "anonymous suite";
882
- return [{
883
- type,
884
- title: pwSuite.title,
885
- location: this._createLocation(context, location),
886
- suites: (await Promise.all(pwSuite.suites.map((suite) => this._toFKSuites(context, suite)))).flat(),
887
- tests: await Promise.all(pwSuite.tests.map((test) => this._toFKTest(context, test)))
888
- }];
889
- }
890
- async _toFKTest(context, pwTest) {
891
- return {
892
- title: pwTest.title,
893
- // Playwright Test tags must start with '@' so we cut it off.
894
- tags: pwTest.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
895
- location: this._createLocation(context, pwTest.location),
896
- // de-duplication of tests will happen later, so here we will have all attempts.
897
- attempts: await Promise.all(this._results.getAll(pwTest).map((result) => this._toFKRunAttempt(context, pwTest, result)))
898
- };
899
- }
900
- async _toFKRunAttempt(context, pwTest, result) {
901
- const attachments = [];
902
- const attempt = {
903
- timeout: parseDurationMS(pwTest.timeout),
904
- annotations: pwTest.annotations.map((annotation) => ({
905
- type: annotation.type,
906
- description: annotation.description,
907
- location: annotation.location ? this._createLocation(context, annotation.location) : void 0
908
- })),
909
- environmentIdx: context.project2environmentIdx.get(pwTest.parent.project()),
910
- expectedStatus: pwTest.expectedStatus,
911
- parallelIndex: result.parallelIndex,
912
- status: result.status,
913
- errors: result.errors && result.errors.length ? result.errors.map((error) => this._toFKTestError(context, error)) : void 0,
914
- stdout: result.stdout ? result.stdout.map(toSTDIOEntry) : void 0,
915
- stderr: result.stderr ? result.stderr.map(toSTDIOEntry) : void 0,
916
- steps: result.steps ? result.steps.map((jsonTestStep) => this._toFKTestStep(context, jsonTestStep)) : void 0,
917
- startTimestamp: +result.startTime,
918
- duration: +result.duration,
919
- attachments
920
- };
921
- await Promise.all((result.attachments ?? []).map(async (jsonAttachment) => {
922
- if (jsonAttachment.path && !await existsAsync(jsonAttachment.path)) {
923
- context.unaccessibleAttachmentPaths.push(jsonAttachment.path);
924
- return;
925
- }
926
- const id = jsonAttachment.path ? await sha1File(jsonAttachment.path) : sha1Buffer(jsonAttachment.body ?? "");
927
- context.attachments.set(id, {
928
- contentType: jsonAttachment.contentType,
929
- id,
930
- body: jsonAttachment.body,
931
- path: jsonAttachment.path
932
- });
933
- attachments.push({
934
- id,
935
- name: jsonAttachment.name,
936
- contentType: jsonAttachment.contentType
937
- });
938
- }));
939
- return attempt;
940
- }
941
- _toFKTestStep(context, pwStep) {
942
- const step = {
943
- // NOTE: jsonStep.duration was -1 in some playwright versions
944
- duration: parseDurationMS(Math.max(pwStep.duration, 0)),
945
- title: pwStep.title,
946
- location: pwStep.location ? this._createLocation(context, pwStep.location) : void 0
947
- };
948
- if (pwStep.location) {
949
- const resolvedPath = path5.resolve(pwStep.location.file);
950
- this._filepathToSteps.set(resolvedPath, step);
951
- }
952
- if (pwStep.error)
953
- step.error = this._toFKTestError(context, pwStep.error);
954
- if (pwStep.steps)
955
- step.steps = pwStep.steps.map((childJSONStep) => this._toFKTestStep(context, childJSONStep));
956
- return step;
957
- }
958
- _createLocation(context, pwLocation) {
959
- return {
960
- file: gitFilePath(context.gitRoot, normalizePath(pwLocation.file)),
961
- line: pwLocation.line,
962
- column: pwLocation.column
963
- };
964
- }
965
- _toFKTestError(context, pwError) {
966
- return {
967
- location: pwError.location ? this._createLocation(context, pwError.location) : void 0,
968
- message: stripAnsi(pwError.message ?? "").split("\n")[0],
969
- snippet: pwError.snippet,
970
- stack: pwError.stack,
971
- value: pwError.value
972
- };
973
- }
974
- async onEnd(result) {
975
- this._systemUtilizationSampler.dispose();
976
- if (!this._config || !this._rootSuite)
977
- throw new Error("ERROR: failed to resolve config");
978
- let commitId;
979
- try {
980
- commitId = gitCommitInfo(this._config.rootDir);
981
- } catch (e) {
982
- warn(`Failed to fetch commit info - is this a git repo?`);
983
- err(`Report is NOT generated.`);
984
- return;
985
- }
986
- const gitRoot = normalizePath(computeGitRoot(this._config.rootDir));
987
- const configPath = this._config.configFile ? gitFilePath(gitRoot, normalizePath(this._config.configFile)) : void 0;
988
- const context = {
989
- project2environmentIdx: /* @__PURE__ */ new Map(),
990
- testBaseDir: normalizePath(this._config.rootDir),
991
- gitRoot,
992
- attachments: /* @__PURE__ */ new Map(),
993
- unaccessibleAttachmentPaths: []
994
- };
995
- const environmentsMap = createEnvironments(this._config.projects);
996
- if (this._options.collectBrowserVersions) {
997
- try {
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"));
1002
- for (const [project, env] of environmentsMap) {
1003
- const { browserName = "chromium", channel, headless } = project.use;
1004
- let browserType;
1005
- switch (browserName) {
1006
- case "chromium":
1007
- browserType = module.default.chromium;
1008
- break;
1009
- case "firefox":
1010
- browserType = module.default.firefox;
1011
- break;
1012
- case "webkit":
1013
- browserType = module.default.webkit;
1014
- break;
1015
- default:
1016
- throw new Error(`Unsupported browser: ${browserName}`);
1017
- }
1018
- const browser = await browserType.launch({ channel, headless });
1019
- const version = browser.version();
1020
- await browser.close();
1021
- env.userSuppliedData ??= {};
1022
- env.userSuppliedData["browser"] = (channel ?? browserName).toLowerCase().trim() + " " + version;
1023
- }
1024
- } catch (e) {
1025
- err(`Failed to resolve browser version: ${e}`);
1026
- }
1027
- }
1028
- const environments = [...environmentsMap.values()];
1029
- for (let envIdx = 0; envIdx < environments.length; ++envIdx)
1030
- context.project2environmentIdx.set(this._config.projects[envIdx], envIdx);
1031
- const relatedCommitIds = this._options.relatedCommitIds;
1032
- const report = ReportUtils2.dedupeSuitesTestsEnvironments({
1033
- category: "playwright",
1034
- commitId,
1035
- relatedCommitIds,
1036
- systemUtilization: this._systemUtilizationSampler.result,
1037
- configPath,
1038
- url: inferRunUrl(),
1039
- environments,
1040
- suites: await this._toFKSuites(context, this._rootSuite),
1041
- opaqueData: this._config,
1042
- unattributedErrors: this._unattributedErrors.map((e) => this._toFKTestError(context, e)),
1043
- duration: parseDurationMS(result.duration),
1044
- startTimestamp: +result.startTime
1045
- });
1046
- createTestStepSnippets(this._filepathToSteps);
1047
- for (const unaccessibleAttachment of context.unaccessibleAttachmentPaths)
1048
- warn(`cannot access attachment ${unaccessibleAttachment}`);
1049
- this._report = report;
1050
- this._attachments = await saveReportAndAttachments(report, Array.from(context.attachments.values()), this._outputFolder);
1051
- this._result = result;
1052
- }
1053
- async onExit() {
1054
- if (!this._report)
1055
- return;
1056
- await ReportUploader.upload({
1057
- report: this._report,
1058
- attachments: this._attachments,
1059
- flakinessAccessToken: this._options.token,
1060
- flakinessEndpoint: this._options.endpoint,
1061
- log: console.log
948
+ // src/index.ts
949
+ function inferRunUrl() {
950
+ if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
951
+ return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
952
+ return void 0;
953
+ }
954
+ async function showReport(reportFolder) {
955
+ const reportPath = path3.join(reportFolder, "report.json");
956
+ const config = await FlakinessProjectConfig.load();
957
+ const projectPublicId = config.projectPublicId();
958
+ const reportViewerEndpoint = config.reportViewerEndpoint();
959
+ const server = await LocalReportServer.create({
960
+ endpoint: reportViewerEndpoint,
961
+ port: 9373,
962
+ reportPath,
963
+ attachmentsFolder: reportFolder
964
+ });
965
+ const url = new URL(reportViewerEndpoint);
966
+ url.searchParams.set("port", String(server.port()));
967
+ url.searchParams.set("token", server.authToken());
968
+ if (projectPublicId)
969
+ url.searchParams.set("ppid", projectPublicId);
970
+ console.log(chalk.cyan(`
971
+ Serving Flakiness report at ${url.toString()}
972
+ Press Ctrl+C to quit.`));
973
+ await open(url.toString());
974
+ await new Promise(() => {
975
+ });
976
+ }
977
+ async function saveReport(report, attachments, outputFolder) {
978
+ const reportPath = path3.join(outputFolder, "report.json");
979
+ const attachmentsFolder = path3.join(outputFolder, "attachments");
980
+ await fs6.promises.rm(outputFolder, { recursive: true, force: true });
981
+ await fs6.promises.mkdir(outputFolder, { recursive: true });
982
+ await fs6.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
983
+ if (attachments.length)
984
+ await fs6.promises.mkdir(attachmentsFolder);
985
+ const movedAttachments = [];
986
+ for (const attachment of attachments) {
987
+ const attachmentPath = path3.join(attachmentsFolder, attachment.id);
988
+ if (attachment.path)
989
+ await fs6.promises.cp(attachment.path, attachmentPath);
990
+ else if (attachment.body)
991
+ await fs6.promises.writeFile(attachmentPath, attachment.body);
992
+ movedAttachments.push({
993
+ contentType: attachment.contentType,
994
+ id: attachment.id,
995
+ path: attachmentPath
1062
996
  });
1063
- const openMode = this._options.open ?? "on-failure";
1064
- const shouldOpen = process.stdin.isTTY && !process.env.CI && (openMode === "always" || openMode === "on-failure" && this._result?.status === "failed");
1065
- if (shouldOpen) {
1066
- await showReport(this._outputFolder);
1067
- } else {
1068
- const defaultOutputFolder = path5.join(process.cwd(), "flakiness-report");
1069
- const folder = defaultOutputFolder === this._outputFolder ? "" : path5.relative(process.cwd(), this._outputFolder);
1070
- console.log(`
1071
- To open last Flakiness report, install Flakiness CLI tool and run:
1072
-
1073
- ${chalk2.cyan(`flakiness show ${folder}`)}
1074
- `);
1075
- }
1076
997
  }
1077
- };
1078
- function toSTDIOEntry(data) {
1079
- if (Buffer.isBuffer(data))
1080
- return { buffer: data.toString("base64") };
1081
- return { text: data };
998
+ return movedAttachments;
1082
999
  }
1083
1000
  export {
1084
- FlakinessReporter as default
1001
+ FlakinessProjectConfig,
1002
+ FlakinessReport,
1003
+ ReportUploader,
1004
+ ReportUtils,
1005
+ SystemUtilizationSampler,
1006
+ computeGitRoot,
1007
+ createDataAttachment,
1008
+ createEnvironment,
1009
+ createFileAttachment,
1010
+ createTestStepSnippetsInplace,
1011
+ gitCommitInfo,
1012
+ inferRunUrl,
1013
+ pathutils_exports as pathutils,
1014
+ saveReport,
1015
+ showReport,
1016
+ stripAnsi
1085
1017
  };
1086
- //# sourceMappingURL=playwright-test.js.map
1018
+ //# sourceMappingURL=index.js.map