@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.
@@ -4,20 +4,22 @@ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
4
4
  import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
5
5
  import bodyParser from "body-parser";
6
6
  import compression from "compression";
7
- import debug from "debug";
7
+ import debug2 from "debug";
8
8
  import express from "express";
9
9
  import "express-async-errors";
10
- import http2 from "http";
10
+ import http from "http";
11
11
 
12
12
  // src/localReportApi.ts
13
13
  import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
14
- import fs2 from "fs";
15
- import path2 from "path";
14
+ import fs from "fs";
15
+ import path from "path";
16
16
  import { z } from "zod/v4";
17
17
 
18
18
  // src/localGit.ts
19
19
  import { exec } from "child_process";
20
+ import debug from "debug";
20
21
  import { promisify } from "util";
22
+ var log = debug("fk:git");
21
23
  var execAsync = promisify(exec);
22
24
  async function listLocalCommits(gitRoot, head, count) {
23
25
  const FIELD_SEPARATOR = "|~|";
@@ -53,136 +55,130 @@ async function listLocalCommits(gitRoot, head, count) {
53
55
  };
54
56
  });
55
57
  } catch (error) {
56
- console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
57
- throw error;
58
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
59
+ return [];
58
60
  }
59
61
  }
60
62
 
61
- // src/utils.ts
62
- import { ReportUtils } from "@flakiness/report";
63
- import fs from "fs";
64
- import http from "http";
65
- import https from "https";
66
- import path, { posix as posixPath, win32 as win32Path } from "path";
67
- var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
68
- function errorText(error) {
69
- return FLAKINESS_DBG ? error.stack : error.message;
70
- }
71
- async function retryWithBackoff(job, backoff = []) {
72
- for (const timeout of backoff) {
73
- try {
74
- return await job();
75
- } catch (e) {
76
- if (e instanceof AggregateError)
77
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
78
- else if (e instanceof Error)
79
- console.error(`[flakiness.io err]`, errorText(e));
80
- else
81
- console.error(`[flakiness.io err]`, e);
82
- await new Promise((x) => setTimeout(x, timeout));
63
+ // src/reportUtils.ts
64
+ import { Multimap } from "@flakiness/shared/common/multimap.js";
65
+ import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
66
+ var ReportUtils;
67
+ ((ReportUtils2) => {
68
+ function visitTests(report, testVisitor) {
69
+ function visitSuite(suite, parents) {
70
+ parents.push(suite);
71
+ for (const test of suite.tests ?? [])
72
+ testVisitor(test, parents);
73
+ for (const childSuite of suite.suites ?? [])
74
+ visitSuite(childSuite, parents);
75
+ parents.pop();
83
76
  }
77
+ for (const test of report.tests ?? [])
78
+ testVisitor(test, []);
79
+ for (const suite of report.suites)
80
+ visitSuite(suite, []);
84
81
  }
85
- return await job();
86
- }
87
- var httpUtils;
88
- ((httpUtils2) => {
89
- function createRequest({ url, method = "get", headers = {} }) {
90
- let resolve;
91
- let reject;
92
- const responseDataPromise = new Promise((a, b) => {
93
- resolve = a;
94
- reject = b;
95
- });
96
- const protocol = url.startsWith("https") ? https : http;
97
- headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
98
- const request = protocol.request(url, { method, headers }, (res) => {
99
- const chunks = [];
100
- res.on("data", (chunk) => chunks.push(chunk));
101
- res.on("end", () => {
102
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
103
- resolve(Buffer.concat(chunks));
104
- else
105
- reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
82
+ ReportUtils2.visitTests = visitTests;
83
+ function normalizeReport(report) {
84
+ const gEnvs = /* @__PURE__ */ new Map();
85
+ const gSuites = /* @__PURE__ */ new Map();
86
+ const gTests = new Multimap();
87
+ const gSuiteIds = /* @__PURE__ */ new Map();
88
+ const gTestIds = /* @__PURE__ */ new Map();
89
+ const gEnvIds = /* @__PURE__ */ new Map();
90
+ const gSuiteChildren = new Multimap();
91
+ const gSuiteTests = new Multimap();
92
+ for (const env of report.environments) {
93
+ const envId = computeEnvId(env);
94
+ gEnvs.set(envId, env);
95
+ gEnvIds.set(env, envId);
96
+ }
97
+ const usedEnvIds = /* @__PURE__ */ new Set();
98
+ function visitTests2(tests, suiteId) {
99
+ for (const test of tests ?? []) {
100
+ const testId = computeTestId(test, suiteId);
101
+ gTests.set(testId, test);
102
+ gTestIds.set(test, testId);
103
+ gSuiteTests.set(suiteId, test);
104
+ for (const attempt of test.attempts) {
105
+ const env = report.environments[attempt.environmentIdx];
106
+ const envId = gEnvIds.get(env);
107
+ usedEnvIds.add(envId);
108
+ }
109
+ }
110
+ }
111
+ function visitSuite(suite, parentSuiteId) {
112
+ const suiteId = computeSuiteId(suite, parentSuiteId);
113
+ gSuites.set(suiteId, suite);
114
+ gSuiteIds.set(suite, suiteId);
115
+ for (const childSuite of suite.suites ?? []) {
116
+ visitSuite(childSuite, suiteId);
117
+ gSuiteChildren.set(suiteId, childSuite);
118
+ }
119
+ visitTests2(suite.tests ?? [], suiteId);
120
+ }
121
+ function transformTests(tests) {
122
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
123
+ return [...testIds].map((testId) => {
124
+ const tests2 = gTests.getAll(testId);
125
+ const tags = tests2.map((test) => test.tags ?? []).flat();
126
+ return {
127
+ location: tests2[0].location,
128
+ title: tests2[0].title,
129
+ tags: tags.length ? tags : void 0,
130
+ attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
131
+ ...attempt,
132
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
133
+ }))
134
+ };
106
135
  });
107
- res.on("error", (error) => reject(error));
108
- });
109
- request.on("error", reject);
110
- return { request, responseDataPromise };
111
- }
112
- httpUtils2.createRequest = createRequest;
113
- async function getBuffer(url, backoff) {
114
- return await retryWithBackoff(async () => {
115
- const { request, responseDataPromise } = createRequest({ url });
116
- request.end();
117
- return await responseDataPromise;
118
- }, backoff);
119
- }
120
- httpUtils2.getBuffer = getBuffer;
121
- async function getText(url, backoff) {
122
- const buffer = await getBuffer(url, backoff);
123
- return buffer.toString("utf-8");
124
- }
125
- httpUtils2.getText = getText;
126
- async function getJSON(url) {
127
- return JSON.parse(await getText(url));
128
- }
129
- httpUtils2.getJSON = getJSON;
130
- async function postText(url, text, backoff) {
131
- const headers = {
132
- "Content-Type": "application/json",
133
- "Content-Length": Buffer.byteLength(text) + ""
136
+ }
137
+ function transformSuites(suites) {
138
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
139
+ return [...suiteIds].map((suiteId) => {
140
+ const suite = gSuites.get(suiteId);
141
+ return {
142
+ location: suite.location,
143
+ title: suite.title,
144
+ type: suite.type,
145
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
146
+ tests: transformTests(gSuiteTests.getAll(suiteId))
147
+ };
148
+ });
149
+ }
150
+ visitTests2(report.tests ?? [], "suiteless");
151
+ for (const suite of report.suites)
152
+ visitSuite(suite);
153
+ const newEnvironments = [...usedEnvIds];
154
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
155
+ return {
156
+ ...report,
157
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
158
+ suites: transformSuites(report.suites),
159
+ tests: transformTests(report.tests ?? [])
134
160
  };
135
- return await retryWithBackoff(async () => {
136
- const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
137
- request.write(text);
138
- request.end();
139
- return await responseDataPromise;
140
- }, backoff);
141
161
  }
142
- httpUtils2.postText = postText;
143
- async function postJSON(url, json, backoff) {
144
- const buffer = await postText(url, JSON.stringify(json), backoff);
145
- return JSON.parse(buffer.toString("utf-8"));
162
+ ReportUtils2.normalizeReport = normalizeReport;
163
+ function computeEnvId(env) {
164
+ return xxHashObject(env);
146
165
  }
147
- httpUtils2.postJSON = postJSON;
148
- })(httpUtils || (httpUtils = {}));
149
- 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");
150
- async function resolveAttachmentPaths(report, attachmentsDir) {
151
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
152
- const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
153
- const attachmentIdToPath = /* @__PURE__ */ new Map();
154
- const missingAttachments = /* @__PURE__ */ new Set();
155
- ReportUtils.visitTests(report, (test) => {
156
- for (const attempt of test.attempts) {
157
- for (const attachment of attempt.attachments ?? []) {
158
- const attachmentPath = filenameToPath.get(attachment.id);
159
- if (!attachmentPath) {
160
- missingAttachments.add(attachment.id);
161
- } else {
162
- attachmentIdToPath.set(attachment.id, {
163
- contentType: attachment.contentType,
164
- id: attachment.id,
165
- path: attachmentPath
166
- });
167
- }
168
- }
169
- }
170
- });
171
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
172
- }
173
- async function listFilesRecursively(dir, result = []) {
174
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
175
- for (const entry of entries) {
176
- const fullPath = path.join(dir, entry.name);
177
- if (entry.isDirectory())
178
- await listFilesRecursively(fullPath, result);
179
- else
180
- result.push(fullPath);
166
+ function computeSuiteId(suite, parentSuiteId) {
167
+ return xxHash([
168
+ parentSuiteId ?? "",
169
+ suite.type,
170
+ suite.location?.file ?? "",
171
+ suite.title
172
+ ]);
181
173
  }
182
- return result;
183
- }
184
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
185
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
174
+ function computeTestId(test, suiteId) {
175
+ return xxHash([
176
+ suiteId,
177
+ test.location?.file ?? "",
178
+ test.title
179
+ ]);
180
+ }
181
+ })(ReportUtils || (ReportUtils = {}));
186
182
 
187
183
  // src/localReportApi.ts
188
184
  var ReportInfo = class {
@@ -193,7 +189,7 @@ var ReportInfo = class {
193
189
  attachmentIdToPath = /* @__PURE__ */ new Map();
194
190
  commits = [];
195
191
  async refresh() {
196
- const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
192
+ const report = await fs.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
197
193
  if (!report) {
198
194
  this.report = void 0;
199
195
  this.commits = [];
@@ -203,7 +199,7 @@ var ReportInfo = class {
203
199
  if (JSON.stringify(report) === JSON.stringify(this.report))
204
200
  return;
205
201
  this.report = report;
206
- this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
202
+ this.commits = await listLocalCommits(path.dirname(this._options.reportPath), report.commitId, 100);
207
203
  const attachmentsDir = this._options.attachmentsFolder;
208
204
  const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
209
205
  if (missingAttachments.length) {
@@ -237,7 +233,7 @@ var localReportRouter = {
237
233
  const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
238
234
  if (!idx)
239
235
  throw TypedHTTP.HttpError.withCode("NOT_FOUND");
240
- const buffer = await fs2.promises.readFile(idx.path);
236
+ const buffer = await fs.promises.readFile(idx.path);
241
237
  return TypedHTTP.ok(buffer, idx.contentType);
242
238
  }
243
239
  }),
@@ -249,9 +245,43 @@ var localReportRouter = {
249
245
  })
250
246
  }
251
247
  };
248
+ async function resolveAttachmentPaths(report, attachmentsDir) {
249
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
250
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
251
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
252
+ const missingAttachments = /* @__PURE__ */ new Set();
253
+ ReportUtils.visitTests(report, (test) => {
254
+ for (const attempt of test.attempts) {
255
+ for (const attachment of attempt.attachments ?? []) {
256
+ const attachmentPath = filenameToPath.get(attachment.id);
257
+ if (!attachmentPath) {
258
+ missingAttachments.add(attachment.id);
259
+ } else {
260
+ attachmentIdToPath.set(attachment.id, {
261
+ contentType: attachment.contentType,
262
+ id: attachment.id,
263
+ path: attachmentPath
264
+ });
265
+ }
266
+ }
267
+ }
268
+ });
269
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
270
+ }
271
+ async function listFilesRecursively(dir, result = []) {
272
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
273
+ for (const entry of entries) {
274
+ const fullPath = path.join(dir, entry.name);
275
+ if (entry.isDirectory())
276
+ await listFilesRecursively(fullPath, result);
277
+ else
278
+ result.push(fullPath);
279
+ }
280
+ return result;
281
+ }
252
282
 
253
283
  // src/localReportServer.ts
254
- var logHTTPServer = debug("fk:http");
284
+ var logHTTPServer = debug2("fk:http");
255
285
  var LocalReportServer = class _LocalReportServer {
256
286
  constructor(_server, _port, _authToken) {
257
287
  this._server = _server;
@@ -292,7 +322,7 @@ var LocalReportServer = class _LocalReportServer {
292
322
  logHTTPServer(err);
293
323
  res.status(500).send({ error: "Internal Server Error" });
294
324
  });
295
- const server = http2.createServer(app);
325
+ const server = http.createServer(app);
296
326
  server.on("error", (err) => {
297
327
  if (err.code === "ECONNRESET") {
298
328
  logHTTPServer("Client connection reset. Ignoring.");
@@ -0,0 +1,20 @@
1
+ // src/pathutils.ts
2
+ import { posix as posixPath, win32 as win32Path } from "path";
3
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
4
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
5
+ function normalizePath(aPath) {
6
+ if (IS_WIN32_PATH.test(aPath)) {
7
+ aPath = aPath.split(win32Path.sep).join(posixPath.sep);
8
+ }
9
+ if (IS_ALMOST_POSIX_PATH.test(aPath))
10
+ return "/" + aPath[0] + aPath.substring(2);
11
+ return aPath;
12
+ }
13
+ function gitFilePath(gitRoot, absolutePath) {
14
+ return posixPath.relative(gitRoot, absolutePath);
15
+ }
16
+ export {
17
+ gitFilePath,
18
+ normalizePath
19
+ };
20
+ //# sourceMappingURL=pathutils.js.map
@@ -1,16 +1,15 @@
1
1
  // src/reportUploader.ts
2
2
  import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
3
3
  import assert from "assert";
4
+ import crypto from "crypto";
4
5
  import fs from "fs";
5
6
  import { URL } from "url";
6
7
 
7
- // src/serverapi.ts
8
- import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
9
-
10
- // src/utils.ts
11
- import { ReportUtils } from "@flakiness/report";
8
+ // src/httpUtils.ts
12
9
  import http from "http";
13
10
  import https from "https";
11
+
12
+ // src/utils.ts
14
13
  var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
15
14
  function errorText(error) {
16
15
  return FLAKINESS_DBG ? error.stack : error.message;
@@ -31,6 +30,10 @@ async function retryWithBackoff(job, backoff = []) {
31
30
  }
32
31
  return await job();
33
32
  }
33
+ 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");
34
+
35
+ // src/httpUtils.ts
36
+ var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
34
37
  var httpUtils;
35
38
  ((httpUtils2) => {
36
39
  function createRequest({ url, method = "get", headers = {} }) {
@@ -93,26 +96,40 @@ var httpUtils;
93
96
  }
94
97
  httpUtils2.postJSON = postJSON;
95
98
  })(httpUtils || (httpUtils = {}));
96
- 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");
97
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
98
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
99
-
100
- // src/serverapi.ts
101
- function createServerAPI(endpoint, options) {
102
- endpoint += "/api/";
103
- const fetcher = options?.auth ? (url, init) => fetch(url, {
104
- ...init,
105
- headers: {
106
- ...init.headers,
107
- "Authorization": `Bearer ${options.auth}`
108
- }
109
- }) : fetch;
110
- if (options?.retries)
111
- return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
112
- return TypedHTTP.createClient(endpoint, fetcher);
113
- }
114
99
 
115
100
  // src/reportUploader.ts
101
+ function sha1File(filePath) {
102
+ return new Promise((resolve, reject) => {
103
+ const hash = crypto.createHash("sha1");
104
+ const stream = fs.createReadStream(filePath);
105
+ stream.on("data", (chunk) => {
106
+ hash.update(chunk);
107
+ });
108
+ stream.on("end", () => {
109
+ resolve(hash.digest("hex"));
110
+ });
111
+ stream.on("error", (err) => {
112
+ reject(err);
113
+ });
114
+ });
115
+ }
116
+ async function createFileAttachment(contentType, filePath) {
117
+ return {
118
+ contentType,
119
+ id: await sha1File(filePath),
120
+ path: filePath
121
+ };
122
+ }
123
+ async function createDataAttachment(contentType, data) {
124
+ const hash = crypto.createHash("sha1");
125
+ hash.update(data);
126
+ const id = hash.digest("hex");
127
+ return {
128
+ contentType,
129
+ id,
130
+ body: data
131
+ };
132
+ }
116
133
  var ReportUploader = class _ReportUploader {
117
134
  static optionsFromEnv(overrides) {
118
135
  const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
@@ -153,33 +170,54 @@ var ReportUpload = class {
153
170
  _report;
154
171
  _attachments;
155
172
  _options;
156
- _api;
157
173
  constructor(options, report, attachments) {
158
174
  this._options = options;
159
175
  this._report = report;
160
176
  this._attachments = attachments;
161
- this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
177
+ }
178
+ async _api(pathname, token, body) {
179
+ const url = new URL(this._options.flakinessEndpoint);
180
+ url.pathname = pathname;
181
+ return await fetch(url, {
182
+ method: "POST",
183
+ headers: {
184
+ "Authorization": `Bearer ${token}`,
185
+ "Content-Type": "application/json"
186
+ },
187
+ body: body ? JSON.stringify(body) : void 0
188
+ }).then(async (response) => !response.ok ? {
189
+ result: void 0,
190
+ error: response.status + " " + url.href + " " + await response.text()
191
+ } : {
192
+ result: await response.json(),
193
+ error: void 0
194
+ }).catch((error) => ({
195
+ result: void 0,
196
+ error
197
+ }));
162
198
  }
163
199
  async upload(options) {
164
- const response = await this._api.run.startUpload.POST({
165
- attachmentIds: this._attachments.map((attachment) => attachment.id)
166
- }).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
200
+ const response = await this._api("/api/upload/start", this._options.flakinessAccessToken);
167
201
  if (response?.error || !response.result)
168
- return { success: false, message: `flakiness.io returned error: ${response.error.message}` };
202
+ return { success: false, message: response.error };
203
+ const webUrl = new URL(response.result.webUrl, this._options.flakinessEndpoint).toString();
204
+ const attachmentsPresignedUrls = await this._api("/api/upload/attachments", response.result.uploadToken, {
205
+ attachmentIds: this._attachments.map((a) => a.id)
206
+ });
207
+ if (attachmentsPresignedUrls?.error || !attachmentsPresignedUrls.result)
208
+ return { success: false, message: attachmentsPresignedUrls.error };
209
+ const attachments = new Map(attachmentsPresignedUrls.result.map((a) => [a.attachmentId, a.presignedUrl]));
169
210
  await Promise.all([
170
- this._uploadReport(JSON.stringify(this._report), response.result.report_upload_url, options?.syncCompression ?? false),
211
+ this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl, options?.syncCompression ?? false),
171
212
  ...this._attachments.map((attachment) => {
172
- const uploadURL = response.result.attachment_upload_urls[attachment.id];
213
+ const uploadURL = attachments.get(attachment.id);
173
214
  if (!uploadURL)
174
215
  throw new Error("Internal error: missing upload URL for attachment!");
175
216
  return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
176
217
  })
177
218
  ]);
178
- const response2 = await this._api.run.completeUpload.POST({
179
- upload_token: response.result.upload_token
180
- }).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
181
- const url = response2?.result?.report_url ? new URL(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
182
- return { success: true, reportUrl: url };
219
+ await this._api("/api/upload/finish", response.result.uploadToken);
220
+ return { success: true, reportUrl: webUrl };
183
221
  }
184
222
  async _uploadReport(data, uploadUrl, syncCompression) {
185
223
  const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
@@ -241,6 +279,8 @@ var ReportUpload = class {
241
279
  }
242
280
  };
243
281
  export {
244
- ReportUploader
282
+ ReportUploader,
283
+ createDataAttachment,
284
+ createFileAttachment
245
285
  };
246
286
  //# sourceMappingURL=reportUploader.js.map
@@ -0,0 +1,123 @@
1
+ // src/reportUtils.ts
2
+ import { Multimap } from "@flakiness/shared/common/multimap.js";
3
+ import { xxHash, xxHashObject } from "@flakiness/shared/common/utils.js";
4
+ var ReportUtils;
5
+ ((ReportUtils2) => {
6
+ function visitTests(report, testVisitor) {
7
+ function visitSuite(suite, parents) {
8
+ parents.push(suite);
9
+ for (const test of suite.tests ?? [])
10
+ testVisitor(test, parents);
11
+ for (const childSuite of suite.suites ?? [])
12
+ visitSuite(childSuite, parents);
13
+ parents.pop();
14
+ }
15
+ for (const test of report.tests ?? [])
16
+ testVisitor(test, []);
17
+ for (const suite of report.suites)
18
+ visitSuite(suite, []);
19
+ }
20
+ ReportUtils2.visitTests = visitTests;
21
+ function normalizeReport(report) {
22
+ const gEnvs = /* @__PURE__ */ new Map();
23
+ const gSuites = /* @__PURE__ */ new Map();
24
+ const gTests = new Multimap();
25
+ const gSuiteIds = /* @__PURE__ */ new Map();
26
+ const gTestIds = /* @__PURE__ */ new Map();
27
+ const gEnvIds = /* @__PURE__ */ new Map();
28
+ const gSuiteChildren = new Multimap();
29
+ const gSuiteTests = new Multimap();
30
+ for (const env of report.environments) {
31
+ const envId = computeEnvId(env);
32
+ gEnvs.set(envId, env);
33
+ gEnvIds.set(env, envId);
34
+ }
35
+ const usedEnvIds = /* @__PURE__ */ new Set();
36
+ function visitTests2(tests, suiteId) {
37
+ for (const test of tests ?? []) {
38
+ const testId = computeTestId(test, suiteId);
39
+ gTests.set(testId, test);
40
+ gTestIds.set(test, testId);
41
+ gSuiteTests.set(suiteId, test);
42
+ for (const attempt of test.attempts) {
43
+ const env = report.environments[attempt.environmentIdx];
44
+ const envId = gEnvIds.get(env);
45
+ usedEnvIds.add(envId);
46
+ }
47
+ }
48
+ }
49
+ function visitSuite(suite, parentSuiteId) {
50
+ const suiteId = computeSuiteId(suite, parentSuiteId);
51
+ gSuites.set(suiteId, suite);
52
+ gSuiteIds.set(suite, suiteId);
53
+ for (const childSuite of suite.suites ?? []) {
54
+ visitSuite(childSuite, suiteId);
55
+ gSuiteChildren.set(suiteId, childSuite);
56
+ }
57
+ visitTests2(suite.tests ?? [], suiteId);
58
+ }
59
+ function transformTests(tests) {
60
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
61
+ return [...testIds].map((testId) => {
62
+ const tests2 = gTests.getAll(testId);
63
+ const tags = tests2.map((test) => test.tags ?? []).flat();
64
+ return {
65
+ location: tests2[0].location,
66
+ title: tests2[0].title,
67
+ tags: tags.length ? tags : void 0,
68
+ attempts: tests2.map((t) => t.attempts).flat().map((attempt) => ({
69
+ ...attempt,
70
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
71
+ }))
72
+ };
73
+ });
74
+ }
75
+ function transformSuites(suites) {
76
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
77
+ return [...suiteIds].map((suiteId) => {
78
+ const suite = gSuites.get(suiteId);
79
+ return {
80
+ location: suite.location,
81
+ title: suite.title,
82
+ type: suite.type,
83
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
84
+ tests: transformTests(gSuiteTests.getAll(suiteId))
85
+ };
86
+ });
87
+ }
88
+ visitTests2(report.tests ?? [], "suiteless");
89
+ for (const suite of report.suites)
90
+ visitSuite(suite);
91
+ const newEnvironments = [...usedEnvIds];
92
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
93
+ return {
94
+ ...report,
95
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
96
+ suites: transformSuites(report.suites),
97
+ tests: transformTests(report.tests ?? [])
98
+ };
99
+ }
100
+ ReportUtils2.normalizeReport = normalizeReport;
101
+ function computeEnvId(env) {
102
+ return xxHashObject(env);
103
+ }
104
+ function computeSuiteId(suite, parentSuiteId) {
105
+ return xxHash([
106
+ parentSuiteId ?? "",
107
+ suite.type,
108
+ suite.location?.file ?? "",
109
+ suite.title
110
+ ]);
111
+ }
112
+ function computeTestId(test, suiteId) {
113
+ return xxHash([
114
+ suiteId,
115
+ test.location?.file ?? "",
116
+ test.title
117
+ ]);
118
+ }
119
+ })(ReportUtils || (ReportUtils = {}));
120
+ export {
121
+ ReportUtils
122
+ };
123
+ //# sourceMappingURL=reportUtils.js.map