@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.
package/lib/localGit.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // src/localGit.ts
2
2
  import { exec } from "child_process";
3
+ import debug from "debug";
3
4
  import { promisify } from "util";
5
+ var log = debug("fk:git");
4
6
  var execAsync = promisify(exec);
5
7
  async function listLocalCommits(gitRoot, head, count) {
6
8
  const FIELD_SEPARATOR = "|~|";
@@ -36,8 +38,8 @@ async function listLocalCommits(gitRoot, head, count) {
36
38
  };
37
39
  });
38
40
  } catch (error) {
39
- console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
40
- throw error;
41
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
42
+ return [];
41
43
  }
42
44
  }
43
45
  export {
@@ -1,12 +1,14 @@
1
1
  // src/localReportApi.ts
2
2
  import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
3
- import fs2 from "fs";
4
- import path2 from "path";
3
+ import fs from "fs";
4
+ import path from "path";
5
5
  import { z } from "zod/v4";
6
6
 
7
7
  // src/localGit.ts
8
8
  import { exec } from "child_process";
9
+ import debug from "debug";
9
10
  import { promisify } from "util";
11
+ var log = debug("fk:git");
10
12
  var execAsync = promisify(exec);
11
13
  async function listLocalCommits(gitRoot, head, count) {
12
14
  const FIELD_SEPARATOR = "|~|";
@@ -42,136 +44,130 @@ async function listLocalCommits(gitRoot, head, count) {
42
44
  };
43
45
  });
44
46
  } catch (error) {
45
- console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
46
- throw error;
47
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
48
+ return [];
47
49
  }
48
50
  }
49
51
 
