@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/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
|
-
|
|
40
|
-
|
|
41
|
+
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
42
|
+
return [];
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
export {
|
package/lib/localReportApi.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// src/localReportApi.ts
|
|
2
2
|
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
-
|
|
46
|
-
|
|
47
|
+
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
48
|
+
return [];
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
// src/
|
|
51
|
-
import {
|
|
52
|
-
import
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
151
|
+
ReportUtils2.normalizeReport = normalizeReport;
|
|
152
|
+
function computeEnvId(env) {
|
|
153
|
+
return xxHashObject(env);
|
|
135
154
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|