@flakiness/sdk 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli/cli.js ADDED
@@ -0,0 +1,1435 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/cli.ts
4
+ import { Command, Option } from "commander";
5
+ import fs10 from "fs";
6
+ import path8 from "path";
7
+
8
+ // src/flakinessSession.ts
9
+ import fs2 from "fs/promises";
10
+ import os2 from "os";
11
+ import path from "path";
12
+
13
+ // ../server/lib/common/typedHttp.js
14
+ var TypedHTTP;
15
+ ((TypedHTTP2) => {
16
+ TypedHTTP2.StatusCodes = {
17
+ Informational: {
18
+ CONTINUE: 100,
19
+ SWITCHING_PROTOCOLS: 101,
20
+ PROCESSING: 102,
21
+ EARLY_HINTS: 103
22
+ },
23
+ Success: {
24
+ OK: 200,
25
+ CREATED: 201,
26
+ ACCEPTED: 202,
27
+ NON_AUTHORITATIVE_INFORMATION: 203,
28
+ NO_CONTENT: 204,
29
+ RESET_CONTENT: 205,
30
+ PARTIAL_CONTENT: 206,
31
+ MULTI_STATUS: 207
32
+ },
33
+ Redirection: {
34
+ MULTIPLE_CHOICES: 300,
35
+ MOVED_PERMANENTLY: 301,
36
+ MOVED_TEMPORARILY: 302,
37
+ SEE_OTHER: 303,
38
+ NOT_MODIFIED: 304,
39
+ USE_PROXY: 305,
40
+ TEMPORARY_REDIRECT: 307,
41
+ PERMANENT_REDIRECT: 308
42
+ },
43
+ ClientErrors: {
44
+ BAD_REQUEST: 400,
45
+ UNAUTHORIZED: 401,
46
+ PAYMENT_REQUIRED: 402,
47
+ FORBIDDEN: 403,
48
+ NOT_FOUND: 404,
49
+ METHOD_NOT_ALLOWED: 405,
50
+ NOT_ACCEPTABLE: 406,
51
+ PROXY_AUTHENTICATION_REQUIRED: 407,
52
+ REQUEST_TIMEOUT: 408,
53
+ CONFLICT: 409,
54
+ GONE: 410,
55
+ LENGTH_REQUIRED: 411,
56
+ PRECONDITION_FAILED: 412,
57
+ REQUEST_TOO_LONG: 413,
58
+ REQUEST_URI_TOO_LONG: 414,
59
+ UNSUPPORTED_MEDIA_TYPE: 415,
60
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416,
61
+ EXPECTATION_FAILED: 417,
62
+ IM_A_TEAPOT: 418,
63
+ INSUFFICIENT_SPACE_ON_RESOURCE: 419,
64
+ METHOD_FAILURE: 420,
65
+ MISDIRECTED_REQUEST: 421,
66
+ UNPROCESSABLE_ENTITY: 422,
67
+ LOCKED: 423,
68
+ FAILED_DEPENDENCY: 424,
69
+ UPGRADE_REQUIRED: 426,
70
+ PRECONDITION_REQUIRED: 428,
71
+ TOO_MANY_REQUESTS: 429,
72
+ REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
73
+ UNAVAILABLE_FOR_LEGAL_REASONS: 451
74
+ },
75
+ ServerErrors: {
76
+ INTERNAL_SERVER_ERROR: 500,
77
+ NOT_IMPLEMENTED: 501,
78
+ BAD_GATEWAY: 502,
79
+ SERVICE_UNAVAILABLE: 503,
80
+ GATEWAY_TIMEOUT: 504,
81
+ HTTP_VERSION_NOT_SUPPORTED: 505,
82
+ INSUFFICIENT_STORAGE: 507,
83
+ NETWORK_AUTHENTICATION_REQUIRED: 511
84
+ }
85
+ };
86
+ const AllErrorCodes = {
87
+ ...TypedHTTP2.StatusCodes.ClientErrors,
88
+ ...TypedHTTP2.StatusCodes.ServerErrors
89
+ };
90
+ class HttpError extends Error {
91
+ constructor(status, message) {
92
+ super(message);
93
+ this.status = status;
94
+ }
95
+ static withCode(code, message) {
96
+ const statusCode = AllErrorCodes[code];
97
+ const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
98
+ return new HttpError(statusCode, message ?? defaultMessage);
99
+ }
100
+ }
101
+ TypedHTTP2.HttpError = HttpError;
102
+ function isInformationalResponse(response) {
103
+ return response.status >= 100 && response.status < 200;
104
+ }
105
+ TypedHTTP2.isInformationalResponse = isInformationalResponse;
106
+ function isSuccessResponse(response) {
107
+ return response.status >= 200 && response.status < 300;
108
+ }
109
+ TypedHTTP2.isSuccessResponse = isSuccessResponse;
110
+ function isRedirectResponse(response) {
111
+ return response.status >= 300 && response.status < 400;
112
+ }
113
+ TypedHTTP2.isRedirectResponse = isRedirectResponse;
114
+ function isErrorResponse(response) {
115
+ return response.status >= 400 && response.status < 600;
116
+ }
117
+ TypedHTTP2.isErrorResponse = isErrorResponse;
118
+ function info(status) {
119
+ return { status };
120
+ }
121
+ TypedHTTP2.info = info;
122
+ function ok(data, status) {
123
+ return {
124
+ status: status ?? TypedHTTP2.StatusCodes.Success.OK,
125
+ data
126
+ };
127
+ }
128
+ TypedHTTP2.ok = ok;
129
+ function redirect(url, status = 302) {
130
+ return { status, url };
131
+ }
132
+ TypedHTTP2.redirect = redirect;
133
+ function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
134
+ return { status, message };
135
+ }
136
+ TypedHTTP2.error = error;
137
+ class Router {
138
+ constructor(_resolveContext) {
139
+ this._resolveContext = _resolveContext;
140
+ }
141
+ static create() {
142
+ return new Router(async (e) => e.ctx);
143
+ }
144
+ rawMethod(method, route) {
145
+ return {
146
+ [method]: {
147
+ method,
148
+ input: route.input,
149
+ etag: route.etag,
150
+ resolveContext: this._resolveContext,
151
+ handler: route.handler
152
+ }
153
+ };
154
+ }
155
+ get(route) {
156
+ return this.rawMethod("GET", {
157
+ ...route,
158
+ handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
159
+ });
160
+ }
161
+ post(route) {
162
+ return this.rawMethod("POST", {
163
+ ...route,
164
+ handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
165
+ });
166
+ }
167
+ use(resolveContext) {
168
+ return new Router(async (options) => {
169
+ const m = await this._resolveContext(options);
170
+ return resolveContext({ ...options, ctx: m });
171
+ });
172
+ }
173
+ }
174
+ TypedHTTP2.Router = Router;
175
+ function createClient(base, fetchCallback) {
176
+ function buildUrl(path9, input, options) {
177
+ const method = path9.at(-1);
178
+ const url = new URL(path9.slice(0, path9.length - 1).join("/"), base);
179
+ const signal = options?.signal;
180
+ let body = void 0;
181
+ if (method === "GET" && input)
182
+ url.searchParams.set("input", JSON.stringify(input));
183
+ else if (method !== "GET" && input)
184
+ body = JSON.stringify(input);
185
+ return {
186
+ url,
187
+ method,
188
+ headers: body ? { "Content-Type": "application/json" } : void 0,
189
+ body,
190
+ signal
191
+ };
192
+ }
193
+ function createProxy(path9 = []) {
194
+ return new Proxy(() => {
195
+ }, {
196
+ get(target, prop) {
197
+ if (typeof prop === "symbol")
198
+ return void 0;
199
+ if (prop === "prepare")
200
+ return (input, options) => buildUrl(path9, input, options);
201
+ const newPath = [...path9, prop];
202
+ return createProxy(newPath);
203
+ },
204
+ apply(target, thisArg, args) {
205
+ const options = buildUrl(path9, args[0], args[1]);
206
+ return fetchCallback(options.url, {
207
+ method: options.method,
208
+ body: options.body,
209
+ headers: options.headers,
210
+ signal: options.signal
211
+ }).then(async (response) => {
212
+ if (response.status >= 200 && response.status < 300) {
213
+ if (response.headers.get("content-type")?.includes("application/json")) {
214
+ const text = await response.text();
215
+ return text.length ? JSON.parse(text) : void 0;
216
+ }
217
+ return await response.blob();
218
+ }
219
+ if (response.status >= 400 && response.status < 600) {
220
+ const text = await response.text();
221
+ if (text)
222
+ throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
223
+ else
224
+ throw new Error(`HTTP request failed with status ${response.status}`);
225
+ }
226
+ });
227
+ }
228
+ });
229
+ }
230
+ return createProxy();
231
+ }
232
+ TypedHTTP2.createClient = createClient;
233
+ })(TypedHTTP || (TypedHTTP = {}));
234
+
235
+ // src/utils.ts
236
+ import assert from "assert";
237
+ import { spawnSync } from "child_process";
238
+ import crypto from "crypto";
239
+ import fs from "fs";
240
+ import http from "http";
241
+ import https from "https";
242
+ import os from "os";
243
+ import { posix as posixPath, win32 as win32Path } from "path";
244
+ import util from "util";
245
+ import zlib from "zlib";
246
+ var gzipAsync = util.promisify(zlib.gzip);
247
+ var gunzipAsync = util.promisify(zlib.gunzip);
248
+ var gunzipSync = zlib.gunzipSync;
249
+ var brotliCompressAsync = util.promisify(zlib.brotliCompress);
250
+ var brotliCompressSync = zlib.brotliCompressSync;
251
+ async function existsAsync(aPath) {
252
+ return fs.promises.stat(aPath).then(() => true).catch((e) => false);
253
+ }
254
+ function extractEnvConfiguration() {
255
+ const ENV_PREFIX = "FK_ENV_";
256
+ return Object.fromEntries(
257
+ Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
258
+ );
259
+ }
260
+ function sha1File(filePath) {
261
+ return new Promise((resolve, reject) => {
262
+ const hash = crypto.createHash("sha1");
263
+ const stream = fs.createReadStream(filePath);
264
+ stream.on("data", (chunk) => {
265
+ hash.update(chunk);
266
+ });
267
+ stream.on("end", () => {
268
+ resolve(hash.digest("hex"));
269
+ });
270
+ stream.on("error", (err) => {
271
+ reject(err);
272
+ });
273
+ });
274
+ }
275
+ function sha1Buffer(data) {
276
+ const hash = crypto.createHash("sha1");
277
+ hash.update(data);
278
+ return hash.digest("hex");
279
+ }
280
+ async function retryWithBackoff(job, backoff = []) {
281
+ for (const timeout of backoff) {
282
+ try {
283
+ return await job();
284
+ } catch (e) {
285
+ if (e instanceof AggregateError)
286
+ console.error(`[flakiness.io err]`, e.errors[0].message);
287
+ else if (e instanceof Error)
288
+ console.error(`[flakiness.io err]`, e.message);
289
+ else
290
+ console.error(`[flakiness.io err]`, e);
291
+ await new Promise((x) => setTimeout(x, timeout));
292
+ }
293
+ }
294
+ return await job();
295
+ }
296
+ var httpUtils;
297
+ ((httpUtils2) => {
298
+ function createRequest({ url, method = "get", headers = {} }) {
299
+ let resolve;
300
+ let reject;
301
+ const responseDataPromise = new Promise((a, b) => {
302
+ resolve = a;
303
+ reject = b;
304
+ });
305
+ const protocol = url.startsWith("https") ? https : http;
306
+ const request = protocol.request(url, { method, headers }, (res) => {
307
+ const chunks = [];
308
+ res.on("data", (chunk) => chunks.push(chunk));
309
+ res.on("end", () => {
310
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
311
+ resolve(Buffer.concat(chunks));
312
+ else
313
+ reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
314
+ });
315
+ res.on("error", (error) => reject(error));
316
+ });
317
+ request.on("error", reject);
318
+ return { request, responseDataPromise };
319
+ }
320
+ httpUtils2.createRequest = createRequest;
321
+ async function getBuffer(url, backoff) {
322
+ return await retryWithBackoff(async () => {
323
+ const { request, responseDataPromise } = createRequest({ url });
324
+ request.end();
325
+ return await responseDataPromise;
326
+ }, backoff);
327
+ }
328
+ httpUtils2.getBuffer = getBuffer;
329
+ async function getText(url, backoff) {
330
+ const buffer = await getBuffer(url, backoff);
331
+ return buffer.toString("utf-8");
332
+ }
333
+ httpUtils2.getText = getText;
334
+ async function getJSON(url) {
335
+ return JSON.parse(await getText(url));
336
+ }
337
+ httpUtils2.getJSON = getJSON;
338
+ async function postText(url, text, backoff) {
339
+ const headers = {
340
+ "Content-Type": "application/json",
341
+ "Content-Length": Buffer.byteLength(text) + ""
342
+ };
343
+ return await retryWithBackoff(async () => {
344
+ const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
345
+ request.write(text);
346
+ request.end();
347
+ return await responseDataPromise;
348
+ }, backoff);
349
+ }
350
+ httpUtils2.postText = postText;
351
+ async function postJSON(url, json, backoff) {
352
+ const buffer = await postText(url, JSON.stringify(json), backoff);
353
+ return JSON.parse(buffer.toString("utf-8"));
354
+ }
355
+ httpUtils2.postJSON = postJSON;
356
+ })(httpUtils || (httpUtils = {}));
357
+ 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");
358
+ function stripAnsi(str) {
359
+ return str.replace(ansiRegex, "");
360
+ }
361
+ function shell(command, args, options) {
362
+ try {
363
+ const result = spawnSync(command, args, { encoding: "utf-8", ...options });
364
+ if (result.status !== 0) {
365
+ console.log(result);
366
+ console.log(options);
367
+ return void 0;
368
+ }
369
+ return result.stdout.trim();
370
+ } catch (e) {
371
+ console.log(e);
372
+ return void 0;
373
+ }
374
+ }
375
+ function readLinuxOSRelease() {
376
+ const osReleaseText = fs.readFileSync("/etc/os-release", "utf-8");
377
+ return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
378
+ line = line.trim();
379
+ let [key, value] = line.split("=");
380
+ if (value.startsWith('"') && value.endsWith('"'))
381
+ value = value.substring(1, value.length - 1);
382
+ return [key, value];
383
+ }));
384
+ }
385
+ function osLinuxInfo() {
386
+ const arch = shell(`uname`, [`-m`]);
387
+ const osReleaseMap = readLinuxOSRelease();
388
+ const name = osReleaseMap.get("name") ?? shell(`uname`);
389
+ const version = osReleaseMap.get("version_id");
390
+ return { name, arch, version };
391
+ }
392
+ function osDarwinInfo() {
393
+ const name = "macos";
394
+ const arch = shell(`uname`, [`-m`]);
395
+ const version = shell(`sw_vers`, [`-productVersion`]);
396
+ return { name, arch, version };
397
+ }
398
+ function osWinInfo() {
399
+ const name = "win";
400
+ const arch = process.arch;
401
+ const version = os.release();
402
+ return { name, arch, version };
403
+ }
404
+ function getOSInfo() {
405
+ if (process.platform === "darwin")
406
+ return osDarwinInfo();
407
+ if (process.platform === "win32")
408
+ return osWinInfo();
409
+ return osLinuxInfo();
410
+ }
411
+ function inferRunUrl() {
412
+ if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
413
+ return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
414
+ return void 0;
415
+ }
416
+ function parseStringDate(dateString) {
417
+ return +new Date(dateString);
418
+ }
419
+ function gitCommitInfo(gitRepo) {
420
+ const sha = shell(`git`, ["rev-parse", "HEAD"], {
421
+ cwd: gitRepo,
422
+ encoding: "utf-8"
423
+ });
424
+ assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
425
+ return sha.trim();
426
+ }
427
+ function computeGitRoot(somePathInsideGitRepo) {
428
+ const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
429
+ cwd: somePathInsideGitRepo,
430
+ encoding: "utf-8"
431
+ });
432
+ assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
433
+ return normalizePath(root);
434
+ }
435
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
436
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
437
+ function normalizePath(aPath) {
438
+ if (IS_WIN32_PATH.test(aPath)) {
439
+ aPath = aPath.split(win32Path.sep).join(posixPath.sep);
440
+ }
441
+ if (IS_ALMOST_POSIX_PATH.test(aPath))
442
+ return "/" + aPath[0] + aPath.substring(2);
443
+ return aPath;
444
+ }
445
+ function gitFilePath(gitRoot, absolutePath) {
446
+ return posixPath.relative(gitRoot, absolutePath);
447
+ }
448
+ function parseDurationMS(value) {
449
+ if (isNaN(value))
450
+ throw new Error("Duration cannot be NaN");
451
+ if (value < 0)
452
+ throw new Error(`Duration cannot be less than 0, found ${value}`);
453
+ return value | 0;
454
+ }
455
+ function createEnvironments(projects) {
456
+ const envConfiguration = extractEnvConfiguration();
457
+ const osInfo = getOSInfo();
458
+ let uniqueNames = /* @__PURE__ */ new Set();
459
+ const result = /* @__PURE__ */ new Map();
460
+ for (const project of projects) {
461
+ let defaultName = project.name;
462
+ if (!defaultName.trim())
463
+ defaultName = "anonymous";
464
+ let name = defaultName;
465
+ for (let i = 2; uniqueNames.has(name); ++i)
466
+ name = `${defaultName}-${i}`;
467
+ uniqueNames.add(defaultName);
468
+ result.set(project, {
469
+ name,
470
+ systemData: {
471
+ osArch: osInfo.arch,
472
+ osName: osInfo.name,
473
+ osVersion: osInfo.version
474
+ },
475
+ userSuppliedData: {
476
+ ...envConfiguration,
477
+ ...project.metadata
478
+ },
479
+ opaqueData: {
480
+ project
481
+ }
482
+ });
483
+ }
484
+ return result;
485
+ }
486
+
487
+ // src/serverapi.ts
488
+ function createServerAPI(endpoint, options) {
489
+ endpoint += "/api/";
490
+ const fetcher = options?.auth ? (url, init) => fetch(url, {
491
+ ...init,
492
+ headers: {
493
+ ...init.headers,
494
+ "Authorization": `Bearer ${options.auth}`
495
+ }
496
+ }) : fetch;
497
+ if (options?.retries)
498
+ return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
499
+ return TypedHTTP.createClient(endpoint, fetcher);
500
+ }
501
+
502
+ // src/flakinessSession.ts
503
+ var CONFIG_DIR = (() => {
504
+ const configDir = process.platform === "darwin" ? path.join(os2.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os2.homedir(), "AppData", "Roaming", "flakiness") : path.join(os2.homedir(), ".config", "flakiness");
505
+ return configDir;
506
+ })();
507
+ var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
508
+ var FlakinessSession = class _FlakinessSession {
509
+ constructor(_config) {
510
+ this._config = _config;
511
+ this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
512
+ }
513
+ static async load() {
514
+ const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
515
+ if (!data)
516
+ return void 0;
517
+ const json = JSON.parse(data);
518
+ return new _FlakinessSession(json);
519
+ }
520
+ static async remove() {
521
+ await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
522
+ }
523
+ api;
524
+ endpoint() {
525
+ return this._config.endpoint;
526
+ }
527
+ path() {
528
+ return CONFIG_PATH;
529
+ }
530
+ sessionToken() {
531
+ return this._config.token;
532
+ }
533
+ async save() {
534
+ await fs2.mkdir(CONFIG_DIR, { recursive: true });
535
+ await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
536
+ }
537
+ };
538
+
539
+ // src/cli/cmd-convert.ts
540
+ import fs4 from "fs/promises";
541
+ import path3 from "path";
542
+
543
+ // src/junit.ts
544
+ import { FlakinessReport as FK } from "@flakiness/report";
545
+ import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
546
+ import assert2 from "assert";
547
+ import fs3 from "fs";
548
+ import path2 from "path";
549
+ function getProperties(element) {
550
+ const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
551
+ if (!propertiesNodes.length)
552
+ return [];
553
+ const result = [];
554
+ for (const propertiesNode of propertiesNodes) {
555
+ const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
556
+ for (const property of properties) {
557
+ const name = property.attributes["name"];
558
+ const innerText = property.children.find((node) => node instanceof XmlText);
559
+ const value = property.attributes["value"] ?? innerText?.text ?? "";
560
+ result.push([name, value]);
561
+ }
562
+ }
563
+ return result;
564
+ }
565
+ function extractErrors(testcase) {
566
+ const xmlErrors = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
567
+ if (!xmlErrors.length)
568
+ return void 0;
569
+ const errors = [];
570
+ for (const xmlErr of xmlErrors) {
571
+ const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
572
+ const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
573
+ const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
574
+ errors.push({
575
+ message,
576
+ stack
577
+ });
578
+ }
579
+ return errors;
580
+ }
581
+ function extractStdout(testcase, stdio) {
582
+ const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === stdio);
583
+ if (!xmlStdio.length)
584
+ return void 0;
585
+ return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
586
+ text: txtNode.text
587
+ }));
588
+ }
589
+ async function parseAttachment(value) {
590
+ let absolutePath = path2.resolve(process.cwd(), value);
591
+ if (fs3.existsSync(absolutePath)) {
592
+ const id = await sha1File(absolutePath);
593
+ return {
594
+ contentType: "image/png",
595
+ path: absolutePath,
596
+ id
597
+ };
598
+ }
599
+ return {
600
+ contentType: "text/plain",
601
+ id: sha1Buffer(value),
602
+ body: Buffer.from(value)
603
+ };
604
+ }
605
+ async function traverseJUnitReport(context, node) {
606
+ const element = node;
607
+ if (!(element instanceof XmlElement))
608
+ return;
609
+ let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
610
+ if (element.attributes["timestamp"])
611
+ currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
612
+ if (element.name === "testsuite") {
613
+ const file = element.attributes["file"];
614
+ const line = parseInt(element.attributes["line"], 10);
615
+ const name = element.attributes["name"];
616
+ const newSuite = {
617
+ title: name ?? file,
618
+ location: file && !isNaN(line) ? {
619
+ file,
620
+ line,
621
+ column: 1
622
+ } : FK.NO_LOCATION,
623
+ type: name ? "suite" : file ? "file" : "anonymous suite",
624
+ suites: [],
625
+ tests: []
626
+ };
627
+ if (currentSuite) {
628
+ currentSuite.suites ??= [];
629
+ currentSuite.suites.push(newSuite);
630
+ } else {
631
+ report.suites.push(newSuite);
632
+ }
633
+ currentSuite = newSuite;
634
+ const userSuppliedData = getProperties(element);
635
+ if (userSuppliedData.length) {
636
+ currentEnv = structuredClone(currentEnv);
637
+ currentEnv.userSuppliedData ??= {};
638
+ for (const [key, value] of userSuppliedData)
639
+ currentEnv.userSuppliedData[key] = value;
640
+ currentEnvIndex = report.environments.push(currentEnv) - 1;
641
+ }
642
+ } else if (element.name === "testcase") {
643
+ assert2(currentSuite);
644
+ const file = element.attributes["file"];
645
+ const name = element.attributes["name"];
646
+ const line = parseInt(element.attributes["line"], 10);
647
+ const timeMs = parseFloat(element.attributes["time"]) * 1e3;
648
+ const startTimestamp = currentTimeMs;
649
+ const duration = timeMs;
650
+ currentTimeMs += timeMs;
651
+ const annotations = [];
652
+ const attachments2 = [];
653
+ for (const [key, value] of getProperties(element)) {
654
+ if (key.toLowerCase().startsWith("attachment")) {
655
+ if (context.ignoreAttachments)
656
+ continue;
657
+ const attachment = await parseAttachment(value);
658
+ context.attachments.set(attachment.id, attachment);
659
+ attachments2.push({
660
+ id: attachment.id,
661
+ contentType: attachment.contentType,
662
+ //TODO: better default names for attachments?
663
+ name: attachment.path ? path2.basename(attachment.path) : `attachment`
664
+ });
665
+ } else {
666
+ annotations.push({
667
+ type: key,
668
+ description: value.length ? value : void 0
669
+ });
670
+ }
671
+ }
672
+ const childElements = element.children.filter((child) => child instanceof XmlElement);
673
+ const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
674
+ if (xmlSkippedAnnotation)
675
+ annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
676
+ const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
677
+ const errors = extractErrors(element);
678
+ const test = {
679
+ title: name,
680
+ location: file && !isNaN(line) ? {
681
+ file,
682
+ line,
683
+ column: 1
684
+ } : FK.NO_LOCATION,
685
+ attempts: [{
686
+ environmentIdx: currentEnvIndex,
687
+ expectedStatus,
688
+ annotations,
689
+ attachments: attachments2,
690
+ startTimestamp,
691
+ duration,
692
+ status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
693
+ errors,
694
+ stdout: extractStdout(element, "system-out"),
695
+ stderr: extractStdout(element, "system-err")
696
+ }]
697
+ };
698
+ currentSuite.tests ??= [];
699
+ currentSuite.tests.push(test);
700
+ }
701
+ context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
702
+ for (const child of element.children)
703
+ await traverseJUnitReport(context, child);
704
+ }
705
+ async function parseJUnit(xmls, options) {
706
+ const report = {
707
+ category: "junit",
708
+ commitId: options.commitId,
709
+ duration: options.runDuration,
710
+ startTimestamp: options.runStartTimestamp,
711
+ url: options.runUrl,
712
+ environments: [options.defaultEnv],
713
+ suites: [],
714
+ unattributedErrors: []
715
+ };
716
+ const context = {
717
+ currentEnv: options.defaultEnv,
718
+ currentEnvIndex: 0,
719
+ currentTimeMs: 0,
720
+ report,
721
+ currentSuite: void 0,
722
+ attachments: /* @__PURE__ */ new Map(),
723
+ ignoreAttachments: !!options.ignoreAttachments
724
+ };
725
+ for (const xml of xmls) {
726
+ const doc = parseXml(xml);
727
+ for (const element of doc.children)
728
+ await traverseJUnitReport(context, element);
729
+ }
730
+ return {
731
+ report: FK.dedupeSuitesTestsEnvironments(report),
732
+ attachments: Array.from(context.attachments.values())
733
+ };
734
+ }
735
+
736
+ // src/cli/cmd-convert.ts
737
+ async function cmdConvert(junitPath, options) {
738
+ const fullPath = path3.resolve(junitPath);
739
+ if (!await fs4.access(fullPath, fs4.constants.F_OK).then(() => true).catch(() => false)) {
740
+ console.error(`Error: path ${fullPath} is not accessible`);
741
+ process.exit(1);
742
+ }
743
+ const stat = await fs4.stat(fullPath);
744
+ let xmlContents = [];
745
+ if (stat.isFile()) {
746
+ const xmlContent = await fs4.readFile(fullPath, "utf-8");
747
+ xmlContents.push(xmlContent);
748
+ } else if (stat.isDirectory()) {
749
+ const xmlFiles = await findXmlFiles(fullPath);
750
+ if (xmlFiles.length === 0) {
751
+ console.error(`Error: No XML files found in directory ${fullPath}`);
752
+ process.exit(1);
753
+ }
754
+ console.log(`Found ${xmlFiles.length} XML files`);
755
+ for (const xmlFile of xmlFiles) {
756
+ const xmlContent = await fs4.readFile(xmlFile, "utf-8");
757
+ xmlContents.push(xmlContent);
758
+ }
759
+ } else {
760
+ console.error(`Error: ${fullPath} is neither a file nor a directory`);
761
+ process.exit(1);
762
+ }
763
+ let commitId;
764
+ if (options.commitId) {
765
+ commitId = options.commitId;
766
+ } else {
767
+ try {
768
+ commitId = gitCommitInfo(process.cwd());
769
+ } catch (e) {
770
+ console.error("Failed to get git commit info. Please provide --commit-id option.");
771
+ process.exit(1);
772
+ }
773
+ }
774
+ const { report, attachments } = await parseJUnit(xmlContents, {
775
+ commitId,
776
+ defaultEnv: { name: options.envName },
777
+ runStartTimestamp: Date.now(),
778
+ runDuration: 0
779
+ });
780
+ await fs4.writeFile("fkreport.json", JSON.stringify(report, null, 2));
781
+ console.log("\u2713 Saved report to fkreport.json");
782
+ if (attachments.length > 0) {
783
+ await fs4.mkdir("fkattachments", { recursive: true });
784
+ for (const attachment of attachments) {
785
+ if (attachment.path) {
786
+ const destPath = path3.join("fkattachments", attachment.id);
787
+ await fs4.copyFile(attachment.path, destPath);
788
+ } else if (attachment.body) {
789
+ const destPath = path3.join("fkattachments", attachment.id);
790
+ await fs4.writeFile(destPath, attachment.body);
791
+ }
792
+ }
793
+ console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
794
+ }
795
+ }
796
+ async function findXmlFiles(dir, result = []) {
797
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
798
+ for (const entry of entries) {
799
+ const fullPath = path3.join(dir, entry.name);
800
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
801
+ result.push(fullPath);
802
+ else if (entry.isDirectory())
803
+ await findXmlFiles(fullPath, result);
804
+ }
805
+ return result;
806
+ }
807
+
808
+ // src/cli/cmd-download.ts
809
+ import fs6 from "fs";
810
+ import path5 from "path";
811
+
812
+ // src/flakinessLink.ts
813
+ import fs5 from "fs/promises";
814
+ import path4 from "path";
815
+ var GIT_ROOT = computeGitRoot(process.cwd());
816
+ var CONFIG_DIR2 = path4.join(GIT_ROOT, ".flakiness");
817
+ var CONFIG_PATH2 = path4.join(CONFIG_DIR2, "config.json");
818
+ var FlakinessLink = class _FlakinessLink {
819
+ constructor(_config) {
820
+ this._config = _config;
821
+ }
822
+ static async load() {
823
+ const data = await fs5.readFile(CONFIG_PATH2, "utf-8").catch((e) => void 0);
824
+ if (!data)
825
+ return void 0;
826
+ const json = JSON.parse(data);
827
+ return new _FlakinessLink(json);
828
+ }
829
+ static async remove() {
830
+ await fs5.unlink(CONFIG_PATH2).catch((e) => void 0);
831
+ }
832
+ path() {
833
+ return CONFIG_PATH2;
834
+ }
835
+ projectId() {
836
+ return this._config.projectId;
837
+ }
838
+ async save() {
839
+ await fs5.mkdir(CONFIG_DIR2, { recursive: true });
840
+ await fs5.writeFile(CONFIG_PATH2, JSON.stringify(this._config, null, 2));
841
+ }
842
+ };
843
+
844
+ // src/cli/cmd-download.ts
845
+ async function cmdDownload(runId) {
846
+ const session2 = await FlakinessSession.load();
847
+ if (!session2) {
848
+ console.log(`Please login first`);
849
+ process.exit(1);
850
+ }
851
+ const link = await FlakinessLink.load();
852
+ if (!link) {
853
+ console.log(`Please run 'npx flakiness link' to link to the project`);
854
+ process.exit(1);
855
+ }
856
+ const project = await session2.api.project.getProject.GET({ projectPublicId: link.projectId() });
857
+ const urls = await session2.api.run.downloadURLs.GET({
858
+ orgSlug: project.org.orgSlug,
859
+ projectSlug: project.projectSlug,
860
+ runId
861
+ });
862
+ const rootDir = `fkrun-${runId}`;
863
+ if (fs6.existsSync(rootDir)) {
864
+ console.log(`Directory ${rootDir} already exists!`);
865
+ return;
866
+ }
867
+ const attachmentsDir = path5.join(rootDir, "attachments");
868
+ await fs6.promises.mkdir(rootDir, { recursive: true });
869
+ if (urls.attachmentURLs.length)
870
+ await fs6.promises.mkdir(attachmentsDir, { recursive: true });
871
+ const response = await fetch(urls.reportURL);
872
+ if (!response.ok)
873
+ throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
874
+ const reportContent = await response.text();
875
+ await fs6.promises.writeFile(path5.join(rootDir, "report.json"), reportContent);
876
+ const attachmentDownloader = async () => {
877
+ while (urls.attachmentURLs.length) {
878
+ const url = urls.attachmentURLs.pop();
879
+ const response2 = await fetch(url);
880
+ if (!response2.ok)
881
+ throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
882
+ const fileBuffer = Buffer.from(await response2.arrayBuffer());
883
+ const filename = path5.basename(new URL(url).pathname);
884
+ await fs6.promises.writeFile(path5.join(attachmentsDir, filename), fileBuffer);
885
+ }
886
+ };
887
+ const workerPromises = [];
888
+ for (let i = 0; i < 4; ++i)
889
+ workerPromises.push(attachmentDownloader());
890
+ await Promise.all(workerPromises);
891
+ console.log(`\u2714\uFE0F Saved as ${rootDir}`);
892
+ }
893
+
894
+ // src/cli/cmd-link.ts
895
+ async function cmdLink(slug) {
896
+ const session2 = await FlakinessSession.load();
897
+ if (!session2) {
898
+ console.log(`Please login first`);
899
+ process.exit(1);
900
+ }
901
+ const [orgSlug, projectSlug] = slug.split("/");
902
+ const project = await session2.api.project.findProject.GET({
903
+ orgSlug,
904
+ projectSlug
905
+ });
906
+ if (!project) {
907
+ console.log(`Failed to find project ${slug}`);
908
+ process.exit(1);
909
+ }
910
+ const link = new FlakinessLink({
911
+ projectId: project.projectPublicId
912
+ });
913
+ await link.save();
914
+ console.log(`\u2713 Link successful! Config saved to ${link.path()}`);
915
+ }
916
+
917
+ // ../server/lib/common/knownClientIds.js
918
+ var KNOWN_CLIENT_IDS = {
919
+ OFFICIAL_WEB: "flakiness-io-official-cli",
920
+ OFFICIAL_CLI: "flakiness-io-official-website"
921
+ };
922
+
923
+ // src/cli/cmd-login.ts
924
+ import os3 from "os";
925
+ async function cmdLogin(endpoint) {
926
+ const api = createServerAPI(endpoint);
927
+ const data = await api.deviceauth.createRequest.POST({
928
+ clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
929
+ name: os3.hostname()
930
+ });
931
+ console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
932
+ let token;
933
+ while (Date.now() < data.deadline) {
934
+ await new Promise((x) => setTimeout(x, 2e3));
935
+ const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
936
+ if (!result) {
937
+ console.log(`Authorization request was rejected.`);
938
+ return;
939
+ }
940
+ token = result.token;
941
+ if (token)
942
+ break;
943
+ }
944
+ if (!token) {
945
+ console.log(`Failed to login.`);
946
+ process.exit(1);
947
+ }
948
+ const session2 = new FlakinessSession({
949
+ endpoint,
950
+ token
951
+ });
952
+ await session2.save();
953
+ console.log(`\u2713 Login successful! Token saved to ${session2.path()}`);
954
+ }
955
+
956
+ // src/cli/cmd-logout.ts
957
+ async function cmdLogout() {
958
+ await FlakinessSession.remove();
959
+ }
960
+
961
+ // src/cli/cmd-status.ts
962
+ async function cmdStatus() {
963
+ const session2 = await FlakinessSession.load();
964
+ if (!session2) {
965
+ console.log(`user: not logged in`);
966
+ return;
967
+ }
968
+ const user = await session2.api.user.whoami.GET();
969
+ console.log(`user: ${user.userName} (${user.userLogin})`);
970
+ const link = await FlakinessLink.load();
971
+ if (!link) {
972
+ console.log(`project: <not linked>`);
973
+ return;
974
+ }
975
+ const project = await session2.api.project.getProject.GET({
976
+ projectPublicId: link.projectId()
977
+ });
978
+ console.log(`project: ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
979
+ }
980
+
981
+ // src/cli/cmd-unlink.ts
982
+ async function cmdUnlink() {
983
+ await FlakinessLink.remove();
984
+ }
985
+
986
+ // src/cli/cmd-upload-playwright-json.ts
987
+ import fs8 from "fs/promises";
988
+ import path6 from "path";
989
+
990
+ // src/playwrightJSONReport.ts
991
+ import { FlakinessReport as FK2, FlakinessReport } from "@flakiness/report";
992
+ import debug from "debug";
993
+ import { posix as posixPath2 } from "path";
994
+ var dlog = debug("flakiness:json-report");
995
+ var PlaywrightJSONReport;
996
+ ((PlaywrightJSONReport2) => {
997
+ function collectMetadata(somePathInsideProject = process.cwd()) {
998
+ const commitId = gitCommitInfo(somePathInsideProject);
999
+ const osInfo = getOSInfo();
1000
+ const metadata = {
1001
+ gitRoot: computeGitRoot(somePathInsideProject),
1002
+ commitId,
1003
+ osName: osInfo.name,
1004
+ arch: osInfo.arch,
1005
+ osVersion: osInfo.version,
1006
+ runURL: inferRunUrl()
1007
+ };
1008
+ dlog(`metadata directory: ${somePathInsideProject}`);
1009
+ dlog(`metadata: ${JSON.stringify(metadata)}`);
1010
+ dlog(`commit info: ${JSON.stringify(commitId)}`);
1011
+ dlog(`os info: ${JSON.stringify(osInfo)}`);
1012
+ return metadata;
1013
+ }
1014
+ PlaywrightJSONReport2.collectMetadata = collectMetadata;
1015
+ async function parse(metadata, jsonReport, options) {
1016
+ const context = {
1017
+ projectId2environmentIdx: /* @__PURE__ */ new Map(),
1018
+ testBaseDir: normalizePath(jsonReport.config.rootDir),
1019
+ gitRoot: metadata.gitRoot,
1020
+ attachments: /* @__PURE__ */ new Map(),
1021
+ unaccessibleAttachmentPaths: [],
1022
+ extractAttachments: options.extractAttachments
1023
+ };
1024
+ const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
1025
+ const report = {
1026
+ category: FlakinessReport.CATEGORY_PLAYWRIGHT,
1027
+ commitId: metadata.commitId,
1028
+ configPath,
1029
+ url: metadata.runURL,
1030
+ environments: [],
1031
+ suites: [],
1032
+ opaqueData: jsonReport.config,
1033
+ unattributedErrors: jsonReport.errors.map((error) => parseJSONError(context, error)),
1034
+ // The report.stats is a releatively new addition to Playwright's JSONReport,
1035
+ // so we have to polyfill with some reasonable values when it's missing.
1036
+ duration: jsonReport.stats?.duration && jsonReport.stats?.duration > 0 ? parseDurationMS(jsonReport.stats.duration) : 0,
1037
+ startTimestamp: jsonReport.stats && jsonReport.stats.startTime ? parseStringDate(jsonReport.stats.startTime) : Date.now()
1038
+ };
1039
+ report.environments = [...createEnvironments(jsonReport.config.projects).values()];
1040
+ for (let envIdx = 0; envIdx < report.environments.length; ++envIdx)
1041
+ context.projectId2environmentIdx.set(jsonReport.config.projects[envIdx].id, envIdx);
1042
+ report.suites = await Promise.all(jsonReport.suites.map((suite) => parseJSONSuite(context, suite)));
1043
+ return {
1044
+ report: FK2.dedupeSuitesTestsEnvironments(report),
1045
+ attachments: [...context.attachments.values()],
1046
+ unaccessibleAttachmentPaths: context.unaccessibleAttachmentPaths
1047
+ };
1048
+ }
1049
+ PlaywrightJSONReport2.parse = parse;
1050
+ })(PlaywrightJSONReport || (PlaywrightJSONReport = {}));
1051
+ async function parseJSONSuite(context, jsonSuite) {
1052
+ let type = "suite";
1053
+ if (jsonSuite.column === 0 && jsonSuite.line === 0)
1054
+ type = "file";
1055
+ else if (!jsonSuite.title)
1056
+ type = "anonymous suite";
1057
+ const suite = {
1058
+ type,
1059
+ title: jsonSuite.title,
1060
+ location: {
1061
+ file: gitFilePath(context.gitRoot, normalizePath(jsonSuite.file)),
1062
+ line: jsonSuite.line,
1063
+ column: jsonSuite.column
1064
+ }
1065
+ };
1066
+ if (jsonSuite.suites && jsonSuite.suites.length)
1067
+ suite.suites = await Promise.all(jsonSuite.suites.map((suite2) => parseJSONSuite(context, suite2)));
1068
+ if (jsonSuite.specs && jsonSuite.specs.length)
1069
+ suite.tests = await Promise.all(jsonSuite.specs.map((spec) => parseJSONSpec(context, spec)));
1070
+ return suite;
1071
+ }
1072
+ async function parseJSONSpec(context, jsonSpec) {
1073
+ const test = {
1074
+ title: jsonSpec.title,
1075
+ tags: jsonSpec.tags,
1076
+ location: {
1077
+ file: gitFilePath(context.gitRoot, normalizePath(posixPath2.join(context.testBaseDir, normalizePath(jsonSpec.file)))),
1078
+ line: jsonSpec.line,
1079
+ column: jsonSpec.column
1080
+ },
1081
+ attempts: []
1082
+ };
1083
+ for (const jsonTest of jsonSpec.tests) {
1084
+ const environmentIdx = context.projectId2environmentIdx.get(jsonTest.projectId);
1085
+ if (environmentIdx === void 0)
1086
+ throw new Error("Inconsistent report - no project for a test found!");
1087
+ const testResults = jsonTest.results.filter((result) => result.status !== void 0);
1088
+ if (!testResults.length)
1089
+ continue;
1090
+ test.attempts.push(...await Promise.all(testResults.map((jsonTestResult) => parseJSONTestResult(context, jsonTest, environmentIdx, jsonTestResult))));
1091
+ }
1092
+ return test;
1093
+ }
1094
+ function createLocation(context, location) {
1095
+ return {
1096
+ file: gitFilePath(context.gitRoot, normalizePath(location.file)),
1097
+ line: location.line,
1098
+ column: location.column
1099
+ };
1100
+ }
1101
+ async function parseJSONTestResult(context, jsonTest, environmentIdx, jsonTestResult) {
1102
+ const attachments = [];
1103
+ const attempt = {
1104
+ timeout: parseDurationMS(jsonTest.timeout),
1105
+ annotations: jsonTest.annotations.map((annotation) => ({
1106
+ type: annotation.type,
1107
+ description: annotation.description,
1108
+ location: annotation.location ? createLocation(context, annotation.location) : void 0
1109
+ })),
1110
+ environmentIdx,
1111
+ expectedStatus: jsonTest.expectedStatus,
1112
+ parallelIndex: jsonTestResult.parallelIndex,
1113
+ status: jsonTestResult.status,
1114
+ errors: jsonTestResult.errors && jsonTestResult.errors.length ? jsonTestResult.errors.map((error) => parseJSONError(context, error)) : void 0,
1115
+ stdout: jsonTestResult.stdout && jsonTestResult.stdout.length ? jsonTestResult.stdout : void 0,
1116
+ stderr: jsonTestResult.stderr && jsonTestResult.stderr.length ? jsonTestResult.stderr : void 0,
1117
+ steps: jsonTestResult.steps ? jsonTestResult.steps.map((jsonTestStep) => parseJSONTestStep(context, jsonTestStep)) : void 0,
1118
+ startTimestamp: parseStringDate(jsonTestResult.startTime),
1119
+ duration: jsonTestResult.duration && jsonTestResult.duration > 0 ? parseDurationMS(jsonTestResult.duration) : 0,
1120
+ attachments
1121
+ };
1122
+ if (context.extractAttachments) {
1123
+ await Promise.all((jsonTestResult.attachments ?? []).map(async (jsonAttachment) => {
1124
+ if (jsonAttachment.path && !await existsAsync(jsonAttachment.path)) {
1125
+ context.unaccessibleAttachmentPaths.push(jsonAttachment.path);
1126
+ return;
1127
+ }
1128
+ const id = jsonAttachment.path ? await sha1File(jsonAttachment.path) : sha1Buffer(jsonAttachment.body ?? "");
1129
+ context.attachments.set(id, {
1130
+ contentType: jsonAttachment.contentType,
1131
+ id,
1132
+ body: jsonAttachment.body ? Buffer.from(jsonAttachment.body) : void 0,
1133
+ path: jsonAttachment.path
1134
+ });
1135
+ attachments.push({
1136
+ id,
1137
+ name: jsonAttachment.name,
1138
+ contentType: jsonAttachment.contentType
1139
+ });
1140
+ }));
1141
+ }
1142
+ return attempt;
1143
+ }
1144
+ function parseJSONTestStep(context, jsonStep) {
1145
+ const step = {
1146
+ // NOTE: jsonStep.duration was -1 in some playwright versions
1147
+ duration: parseDurationMS(Math.max(jsonStep.duration, 0)),
1148
+ title: jsonStep.title
1149
+ };
1150
+ if (jsonStep.error)
1151
+ step.error = parseJSONError(context, jsonStep.error);
1152
+ if (jsonStep.steps)
1153
+ step.steps = jsonStep.steps.map((childJSONStep) => parseJSONTestStep(context, childJSONStep));
1154
+ return step;
1155
+ }
1156
+ function parseJSONError(context, error) {
1157
+ return {
1158
+ location: error.location ? createLocation(context, error.location) : void 0,
1159
+ message: error.message ? stripAnsi(error.message).split("\n")[0] : void 0,
1160
+ stack: error.stack,
1161
+ value: error.value
1162
+ };
1163
+ }
1164
+
1165
+ // src/reportUploader.ts
1166
+ import fs7 from "fs";
1167
+ import { URL as URL2 } from "url";
1168
+ import { brotliCompressSync as brotliCompressSync2 } from "zlib";
1169
+ var ReportUploader = class _ReportUploader {
1170
+ static optionsFromEnv(overrides) {
1171
+ const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
1172
+ if (!flakinessAccessToken)
1173
+ return void 0;
1174
+ const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
1175
+ return { flakinessAccessToken, flakinessEndpoint };
1176
+ }
1177
+ static async upload(options) {
1178
+ const uploaderOptions = _ReportUploader.optionsFromEnv(options);
1179
+ if (!uploaderOptions) {
1180
+ options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
1181
+ return void 0;
1182
+ }
1183
+ const uploader = new _ReportUploader(uploaderOptions);
1184
+ const upload = uploader.createUpload(options.report, options.attachments);
1185
+ const uploadResult = await upload.upload();
1186
+ if (!uploadResult.success) {
1187
+ options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
1188
+ return { errorMessage: uploadResult.message };
1189
+ }
1190
+ options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
1191
+ if (uploadResult.reportUrl)
1192
+ options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
1193
+ }
1194
+ _options;
1195
+ constructor(options) {
1196
+ this._options = options;
1197
+ }
1198
+ createUpload(report, attachments) {
1199
+ const upload = new ReportUpload(this._options, report, attachments);
1200
+ return upload;
1201
+ }
1202
+ };
1203
+ var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
1204
+ var ReportUpload = class {
1205
+ _report;
1206
+ _attachments;
1207
+ _options;
1208
+ _api;
1209
+ constructor(options, report, attachments) {
1210
+ this._options = options;
1211
+ this._report = report;
1212
+ this._attachments = attachments;
1213
+ this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
1214
+ }
1215
+ async upload(options) {
1216
+ const response = await this._api.run.startUpload.POST({
1217
+ flakinessAccessToken: this._options.flakinessAccessToken,
1218
+ attachmentIds: this._attachments.map((attachment) => attachment.id)
1219
+ }).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
1220
+ if (response?.error || !response.result)
1221
+ return { success: false, message: `flakiness.io returned error: ${response.error.message}` };
1222
+ await Promise.all([
1223
+ this._uploadReport(JSON.stringify(this._report), response.result.report_upload_url, options?.syncCompression ?? false),
1224
+ ...this._attachments.map((attachment) => {
1225
+ const uploadURL = response.result.attachment_upload_urls[attachment.id];
1226
+ if (!uploadURL)
1227
+ throw new Error("Internal error: missing upload URL for attachment!");
1228
+ return this._uploadAttachment(attachment, uploadURL);
1229
+ })
1230
+ ]);
1231
+ const response2 = await this._api.run.completeUpload.POST({
1232
+ upload_token: response.result.upload_token
1233
+ }).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
1234
+ const url = response2?.result?.report_url ? new URL2(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
1235
+ return { success: true, reportUrl: url };
1236
+ }
1237
+ async _uploadReport(data, uploadUrl, syncCompression) {
1238
+ const compressed = syncCompression ? brotliCompressSync2(data) : await brotliCompressAsync(data);
1239
+ const headers = {
1240
+ "Content-Type": "application/json",
1241
+ "Content-Length": Buffer.byteLength(compressed) + "",
1242
+ "Content-Encoding": "br"
1243
+ };
1244
+ await retryWithBackoff(async () => {
1245
+ const { request, responseDataPromise } = httpUtils.createRequest({
1246
+ url: uploadUrl,
1247
+ headers,
1248
+ method: "put"
1249
+ });
1250
+ request.write(compressed);
1251
+ request.end();
1252
+ await responseDataPromise;
1253
+ }, HTTP_BACKOFF);
1254
+ }
1255
+ async _uploadAttachment(attachment, uploadUrl) {
1256
+ const bytesLength = attachment.path ? (await fs7.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
1257
+ const headers = {
1258
+ "Content-Type": attachment.contentType,
1259
+ "Content-Length": bytesLength + ""
1260
+ };
1261
+ await retryWithBackoff(async () => {
1262
+ const { request, responseDataPromise } = httpUtils.createRequest({
1263
+ url: uploadUrl,
1264
+ headers,
1265
+ method: "put"
1266
+ });
1267
+ if (attachment.path) {
1268
+ fs7.createReadStream(attachment.path).pipe(request);
1269
+ } else {
1270
+ if (attachment.body)
1271
+ request.write(attachment.body);
1272
+ request.end();
1273
+ }
1274
+ await responseDataPromise;
1275
+ }, HTTP_BACKOFF);
1276
+ }
1277
+ };
1278
+
1279
+ // src/cli/cmd-upload-playwright-json.ts
1280
+ async function cmdUploadPlaywrightJson(relativePath, options) {
1281
+ const fullPath = path6.resolve(relativePath);
1282
+ if (!await fs8.access(fullPath, fs8.constants.F_OK).then(() => true).catch(() => false)) {
1283
+ console.error(`Error: path ${fullPath} is not accessible`);
1284
+ process.exit(1);
1285
+ }
1286
+ const text = await fs8.readFile(fullPath, "utf-8");
1287
+ const playwrightJson = JSON.parse(text);
1288
+ const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
1289
+ extractAttachments: true
1290
+ });
1291
+ for (const unaccessibleAttachment of unaccessibleAttachmentPaths)
1292
+ console.warn(`WARN: cannot access attachment ${unaccessibleAttachment}`);
1293
+ const uploader = new ReportUploader({
1294
+ flakinessAccessToken: options.accessToken,
1295
+ flakinessEndpoint: options.endpoint
1296
+ });
1297
+ const upload = uploader.createUpload(report, attachments);
1298
+ const uploadResult = await upload.upload();
1299
+ if (!uploadResult.success) {
1300
+ console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
1301
+ } else {
1302
+ console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
1303
+ }
1304
+ }
1305
+
1306
+ // src/cli/cmd-upload.ts
1307
+ import { FlakinessReport as FlakinessReport2 } from "@flakiness/report";
1308
+ import fs9 from "fs/promises";
1309
+ import path7 from "path";
1310
+ async function cmdUpload(relativePath, options) {
1311
+ const fullPath = path7.resolve(relativePath);
1312
+ if (!await fs9.access(fullPath, fs9.constants.F_OK).then(() => true).catch(() => false)) {
1313
+ console.error(`Error: path ${fullPath} is not accessible`);
1314
+ process.exit(1);
1315
+ }
1316
+ const attachmentsDir = options.attachmentsDir ?? path7.dirname(fullPath);
1317
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
1318
+ const attachmentIdToPath = new Map(attachmentFiles.map((file) => [path7.basename(file), file]));
1319
+ const text = await fs9.readFile(fullPath, "utf-8");
1320
+ const report = JSON.parse(text);
1321
+ const attachments = [];
1322
+ const missingAttachments = [];
1323
+ FlakinessReport2.visitTests(report, (test) => {
1324
+ for (const attempt of test.attempts) {
1325
+ for (const attachment of attempt.attachments ?? []) {
1326
+ const file = attachmentIdToPath.get(attachment.id);
1327
+ if (!file) {
1328
+ missingAttachments.push(attachment);
1329
+ continue;
1330
+ }
1331
+ attachments.push({
1332
+ contentType: attachment.contentType,
1333
+ id: attachment.id,
1334
+ path: file
1335
+ });
1336
+ }
1337
+ }
1338
+ });
1339
+ if (missingAttachments.length && !options.ignoreMissingAttachments) {
1340
+ console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
1341
+ process.exit(1);
1342
+ }
1343
+ const uploader = new ReportUploader({
1344
+ flakinessAccessToken: options.accessToken,
1345
+ flakinessEndpoint: options.endpoint
1346
+ });
1347
+ const upload = uploader.createUpload(report, attachments);
1348
+ const uploadResult = await upload.upload();
1349
+ if (!uploadResult.success) {
1350
+ console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
1351
+ } else {
1352
+ console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
1353
+ }
1354
+ }
1355
+ async function listFilesRecursively(dir, result = []) {
1356
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
1357
+ for (const entry of entries) {
1358
+ const fullPath = path7.join(dir, entry.name);
1359
+ if (entry.isDirectory())
1360
+ await listFilesRecursively(fullPath, result);
1361
+ else
1362
+ result.push(fullPath);
1363
+ }
1364
+ return result;
1365
+ }
1366
+
1367
+ // src/cli/cmd-whoami.ts
1368
+ async function cmdWhoami() {
1369
+ const session2 = await FlakinessSession.load();
1370
+ if (!session2) {
1371
+ console.log('Not logged in. Run "flakiness login" first.');
1372
+ process.exit(1);
1373
+ }
1374
+ console.log(`Logged into ${session2.endpoint()}`);
1375
+ const user = await session2.api.user.whoami.GET();
1376
+ console.log(user);
1377
+ }
1378
+
1379
+ // src/cli/cli.ts
1380
+ var session = await FlakinessSession.load();
1381
+ var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").makeOptionMandatory().env("FLAKINESS_ACCESS_TOKEN");
1382
+ var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
1383
+ var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
1384
+ var optIgnoreMissingAttachments = new Option("--ignore-missing-attachments", "Upload report even if some attachments are missing.").default("", "Same directory as the report file");
1385
+ async function runCommand(callback) {
1386
+ try {
1387
+ await callback();
1388
+ } catch (e) {
1389
+ if (!(e instanceof Error))
1390
+ throw e;
1391
+ if (process.env.DBG)
1392
+ console.error(e.stack);
1393
+ else
1394
+ console.error(e.message);
1395
+ process.exit(1);
1396
+ }
1397
+ }
1398
+ var PACKAGE_JSON = JSON.parse(fs10.readFileSync(path8.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
1399
+ var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
1400
+ program.command("upload-playwright-json", { hidden: true }).description("Upload Playwright Test JSON report to the flakiness.io service").argument("<relative-path-to-json>", "Path to the Playwright JSON report file").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePath, options) => runCommand(async () => {
1401
+ await cmdUploadPlaywrightJson(relativePath, options);
1402
+ }));
1403
+ program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
1404
+ await cmdLogin(options.endpoint);
1405
+ }));
1406
+ program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
1407
+ await cmdLogout();
1408
+ }));
1409
+ program.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
1410
+ await cmdWhoami();
1411
+ }));
1412
+ program.command("link").description("Link repository to the flakiness project").argument("org/project", 'An org and project slugs, e.g. "facebook/react"').action(async (slug) => runCommand(async () => {
1413
+ await cmdLink(slug);
1414
+ }));
1415
+ program.command("unlink").description("Unlink repository from the flakiness project").action(async () => runCommand(async () => {
1416
+ await cmdUnlink();
1417
+ }));
1418
+ program.command("status").description("Status repository from the flakiness project").action(async () => runCommand(async () => {
1419
+ await cmdStatus();
1420
+ }));
1421
+ program.command("download").description("Download run").argument("runId", "Run id to download").action(async (runId) => runCommand(async () => {
1422
+ await cmdDownload(parseInt(runId, 10));
1423
+ }));
1424
+ program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-path>", "Path to the Flakiness report file").addOption(optAccessToken).addOption(optEndpoint).addOption(optAttachmentsDir).addOption(optIgnoreMissingAttachments).action(async (relativePath, options) => {
1425
+ await runCommand(async () => {
1426
+ await cmdUpload(relativePath, options);
1427
+ });
1428
+ });
1429
+ program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "default").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").action(async (junitPath, options) => {
1430
+ await runCommand(async () => {
1431
+ await cmdConvert(junitPath, options);
1432
+ });
1433
+ });
1434
+ await program.parseAsync();
1435
+ //# sourceMappingURL=cli.js.map