50
- // src/utils.ts
51
- import { ReportUtils } from "@flakiness/report";
52
- import fs from "fs";
53
- import http from "http";
54
- import https from "https";
55
- import path, { posix as posixPath, win32 as win32Path } from "path";
56
- var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
57
- function errorText(error) {
58
- return FLAKINESS_DBG ? error.stack : error.message;
59
- }
60
- async function retryWithBackoff(job, backoff = []) {
61
- for (const timeout of backoff) {
62
- try {
63
- return await job();
64
- } catch (e) {
65
- if (e instanceof AggregateError)
66
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
67
- else if (e instanceof Error)
68
- console.error(`[flakiness.io err]`, errorText(e));
69
- else
70
- console.error(`[flakiness.io err]`, e);
71
- await new Promise((x) => setTimeout(x, timeout));
52
+ // src/reportUtils.ts
53
+ import { Multimap } from "@flakiness/shared/common/multimap.js";
54
+ import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
55
+ var ReportUtils;
56
+ ((ReportUtils2) => {
57
+ function visitTests(report, testVisitor) {
58
+ function visitSuite(suite, parents) {
59
+ parents.push(suite);
60
+ for (const test of suite.tests ?? [])
61
+ testVisitor(test, parents);
62
+ for (const childSuite of suite.suites ?? [])
63
+ visitSuite(childSuite, parents);
64
+ parents.pop();
72
65
  }
66
+ for (const test of report.tests ?? [])
67
+ testVisitor(test, []);
68
+ for (const suite of report.suites)
69
+ visitSuite(suite, []);
73
70
  }
74
- return await job();
75
- }
76
- var httpUtils;
77
- ((httpUtils2) => {
78
- function createRequest({ url, method = "get", headers = {} }) {
79
- let resolve;
80
- let reject;
81
- const responseDataPromise = new Promise((a, b) => {
82
- resolve = a;
83
- reject = b;
84
- });
85
- const protocol = url.startsWith("https") ? https : http;
86
- headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
87
- const request = protocol.request(url, { method, headers }, (res) => {
88
- const chunks = [];
89
- res.on("data", (chunk) => chunks.push(chunk));
90
- res.on("end", () => {
91
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
92
- resolve(Buffer.concat(chunks));
93
- else
94
- reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
71
+ ReportUtils2.visitTests = visitTests;
72
+ function normalizeReport(report) {
73
+ const gEnvs = /* @__PURE__ */ new Map();
74
+ const gSuites = /* @__PURE__ */ new Map();
75
+ const gTests = new Multimap();
76
+ const gSuiteIds = /* @__PURE__ */ new Map();
77
+ const gTestIds = /* @__PURE__ */ new Map();
78
+ const gEnvIds = /* @__PURE__ */ new Map();
79
+ const gSuiteChildren = new Multimap();
80
+ const gSuiteTests = new Multimap();
81
+ for (const env of report.environments) {
82
+ const envId = computeEnvId(env);
83
+ gEnvs.set(envId, env);
84
+ gEnvIds.set(env, envId);
85
+ }
86
+ const usedEnvIds = /* @__PURE__ */ new Set();
87
+ function visitTests2(tests, suiteId) {
88
+ for (const test of tests ?? []) {
89
+ const testId = computeTestId(test, suiteId);
90
+ gTests.set(testId, test);
91
+ gTestIds.set(test, testId);
92
+ gSuiteTests.set(suiteId, test);
93
+ for (const attempt of test.attempts) {
94
+ const env = report.environments[attempt.environmentIdx];
95
+ const envId = gEnvIds.get(env);
96
+ usedEnvIds.add(envId);
97
+ }
98
+ }
99
+ }
100
+ function visitSuite(suite, parentSuiteId) {
101
+ const suiteId = computeSuiteId(suite, parentSuiteId);
102
+ gSuites.set(suiteId, suite);
103
+ gSuiteIds.set(suite, suiteId);
104
+ for (const childSuite of suite.suites ?? []) {
105
+ visitSuite(childSuite, suiteId);
106
+ gSuiteChildren.set(suiteId, childSuite);
107
+ }
108
+ visitTests2(suite.tests ?? [], suiteId);
109
+ }
110
+ function transformTests(tests) {
111
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
112
+ return [...testIds].map((testId) => {
113
+ const tests2 = gTests.getAll(testId);
114
+ const tags = tests2.map((test) => test.tags ?? []).flat();
115
+ return {
116
+ location: tests2[0].location,
117
+ title: tests2[0].title,
118
+ tags: tags.length ? tags : void 0,
119
+ attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
120
+ ...attempt,
121
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
122
+ }))
123
+ };
95
124
  });
96
- res.on("error", (error) => reject(error));
97
- });
98
- request.on("error", reject);
99
- return { request, responseDataPromise };
100
- }
101
- httpUtils2.createRequest = createRequest;
102
- async function getBuffer(url, backoff) {
103
- return await retryWithBackoff(async () => {
104
- const { request, responseDataPromise } = createRequest({ url });
105
- request.end();
106
- return await responseDataPromise;
107
- }, backoff);
108
- }
109
- httpUtils2.getBuffer = getBuffer;
110
- async function getText(url, backoff) {
111
- const buffer = await getBuffer(url, backoff);
112
- return buffer.toString("utf-8");
113
- }
114
- httpUtils2.getText = getText;
115
- async function getJSON(url) {
116
- return JSON.parse(await getText(url));
117
- }
118
- httpUtils2.getJSON = getJSON;
119
- async function postText(url, text, backoff) {
120
- const headers = {
121
- "Content-Type": "application/json",
122
- "Content-Length": Buffer.byteLength(text) + ""
125
+ }
126
+ function transformSuites(suites) {
127
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
128
+ return [...suiteIds].map((suiteId) => {
129
+ const suite = gSuites.get(suiteId);
130
+ return {
131
+ location: suite.location,
132
+ title: suite.title,
133
+ type: suite.type,
134
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
135
+ tests: transformTests(gSuiteTests.getAll(suiteId))
136
+ };
137
+ });
138
+ }
139
+ visitTests2(report.tests ?? [], "suiteless");
140
+ for (const suite of report.suites)
141
+ visitSuite(suite);
142
+ const newEnvironments = [...usedEnvIds];
143
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
144
+ return {
145
+ ...report,
146
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
147
+ suites: transformSuites(report.suites),
148
+ tests: transformTests(report.tests ?? [])
123
149
  };
124
- return await retryWithBackoff(async () => {
125
- const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
126
- request.write(text);
127
- request.end();
128
- return await responseDataPromise;
129
- }, backoff);
130
150
  }
131
- httpUtils2.postText = postText;
132
- async function postJSON(url, json, backoff) {
133
- const buffer = await postText(url, JSON.stringify(json), backoff);
134
- return JSON.parse(buffer.toString("utf-8"));
151
+ ReportUtils2.normalizeReport = normalizeReport;
152
+ function computeEnvId(env) {
153
+ return xxHashObject(env);
135
154
  }
136
- httpUtils2.postJSON = postJSON;
137
- })(httpUtils || (httpUtils = {}));
138
- 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");
139
- async function resolveAttachmentPaths(report, attachmentsDir) {
140
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
141
- const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
142
- const attachmentIdToPath = /* @__PURE__ */ new Map();
143
- const missingAttachments = /* @__PURE__ */ new Set();
144
- ReportUtils.visitTests(report, (test) => {
145
- for (const attempt of test.attempts) {
146
- for (const attachment of attempt.attachments ?? []) {
147
- const attachmentPath = filenameToPath.get(attachment.id);
148
- if (!attachmentPath) {
149
- missingAttachments.add(attachment.id);
150
- } else {
151
- attachmentIdToPath.set(attachment.id, {
152
- contentType: attachment.contentType,
153
- id: attachment.id,
154
- path: attachmentPath
155
- });
156
- }
157
- }
158
- }
159
- });
160
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
161
- }
162
- async function listFilesRecursively(dir, result = []) {
163
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
164
- for (const entry of entries) {
165
- const fullPath = path.join(dir, entry.name);
166
- if (entry.isDirectory())
167
- await listFilesRecursively(fullPath, result);
168
- else
169
- result.push(fullPath);
155
+ function computeSuiteId(suite, parentSuiteId) {
156
+ return xxHash([
157
+ parentSuiteId ?? "",
158
+ suite.type,
159
+ suite.location?.file ?? "",
160
+ suite.title
161
+ ]);
170
162
  }
171
- return result;
172
- }
173
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
174
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
163
+ function computeTestId(test, suiteId) {
164
+ return xxHash([
165
+ suiteId,
166
+ test.location?.file ?? "",
167
+ test.title
168
+ ]);
169
+ }
170
+ })(ReportUtils || (ReportUtils = {}));
175
171
 
176
172
  // src/localReportApi.ts
177
173
  var ReportInfo = class {
@@ -182,7 +178,7 @@ var ReportInfo = class {
182
178
  attachmentIdToPath = /* @__PURE__ */ new Map();
183
179
  commits = [];
184
180
  async refresh() {
185
- const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
181
+ const report = await fs.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
186
182
  if (!report) {
187
183
  this.report = void 0;
188
184
  this.commits = [];
@@ -192,7 +188,7 @@ var ReportInfo = class {
192
188
  if (JSON.stringify(report) === JSON.stringify(this.report))
193
189
  return;
194
190
  this.report = report;
195
- this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
191
+ this.commits = await listLocalCommits(path.dirname(this._options.reportPath), report.commitId, 100);
196
192
  const attachmentsDir = this._options.attachmentsFolder;
197
193
  const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
198
194
  if (missingAttachments.length) {
@@ -226,7 +222,7 @@ var localReportRouter = {
226
222
  const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
227
223
  if (!idx)
228
224
  throw TypedHTTP.HttpError.withCode("NOT_FOUND");
229
- const buffer = await fs2.promises.readFile(idx.path);
225
+ const buffer = await fs.promises.readFile(idx.path);
230
226
  return TypedHTTP.ok(buffer, idx.contentType);
231
227
  }
232
228
  }),
@@ -238,6 +234,40 @@ var localReportRouter = {
238
234
  })
239
235
  }
240
236
  };
237
+ async function resolveAttachmentPaths(report, attachmentsDir) {
238
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
239
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
240
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
241
+ const missingAttachments = /* @__PURE__ */ new Set();
242
+ ReportUtils.visitTests(report, (test) => {
243
+ for (const attempt of test.attempts) {
244
+ for (const attachment of attempt.attachments ?? []) {
245
+ const attachmentPath = filenameToPath.get(attachment.id);
246
+ if (!attachmentPath) {
247
+ missingAttachments.add(attachment.id);
248
+ } else {
249
+ attachmentIdToPath.set(attachment.id, {
250
+ contentType: attachment.contentType,
251
+ id: attachment.id,
252
+ path: attachmentPath
253
+ });
254
+ }
255
+ }
256
+ }
257
+ });
258
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
259
+ }
260
+ async function listFilesRecursively(dir, result = []) {
261
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
262
+ for (const entry of entries) {
263
+ const fullPath = path.join(dir, entry.name);
264
+ if (entry.isDirectory())
265
+ await listFilesRecursively(fullPath, result);
266
+ else
267
+ result.push(fullPath);
268
+ }
269
+ return result;
270
+ }
241
271
  export {
242
272
  ReportInfo,
243
273
  localReportRouter