@flakiness/sdk 0.146.1 → 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.
package/lib/showReport.js CHANGED
@@ -1,102 +1,31 @@
1
1
  // src/showReport.ts
2
2
  import chalk from "chalk";
3
3
  import open from "open";
4
- import path4 from "path";
4
+ import path3 from "path";
5
5
 
6
6
  // src/flakinessProjectConfig.ts
7
- import fs2 from "fs";
8
- import path2 from "path";
7
+ import fs from "fs";
8
+ import path from "path";
9
9
 
10
- // src/utils.ts
11
- import { ReportUtils } from "@flakiness/report";
10
+ // src/git.ts
12
11
  import assert from "assert";
13
- import { spawnSync } from "child_process";
14
- import fs from "fs";
15
- import http from "http";
16
- import https from "https";
17
- import path, { posix as posixPath, win32 as win32Path } from "path";
18
- var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
19
- function errorText(error) {
20
- return FLAKINESS_DBG ? error.stack : error.message;
21
- }
22
- async function retryWithBackoff(job, backoff = []) {
23
- for (const timeout of backoff) {
24
- try {
25
- return await job();
26
- } catch (e) {
27
- if (e instanceof AggregateError)
28
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
29
- else if (e instanceof Error)
30
- console.error(`[flakiness.io err]`, errorText(e));
31
- else
32
- console.error(`[flakiness.io err]`, e);
33
- await new Promise((x) => setTimeout(x, timeout));
34
- }
12
+
13
+ // src/pathutils.ts
14
+ import { posix as posixPath, win32 as win32Path } from "path";
15
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
16
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
17
+ function normalizePath(aPath) {
18
+ if (IS_WIN32_PATH.test(aPath)) {
19
+ aPath = aPath.split(win32Path.sep).join(posixPath.sep);
35
20
  }
36
- return await job();
21
+ if (IS_ALMOST_POSIX_PATH.test(aPath))
22
+ return "/" + aPath[0] + aPath.substring(2);
23
+ return aPath;
37
24
  }
38
- var httpUtils;
39
- ((httpUtils2) => {
40
- function createRequest({ url, method = "get", headers = {} }) {
41
- let resolve;
42
- let reject;
43
- const responseDataPromise = new Promise((a, b) => {
44
- resolve = a;
45
- reject = b;
46
- });
47
- const protocol = url.startsWith("https") ? https : http;
48
- headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
49
- const request = protocol.request(url, { method, headers }, (res) => {
50
- const chunks = [];
51
- res.on("data", (chunk) => chunks.push(chunk));
52
- res.on("end", () => {
53
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
54
- resolve(Buffer.concat(chunks));
55
- else
56
- reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
57
- });
58
- res.on("error", (error) => reject(error));
59
- });
60
- request.on("error", reject);
61
- return { request, responseDataPromise };
62
- }
63
- httpUtils2.createRequest = createRequest;
64
- async function getBuffer(url, backoff) {
65
- return await retryWithBackoff(async () => {
66
- const { request, responseDataPromise } = createRequest({ url });
67
- request.end();
68
- return await responseDataPromise;
69
- }, backoff);
70
- }
71
- httpUtils2.getBuffer = getBuffer;
72
- async function getText(url, backoff) {
73
- const buffer = await getBuffer(url, backoff);
74
- return buffer.toString("utf-8");
75
- }
76
- httpUtils2.getText = getText;
77
- async function getJSON(url) {
78
- return JSON.parse(await getText(url));
79
- }
80
- httpUtils2.getJSON = getJSON;
81
- async function postText(url, text, backoff) {
82
- const headers = {
83
- "Content-Type": "application/json",
84
- "Content-Length": Buffer.byteLength(text) + ""
85
- };
86
- return await retryWithBackoff(async () => {
87
- const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
88
- request.write(text);
89
- request.end();
90
- return await responseDataPromise;
91
- }, backoff);
92
- }
93
- httpUtils2.postText = postText;
94
- async function postJSON(url, json, backoff) {
95
- const buffer = await postText(url, JSON.stringify(json), backoff);
96
- return JSON.parse(buffer.toString("utf-8"));
97
- }
98
- httpUtils2.postJSON = postJSON;
99
- })(httpUtils || (httpUtils = {}));
25
+
26
+ // src/utils.ts
27
+ import { spawnSync } from "child_process";
28
+ var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
100
29
  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");
101
30
  function shell(command, args, options) {
102
31
  try {
@@ -110,40 +39,8 @@ function shell(command, args, options) {
110
39
  return void 0;
111
40
  }
112
41
  }
113
- async function resolveAttachmentPaths(report, attachmentsDir) {
114
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
115
- const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
116
- const attachmentIdToPath = /* @__PURE__ */ new Map();
117
- const missingAttachments = /* @__PURE__ */ new Set();
118
- ReportUtils.visitTests(report, (test) => {
119
- for (const attempt of test.attempts) {
120
- for (const attachment of attempt.attachments ?? []) {
121
- const attachmentPath = filenameToPath.get(attachment.id);
122
- if (!attachmentPath) {
123
- missingAttachments.add(attachment.id);
124
- } else {
125
- attachmentIdToPath.set(attachment.id, {
126
- contentType: attachment.contentType,
127
- id: attachment.id,
128
- path: attachmentPath
129
- });
130
- }
131
- }
132
- }
133
- });
134
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
135
- }
136
- async function listFilesRecursively(dir, result = []) {
137
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
138
- for (const entry of entries) {
139
- const fullPath = path.join(dir, entry.name);
140
- if (entry.isDirectory())
141
- await listFilesRecursively(fullPath, result);
142
- else
143
- result.push(fullPath);
144
- }
145
- return result;
146
- }
42
+
43
+ // src/git.ts
147
44
  function computeGitRoot(somePathInsideGitRepo) {
148
45
  const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
149
46
  cwd: somePathInsideGitRepo,
@@ -152,20 +49,10 @@ function computeGitRoot(somePathInsideGitRepo) {
152
49
  assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
153
50
  return normalizePath(root);
154
51
  }
155
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
156
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
157
- function normalizePath(aPath) {
158
- if (IS_WIN32_PATH.test(aPath)) {
159
- aPath = aPath.split(win32Path.sep).join(posixPath.sep);
160
- }
161
- if (IS_ALMOST_POSIX_PATH.test(aPath))
162
- return "/" + aPath[0] + aPath.substring(2);
163
- return aPath;
164
- }
165
52
 
166
53
  // src/flakinessProjectConfig.ts
167
54
  function createConfigPath(dir) {
168
- return path2.join(dir, ".flakiness", "config.json");
55
+ return path.join(dir, ".flakiness", "config.json");
169
56
  }
170
57
  var gConfigPath;
171
58
  function ensureConfigPath() {
@@ -174,9 +61,9 @@ function ensureConfigPath() {
174
61
  return gConfigPath;
175
62
  }
176
63
  function computeConfigPath() {
177
- for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
64
+ for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
178
65
  const configPath = createConfigPath(p);
179
- if (fs2.existsSync(configPath))
66
+ if (fs.existsSync(configPath))
180
67
  return configPath;
181
68
  }
182
69
  try {
@@ -193,7 +80,7 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
193
80
  }
194
81
  static async load() {
195
82
  const configPath = ensureConfigPath();
196
- const data = await fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
83
+ const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
197
84
  const json = data ? JSON.parse(data) : {};
198
85
  return new _FlakinessProjectConfig(configPath, json);
199
86
  }
@@ -213,8 +100,8 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
213
100
  this._config.projectPublicId = projectId;
214
101
  }
215
102
  async save() {
216
- await fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
217
- await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
103
+ await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
104
+ await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
218
105
  }
219
106
  };
220
107
 
@@ -224,20 +111,22 @@ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
224
111
  import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
225
112
  import bodyParser from "body-parser";
226
113
  import compression from "compression";
227
- import debug from "debug";
114
+ import debug2 from "debug";
228
115
  import express from "express";
229
116
  import "express-async-errors";
230
- import http2 from "http";
117
+ import http from "http";
231
118
 
232
119
  // src/localReportApi.ts
233
120
  import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
234
- import fs3 from "fs";
235
- import path3 from "path";
121
+ import fs2 from "fs";
122
+ import path2 from "path";
236
123
  import { z } from "zod/v4";
237
124
 
238
125
  // src/localGit.ts
239
126
  import { exec } from "child_process";
127
+ import debug from "debug";
240
128
  import { promisify } from "util";
129
+ var log = debug("fk:git");
241
130
  var execAsync = promisify(exec);
242
131
  async function listLocalCommits(gitRoot, head, count) {
243
132
  const FIELD_SEPARATOR = "|~|";
@@ -273,11 +162,131 @@ async function listLocalCommits(gitRoot, head, count) {
273
162
  };
274
163
  });
275
164
  } catch (error) {
276
- console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
277
- throw error;
165
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
166
+ return [];
278
167
  }
279
168
  }
