@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/LICENSE +21 -45
- package/README.md +2 -27
- package/lib/browser/index.js +127 -0
- package/lib/createEnvironment.js +78 -0
- package/lib/createTestStepSnippets.js +142 -4
- package/lib/flakinessProjectConfig.js +22 -100
- package/lib/git.js +55 -0
- package/lib/{serverapi.js → httpUtils.js} +13 -25
- package/lib/{playwright-test.js → index.js} +746 -814
- package/lib/localGit.js +4 -2
- package/lib/localReportApi.js +155 -125
- package/lib/localReportServer.js +159 -129
- package/lib/pathutils.js +20 -0
- package/lib/reportUploader.js +77 -37
- package/lib/reportUtils.js +123 -0
- package/lib/showReport.js +195 -152
- package/lib/utils.js +0 -330
- package/package.json +12 -38
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/localReportServer.js
CHANGED
|
@@ -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
|
|
7
|
+
import debug2 from "debug";
|
|
8
8
|
import express from "express";
|
|
9
9
|
import "express-async-errors";
|
|
10
|
-
import
|
|
10
|
+
import http from "http";
|
|
11
11
|
|
|
12
12
|
// src/localReportApi.ts
|
|
13
13
|
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
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
|
-
|
|
57
|
-
|
|
58
|
+
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
59
|
+
return [];
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
// src/
|
|
62
|
-
import {
|
|
63
|
-
import
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
162
|
+
ReportUtils2.normalizeReport = normalizeReport;
|
|
163
|
+
function computeEnvId(env) {
|
|
164
|
+
return xxHashObject(env);
|
|
146
165
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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.");
|
package/lib/pathutils.js
ADDED
|
@@ -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
|
package/lib/reportUploader.js
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
211
|
+
this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl, options?.syncCompression ?? false),
|
|
171
212
|
...this._attachments.map((attachment) => {
|
|
172
|
-
const uploadURL =
|
|
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
|
-
|
|
179
|
-
|
|
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
|