280
169
 
170
+ // src/reportUtils.ts
171
+ import { Multimap } from "@flakiness/shared/common/multimap.js";
172
+ import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
173
+ var ReportUtils;
174
+ ((ReportUtils2) => {
175
+ function visitTests(report, testVisitor) {
176
+ function visitSuite(suite, parents) {
177
+ parents.push(suite);
178
+ for (const test of suite.tests ?? [])
179
+ testVisitor(test, parents);
180
+ for (const childSuite of suite.suites ?? [])
181
+ visitSuite(childSuite, parents);
182
+ parents.pop();
183
+ }
184
+ for (const test of report.tests ?? [])
185
+ testVisitor(test, []);
186
+ for (const suite of report.suites)
187
+ visitSuite(suite, []);
188
+ }
189
+ ReportUtils2.visitTests = visitTests;
190
+ function normalizeReport(report) {
191
+ const gEnvs = /* @__PURE__ */ new Map();
192
+ const gSuites = /* @__PURE__ */ new Map();
193
+ const gTests = new Multimap();
194
+ const gSuiteIds = /* @__PURE__ */ new Map();
195
+ const gTestIds = /* @__PURE__ */ new Map();
196
+ const gEnvIds = /* @__PURE__ */ new Map();
197
+ const gSuiteChildren = new Multimap();
198
+ const gSuiteTests = new Multimap();
199
+ for (const env of report.environments) {
200
+ const envId = computeEnvId(env);
201
+ gEnvs.set(envId, env);
202
+ gEnvIds.set(env, envId);
203
+ }
204
+ const usedEnvIds = /* @__PURE__ */ new Set();
205
+ function visitTests2(tests, suiteId) {
206
+ for (const test of tests ?? []) {
207
+ const testId = computeTestId(test, suiteId);
208
+ gTests.set(testId, test);
209
+ gTestIds.set(test, testId);
210
+ gSuiteTests.set(suiteId, test);
211
+ for (const attempt of test.attempts) {
212
+ const env = report.environments[attempt.environmentIdx];
213
+ const envId = gEnvIds.get(env);
214
+ usedEnvIds.add(envId);
215
+ }
216
+ }
217
+ }
218
+ function visitSuite(suite, parentSuiteId) {
219
+ const suiteId = computeSuiteId(suite, parentSuiteId);
220
+ gSuites.set(suiteId, suite);
221
+ gSuiteIds.set(suite, suiteId);
222
+ for (const childSuite of suite.suites ?? []) {
223
+ visitSuite(childSuite, suiteId);
224
+ gSuiteChildren.set(suiteId, childSuite);
225
+ }
226
+ visitTests2(suite.tests ?? [], suiteId);
227
+ }
228
+ function transformTests(tests) {
229
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
230
+ return [...testIds].map((testId) => {
231
+ const tests2 = gTests.getAll(testId);
232
+ const tags = tests2.map((test) => test.tags ?? []).flat();
233
+ return {
234
+ location: tests2[0].location,
235
+ title: tests2[0].title,
236
+ tags: tags.length ? tags : void 0,
237
+ attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
238
+ ...attempt,
239
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
240
+ }))
241
+ };
242
+ });
243
+ }
244
+ function transformSuites(suites) {
245
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
246
+ return [...suiteIds].map((suiteId) => {
247
+ const suite = gSuites.get(suiteId);
248
+ return {
249
+ location: suite.location,
250
+ title: suite.title,
251
+ type: suite.type,
252
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
253
+ tests: transformTests(gSuiteTests.getAll(suiteId))
254
+ };
255
+ });
256
+ }
257
+ visitTests2(report.tests ?? [], "suiteless");
258
+ for (const suite of report.suites)
259
+ visitSuite(suite);
260
+ const newEnvironments = [...usedEnvIds];
261
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
262
+ return {
263
+ ...report,
264
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
265
+ suites: transformSuites(report.suites),
266
+ tests: transformTests(report.tests ?? [])
267
+ };
268
+ }
269
+ ReportUtils2.normalizeReport = normalizeReport;
270
+ function computeEnvId(env) {
271
+ return xxHashObject(env);
272
+ }
273
+ function computeSuiteId(suite, parentSuiteId) {
274
+ return xxHash([
275
+ parentSuiteId ?? "",
276
+ suite.type,
277
+ suite.location?.file ?? "",
278
+ suite.title
279
+ ]);
280
+ }
281
+ function computeTestId(test, suiteId) {
282
+ return xxHash([
283
+ suiteId,
284
+ test.location?.file ?? "",
285
+ test.title
286
+ ]);
287
+ }
288
+ })(ReportUtils || (ReportUtils = {}));
289
+
281
290
  // src/localReportApi.ts
282
291
  var ReportInfo = class {
283
292
  constructor(_options) {
@@ -287,7 +296,7 @@ var ReportInfo = class {
287
296
  attachmentIdToPath = /* @__PURE__ */ new Map();
288
297
  commits = [];
289
298
  async refresh() {
290
- const report = await fs3.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
299
+ const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
291
300
  if (!report) {
292
301
  this.report = void 0;
293
302
  this.commits = [];
@@ -297,7 +306,7 @@ var ReportInfo = class {
297
306
  if (JSON.stringify(report) === JSON.stringify(this.report))
298
307
  return;
299
308
  this.report = report;
300
- this.commits = await listLocalCommits(path3.dirname(this._options.reportPath), report.commitId, 100);
309
+ this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
301
310
  const attachmentsDir = this._options.attachmentsFolder;
302
311
  const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
303
312
  if (missingAttachments.length) {
@@ -331,7 +340,7 @@ var localReportRouter = {
331
340
  const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
332
341
  if (!idx)
333
342
  throw TypedHTTP.HttpError.withCode("NOT_FOUND");
334
- const buffer = await fs3.promises.readFile(idx.path);
343
+ const buffer = await fs2.promises.readFile(idx.path);
335
344
  return TypedHTTP.ok(buffer, idx.contentType);
336
345
  }
337
346
  }),
@@ -343,9 +352,43 @@ var localReportRouter = {
343
352
  })
344
353
  }
345
354
  };
355
+ async function resolveAttachmentPaths(report, attachmentsDir) {
356
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
357
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path2.basename(file), file]));
358
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
359
+ const missingAttachments = /* @__PURE__ */ new Set();
360
+ ReportUtils.visitTests(report, (test) => {
361
+ for (const attempt of test.attempts) {
362
+ for (const attachment of attempt.attachments ?? []) {
363
+ const attachmentPath = filenameToPath.get(attachment.id);
364
+ if (!attachmentPath) {
365
+ missingAttachments.add(attachment.id);
366
+ } else {
367
+ attachmentIdToPath.set(attachment.id, {
368
+ contentType: attachment.contentType,
369
+ id: attachment.id,
370
+ path: attachmentPath
371
+ });
372
+ }
373
+ }
374
+ }
375
+ });
376
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
377
+ }
378
+ async function listFilesRecursively(dir, result = []) {
379
+ const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
380
+ for (const entry of entries) {
381
+ const fullPath = path2.join(dir, entry.name);
382
+ if (entry.isDirectory())
383
+ await listFilesRecursively(fullPath, result);
384
+ else
385
+ result.push(fullPath);
386
+ }
387
+ return result;
388
+ }
346
389
 
347
390
  // src/localReportServer.ts
348
- var logHTTPServer = debug("fk:http");
391
+ var logHTTPServer = debug2("fk:http");
349
392
  var LocalReportServer = class _LocalReportServer {
350
393
  constructor(_server, _port, _authToken) {
351
394
  this._server = _server;
@@ -386,7 +429,7 @@ var LocalReportServer = class _LocalReportServer {
386
429
  logHTTPServer(err);
387
430
  res.status(500).send({ error: "Internal Server Error" });
388
431
  });
389
- const server = http2.createServer(app);
432
+ const server = http.createServer(app);
390
433
  server.on("error", (err) => {
391
434
  if (err.code === "ECONNRESET") {
392
435
  logHTTPServer("Client connection reset. Ignoring.");
@@ -412,7 +455,7 @@ var LocalReportServer = class _LocalReportServer {
412
455
 
413
456
  // src/showReport.ts
414
457
  async function showReport(reportFolder) {
415
- const reportPath = path4.join(reportFolder, "report.json");
458
+ const reportPath = path3.join(reportFolder, "report.json");
416
459
  const config = await FlakinessProjectConfig.load();
417
460
  const projectPublicId = config.projectPublicId();
418
461
  const reportViewerEndpoint = config.reportViewerEndpoint();