@flakiness/sdk 0.145.0 → 0.146.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/{flakinessConfig.js → flakinessProjectConfig.js} +10 -17
- package/lib/playwright-test.js +243 -294
- package/lib/{cli/cmd-show-report.js → showReport.js} +37 -106
- package/package.json +14 -17
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cli.js +0 -2382
- package/lib/cli/cmd-convert.js +0 -421
- package/lib/cli/cmd-download.js +0 -42
- package/lib/cli/cmd-link.js +0 -207
- package/lib/cli/cmd-login.js +0 -223
- package/lib/cli/cmd-logout.js +0 -170
- package/lib/cli/cmd-status.js +0 -273
- package/lib/cli/cmd-unlink.js +0 -199
- package/lib/cli/cmd-upload-playwright-json.js +0 -614
- package/lib/cli/cmd-upload.js +0 -321
- package/lib/cli/cmd-whoami.js +0 -173
- package/lib/flakinessSession.js +0 -159
- package/lib/junit.js +0 -310
- package/lib/playwrightJSONReport.js +0 -430
package/lib/cli/cmd-convert.js
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli/cmd-convert.ts
|
|
4
|
-
import fs3 from "fs/promises";
|
|
5
|
-
import path3 from "path";
|
|
6
|
-
|
|
7
|
-
// src/junit.ts
|
|
8
|
-
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
9
|
-
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
10
|
-
import assert2 from "assert";
|
|
11
|
-
import fs2 from "fs";
|
|
12
|
-
import path2 from "path";
|
|
13
|
-
|
|
14
|
-
// src/utils.ts
|
|
15
|
-
import { ReportUtils } from "@flakiness/report";
|
|
16
|
-
import assert from "assert";
|
|
17
|
-
import { spawnSync } from "child_process";
|
|
18
|
-
import crypto from "crypto";
|
|
19
|
-
import fs from "fs";
|
|
20
|
-
import http from "http";
|
|
21
|
-
import https from "https";
|
|
22
|
-
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
23
|
-
function sha1File(filePath) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const hash = crypto.createHash("sha1");
|
|
26
|
-
const stream = fs.createReadStream(filePath);
|
|
27
|
-
stream.on("data", (chunk) => {
|
|
28
|
-
hash.update(chunk);
|
|
29
|
-
});
|
|
30
|
-
stream.on("end", () => {
|
|
31
|
-
resolve(hash.digest("hex"));
|
|
32
|
-
});
|
|
33
|
-
stream.on("error", (err) => {
|
|
34
|
-
reject(err);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
39
|
-
function errorText(error) {
|
|
40
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
41
|
-
}
|
|
42
|
-
function sha1Buffer(data) {
|
|
43
|
-
const hash = crypto.createHash("sha1");
|
|
44
|
-
hash.update(data);
|
|
45
|
-
return hash.digest("hex");
|
|
46
|
-
}
|
|
47
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
48
|
-
for (const timeout of backoff) {
|
|
49
|
-
try {
|
|
50
|
-
return await job();
|
|
51
|
-
} catch (e) {
|
|
52
|
-
if (e instanceof AggregateError)
|
|
53
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
54
|
-
else if (e instanceof Error)
|
|
55
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
56
|
-
else
|
|
57
|
-
console.error(`[flakiness.io err]`, e);
|
|
58
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return await job();
|
|
62
|
-
}
|
|
63
|
-
var httpUtils;
|
|
64
|
-
((httpUtils2) => {
|
|
65
|
-
function createRequest({ url, method = "get", headers = {} }) {
|
|
66
|
-
let resolve;
|
|
67
|
-
let reject;
|
|
68
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
69
|
-
resolve = a;
|
|
70
|
-
reject = b;
|
|
71
|
-
});
|
|
72
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
73
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
74
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
75
|
-
const chunks = [];
|
|
76
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
77
|
-
res.on("end", () => {
|
|
78
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
79
|
-
resolve(Buffer.concat(chunks));
|
|
80
|
-
else
|
|
81
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
82
|
-
});
|
|
83
|
-
res.on("error", (error) => reject(error));
|
|
84
|
-
});
|
|
85
|
-
request.on("error", reject);
|
|
86
|
-
return { request, responseDataPromise };
|
|
87
|
-
}
|
|
88
|
-
httpUtils2.createRequest = createRequest;
|
|
89
|
-
async function getBuffer(url, backoff) {
|
|
90
|
-
return await retryWithBackoff(async () => {
|
|
91
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
92
|
-
request.end();
|
|
93
|
-
return await responseDataPromise;
|
|
94
|
-
}, backoff);
|
|
95
|
-
}
|
|
96
|
-
httpUtils2.getBuffer = getBuffer;
|
|
97
|
-
async function getText(url, backoff) {
|
|
98
|
-
const buffer = await getBuffer(url, backoff);
|
|
99
|
-
return buffer.toString("utf-8");
|
|
100
|
-
}
|
|
101
|
-
httpUtils2.getText = getText;
|
|
102
|
-
async function getJSON(url) {
|
|
103
|
-
return JSON.parse(await getText(url));
|
|
104
|
-
}
|
|
105
|
-
httpUtils2.getJSON = getJSON;
|
|
106
|
-
async function postText(url, text, backoff) {
|
|
107
|
-
const headers = {
|
|
108
|
-
"Content-Type": "application/json",
|
|
109
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
110
|
-
};
|
|
111
|
-
return await retryWithBackoff(async () => {
|
|
112
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
113
|
-
request.write(text);
|
|
114
|
-
request.end();
|
|
115
|
-
return await responseDataPromise;
|
|
116
|
-
}, backoff);
|
|
117
|
-
}
|
|
118
|
-
httpUtils2.postText = postText;
|
|
119
|
-
async function postJSON(url, json, backoff) {
|
|
120
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
121
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
122
|
-
}
|
|
123
|
-
httpUtils2.postJSON = postJSON;
|
|
124
|
-
})(httpUtils || (httpUtils = {}));
|
|
125
|
-
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");
|
|
126
|
-
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
127
|
-
const reportPath = path.join(outputFolder, "report.json");
|
|
128
|
-
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
129
|
-
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
130
|
-
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
131
|
-
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
132
|
-
if (attachments.length)
|
|
133
|
-
await fs.promises.mkdir(attachmentsFolder);
|
|
134
|
-
const movedAttachments = [];
|
|
135
|
-
for (const attachment of attachments) {
|
|
136
|
-
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
137
|
-
if (attachment.path)
|
|
138
|
-
await fs.promises.cp(attachment.path, attachmentPath);
|
|
139
|
-
else if (attachment.body)
|
|
140
|
-
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
141
|
-
movedAttachments.push({
|
|
142
|
-
contentType: attachment.contentType,
|
|
143
|
-
id: attachment.id,
|
|
144
|
-
path: attachmentPath
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return movedAttachments;
|
|
148
|
-
}
|
|
149
|
-
function shell(command, args, options) {
|
|
150
|
-
try {
|
|
151
|
-
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
152
|
-
if (result.status !== 0) {
|
|
153
|
-
return void 0;
|
|
154
|
-
}
|
|
155
|
-
return result.stdout.trim();
|
|
156
|
-
} catch (e) {
|
|
157
|
-
console.error(e);
|
|
158
|
-
return void 0;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function gitCommitInfo(gitRepo) {
|
|
162
|
-
const sha = shell(`git`, ["rev-parse", "HEAD"], {
|
|
163
|
-
cwd: gitRepo,
|
|
164
|
-
encoding: "utf-8"
|
|
165
|
-
});
|
|
166
|
-
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
167
|
-
return sha.trim();
|
|
168
|
-
}
|
|
169
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
170
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
171
|
-
|
|
172
|
-
// src/junit.ts
|
|
173
|
-
function getProperties(element) {
|
|
174
|
-
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
175
|
-
if (!propertiesNodes.length)
|
|
176
|
-
return [];
|
|
177
|
-
const result = [];
|
|
178
|
-
for (const propertiesNode of propertiesNodes) {
|
|
179
|
-
const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
|
|
180
|
-
for (const property of properties) {
|
|
181
|
-
const name = property.attributes["name"];
|
|
182
|
-
const innerText = property.children.find((node) => node instanceof XmlText);
|
|
183
|
-
const value = property.attributes["value"] ?? innerText?.text ?? "";
|
|
184
|
-
result.push([name, value]);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return result;
|
|
188
|
-
}
|
|
189
|
-
function extractErrors(testcase) {
|
|
190
|
-
const xmlErrors = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
|
|
191
|
-
if (!xmlErrors.length)
|
|
192
|
-
return void 0;
|
|
193
|
-
const errors = [];
|
|
194
|
-
for (const xmlErr of xmlErrors) {
|
|
195
|
-
const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
|
|
196
|
-
const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
|
|
197
|
-
const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
|
|
198
|
-
errors.push({
|
|
199
|
-
message,
|
|
200
|
-
stack
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
return errors;
|
|
204
|
-
}
|
|
205
|
-
function extractStdout(testcase, stdio) {
|
|
206
|
-
const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === stdio);
|
|
207
|
-
if (!xmlStdio.length)
|
|
208
|
-
return void 0;
|
|
209
|
-
return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
|
|
210
|
-
text: txtNode.text
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
async function parseAttachment(value) {
|
|
214
|
-
let absolutePath = path2.resolve(process.cwd(), value);
|
|
215
|
-
if (fs2.existsSync(absolutePath)) {
|
|
216
|
-
const id = await sha1File(absolutePath);
|
|
217
|
-
return {
|
|
218
|
-
contentType: "image/png",
|
|
219
|
-
path: absolutePath,
|
|
220
|
-
id
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
contentType: "text/plain",
|
|
225
|
-
id: sha1Buffer(value),
|
|
226
|
-
body: Buffer.from(value)
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
async function traverseJUnitReport(context, node) {
|
|
230
|
-
const element = node;
|
|
231
|
-
if (!(element instanceof XmlElement))
|
|
232
|
-
return;
|
|
233
|
-
let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
|
|
234
|
-
if (element.attributes["timestamp"])
|
|
235
|
-
currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
|
|
236
|
-
if (element.name === "testsuite") {
|
|
237
|
-
const file = element.attributes["file"];
|
|
238
|
-
const line = parseInt(element.attributes["line"], 10);
|
|
239
|
-
const name = element.attributes["name"];
|
|
240
|
-
const newSuite = {
|
|
241
|
-
title: name ?? file,
|
|
242
|
-
location: file && !isNaN(line) ? {
|
|
243
|
-
file,
|
|
244
|
-
line,
|
|
245
|
-
column: 1
|
|
246
|
-
} : void 0,
|
|
247
|
-
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
248
|
-
suites: [],
|
|
249
|
-
tests: []
|
|
250
|
-
};
|
|
251
|
-
if (currentSuite) {
|
|
252
|
-
currentSuite.suites ??= [];
|
|
253
|
-
currentSuite.suites.push(newSuite);
|
|
254
|
-
} else {
|
|
255
|
-
report.suites.push(newSuite);
|
|
256
|
-
}
|
|
257
|
-
currentSuite = newSuite;
|
|
258
|
-
const userSuppliedData = getProperties(element);
|
|
259
|
-
if (userSuppliedData.length) {
|
|
260
|
-
currentEnv = structuredClone(currentEnv);
|
|
261
|
-
currentEnv.userSuppliedData ??= {};
|
|
262
|
-
for (const [key, value] of userSuppliedData)
|
|
263
|
-
currentEnv.userSuppliedData[key] = value;
|
|
264
|
-
currentEnvIndex = report.environments.push(currentEnv) - 1;
|
|
265
|
-
}
|
|
266
|
-
} else if (element.name === "testcase") {
|
|
267
|
-
assert2(currentSuite);
|
|
268
|
-
const file = element.attributes["file"];
|
|
269
|
-
const name = element.attributes["name"];
|
|
270
|
-
const line = parseInt(element.attributes["line"], 10);
|
|
271
|
-
const timeMs = parseFloat(element.attributes["time"]) * 1e3;
|
|
272
|
-
const startTimestamp = currentTimeMs;
|
|
273
|
-
const duration = timeMs;
|
|
274
|
-
currentTimeMs += timeMs;
|
|
275
|
-
const annotations = [];
|
|
276
|
-
const attachments2 = [];
|
|
277
|
-
for (const [key, value] of getProperties(element)) {
|
|
278
|
-
if (key.toLowerCase().startsWith("attachment")) {
|
|
279
|
-
if (context.ignoreAttachments)
|
|
280
|
-
continue;
|
|
281
|
-
const attachment = await parseAttachment(value);
|
|
282
|
-
context.attachments.set(attachment.id, attachment);
|
|
283
|
-
attachments2.push({
|
|
284
|
-
id: attachment.id,
|
|
285
|
-
contentType: attachment.contentType,
|
|
286
|
-
//TODO: better default names for attachments?
|
|
287
|
-
name: attachment.path ? path2.basename(attachment.path) : `attachment`
|
|
288
|
-
});
|
|
289
|
-
} else {
|
|
290
|
-
annotations.push({
|
|
291
|
-
type: key,
|
|
292
|
-
description: value.length ? value : void 0
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const childElements = element.children.filter((child) => child instanceof XmlElement);
|
|
297
|
-
const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
|
|
298
|
-
if (xmlSkippedAnnotation)
|
|
299
|
-
annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
|
|
300
|
-
const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
|
|
301
|
-
const errors = extractErrors(element);
|
|
302
|
-
const test = {
|
|
303
|
-
title: name,
|
|
304
|
-
location: file && !isNaN(line) ? {
|
|
305
|
-
file,
|
|
306
|
-
line,
|
|
307
|
-
column: 1
|
|
308
|
-
} : void 0,
|
|
309
|
-
attempts: [{
|
|
310
|
-
environmentIdx: currentEnvIndex,
|
|
311
|
-
expectedStatus,
|
|
312
|
-
annotations,
|
|
313
|
-
attachments: attachments2,
|
|
314
|
-
startTimestamp,
|
|
315
|
-
duration,
|
|
316
|
-
status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
|
|
317
|
-
errors,
|
|
318
|
-
stdout: extractStdout(element, "system-out"),
|
|
319
|
-
stderr: extractStdout(element, "system-err")
|
|
320
|
-
}]
|
|
321
|
-
};
|
|
322
|
-
currentSuite.tests ??= [];
|
|
323
|
-
currentSuite.tests.push(test);
|
|
324
|
-
}
|
|
325
|
-
context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
|
|
326
|
-
for (const child of element.children)
|
|
327
|
-
await traverseJUnitReport(context, child);
|
|
328
|
-
}
|
|
329
|
-
async function parseJUnit(xmls, options) {
|
|
330
|
-
const report = {
|
|
331
|
-
category: "junit",
|
|
332
|
-
commitId: options.commitId,
|
|
333
|
-
duration: options.runDuration,
|
|
334
|
-
startTimestamp: options.runStartTimestamp,
|
|
335
|
-
url: options.runUrl,
|
|
336
|
-
environments: [options.defaultEnv],
|
|
337
|
-
suites: [],
|
|
338
|
-
unattributedErrors: []
|
|
339
|
-
};
|
|
340
|
-
const context = {
|
|
341
|
-
currentEnv: options.defaultEnv,
|
|
342
|
-
currentEnvIndex: 0,
|
|
343
|
-
currentTimeMs: 0,
|
|
344
|
-
report,
|
|
345
|
-
currentSuite: void 0,
|
|
346
|
-
attachments: /* @__PURE__ */ new Map(),
|
|
347
|
-
ignoreAttachments: !!options.ignoreAttachments
|
|
348
|
-
};
|
|
349
|
-
for (const xml of xmls) {
|
|
350
|
-
const doc = parseXml(xml);
|
|
351
|
-
for (const element of doc.children)
|
|
352
|
-
await traverseJUnitReport(context, element);
|
|
353
|
-
}
|
|
354
|
-
return {
|
|
355
|
-
report: ReportUtils2.dedupeSuitesTestsEnvironments(report),
|
|
356
|
-
attachments: Array.from(context.attachments.values())
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// src/cli/cmd-convert.ts
|
|
361
|
-
async function cmdConvert(junitPath, options) {
|
|
362
|
-
const fullPath = path3.resolve(junitPath);
|
|
363
|
-
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
364
|
-
console.error(`Error: path ${fullPath} is not accessible`);
|
|
365
|
-
process.exit(1);
|
|
366
|
-
}
|
|
367
|
-
const stat = await fs3.stat(fullPath);
|
|
368
|
-
let xmlContents = [];
|
|
369
|
-
if (stat.isFile()) {
|
|
370
|
-
const xmlContent = await fs3.readFile(fullPath, "utf-8");
|
|
371
|
-
xmlContents.push(xmlContent);
|
|
372
|
-
} else if (stat.isDirectory()) {
|
|
373
|
-
const xmlFiles = await findXmlFiles(fullPath);
|
|
374
|
-
if (xmlFiles.length === 0) {
|
|
375
|
-
console.error(`Error: No XML files found in directory ${fullPath}`);
|
|
376
|
-
process.exit(1);
|
|
377
|
-
}
|
|
378
|
-
console.log(`Found ${xmlFiles.length} XML files`);
|
|
379
|
-
for (const xmlFile of xmlFiles) {
|
|
380
|
-
const xmlContent = await fs3.readFile(xmlFile, "utf-8");
|
|
381
|
-
xmlContents.push(xmlContent);
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
console.error(`Error: ${fullPath} is neither a file nor a directory`);
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
let commitId;
|
|
388
|
-
if (options.commitId) {
|
|
389
|
-
commitId = options.commitId;
|
|
390
|
-
} else {
|
|
391
|
-
try {
|
|
392
|
-
commitId = gitCommitInfo(process.cwd());
|
|
393
|
-
} catch (e) {
|
|
394
|
-
console.error("Failed to get git commit info. Please provide --commit-id option.");
|
|
395
|
-
process.exit(1);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
const { report, attachments } = await parseJUnit(xmlContents, {
|
|
399
|
-
commitId,
|
|
400
|
-
defaultEnv: { name: options.envName },
|
|
401
|
-
runStartTimestamp: Date.now(),
|
|
402
|
-
runDuration: 0
|
|
403
|
-
});
|
|
404
|
-
await saveReportAndAttachments(report, attachments, options.outputDir);
|
|
405
|
-
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
406
|
-
}
|
|
407
|
-
async function findXmlFiles(dir, result = []) {
|
|
408
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
409
|
-
for (const entry of entries) {
|
|
410
|
-
const fullPath = path3.join(dir, entry.name);
|
|
411
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
412
|
-
result.push(fullPath);
|
|
413
|
-
else if (entry.isDirectory())
|
|
414
|
-
await findXmlFiles(fullPath, result);
|
|
415
|
-
}
|
|
416
|
-
return result;
|
|
417
|
-
}
|
|
418
|
-
export {
|
|
419
|
-
cmdConvert
|
|
420
|
-
};
|
|
421
|
-
//# sourceMappingURL=cmd-convert.js.map
|
package/lib/cli/cmd-download.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// src/cli/cmd-download.ts
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
async function cmdDownload(session, project, runId, rootDir) {
|
|
5
|
-
if (fs.existsSync(rootDir)) {
|
|
6
|
-
console.log(`Directory ${rootDir} already exists!`);
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
const urls = await session.api.run.downloadURLs.GET({
|
|
10
|
-
orgSlug: project.org.orgSlug,
|
|
11
|
-
projectSlug: project.projectSlug,
|
|
12
|
-
runId
|
|
13
|
-
});
|
|
14
|
-
const attachmentsDir = path.join(rootDir, "attachments");
|
|
15
|
-
await fs.promises.mkdir(rootDir, { recursive: true });
|
|
16
|
-
if (urls.attachmentURLs.length)
|
|
17
|
-
await fs.promises.mkdir(attachmentsDir, { recursive: true });
|
|
18
|
-
const response = await fetch(urls.reportURL);
|
|
19
|
-
if (!response.ok)
|
|
20
|
-
throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
|
|
21
|
-
const reportContent = await response.text();
|
|
22
|
-
await fs.promises.writeFile(path.join(rootDir, "report.json"), reportContent);
|
|
23
|
-
const attachmentDownloader = async () => {
|
|
24
|
-
while (urls.attachmentURLs.length) {
|
|
25
|
-
const url = urls.attachmentURLs.pop();
|
|
26
|
-
const response2 = await fetch(url);
|
|
27
|
-
if (!response2.ok)
|
|
28
|
-
throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
|
|
29
|
-
const fileBuffer = Buffer.from(await response2.arrayBuffer());
|
|
30
|
-
const filename = path.basename(new URL(url).pathname);
|
|
31
|
-
await fs.promises.writeFile(path.join(attachmentsDir, filename), fileBuffer);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const workerPromises = [];
|
|
35
|
-
for (let i = 0; i < 4; ++i)
|
|
36
|
-
workerPromises.push(attachmentDownloader());
|
|
37
|
-
await Promise.all(workerPromises);
|
|
38
|
-
}
|
|
39
|
-
export {
|
|
40
|
-
cmdDownload
|
|
41
|
-
};
|
|
42
|
-
//# sourceMappingURL=cmd-download.js.map
|
package/lib/cli/cmd-link.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// src/flakinessConfig.ts
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path2 from "path";
|
|
4
|
-
|
|
5
|
-
// src/utils.ts
|
|
6
|
-
import { ReportUtils } from "@flakiness/report";
|
|
7
|
-
import assert from "assert";
|
|
8
|
-
import { spawnSync } from "child_process";
|
|
9
|
-
import http from "http";
|
|
10
|
-
import https from "https";
|
|
11
|
-
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
12
|
-
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
13
|
-
function errorText(error) {
|
|
14
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
15
|
-
}
|
|
16
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
17
|
-
for (const timeout of backoff) {
|
|
18
|
-
try {
|
|
19
|
-
return await job();
|
|
20
|
-
} catch (e) {
|
|
21
|
-
if (e instanceof AggregateError)
|
|
22
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
23
|
-
else if (e instanceof Error)
|
|
24
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
25
|
-
else
|
|
26
|
-
console.error(`[flakiness.io err]`, e);
|
|
27
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return await job();
|
|
31
|
-
}
|
|
32
|
-
var httpUtils;
|
|
33
|
-
((httpUtils2) => {
|
|
34
|
-
function createRequest({ url, method = "get", headers = {} }) {
|
|
35
|
-
let resolve;
|
|
36
|
-
let reject;
|
|
37
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
38
|
-
resolve = a;
|
|
39
|
-
reject = b;
|
|
40
|
-
});
|
|
41
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
42
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
43
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
44
|
-
const chunks = [];
|
|
45
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
46
|
-
res.on("end", () => {
|
|
47
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
48
|
-
resolve(Buffer.concat(chunks));
|
|
49
|
-
else
|
|
50
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
51
|
-
});
|
|
52
|
-
res.on("error", (error) => reject(error));
|
|
53
|
-
});
|
|
54
|
-
request.on("error", reject);
|
|
55
|
-
return { request, responseDataPromise };
|
|
56
|
-
}
|
|
57
|
-
httpUtils2.createRequest = createRequest;
|
|
58
|
-
async function getBuffer(url, backoff) {
|
|
59
|
-
return await retryWithBackoff(async () => {
|
|
60
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
61
|
-
request.end();
|
|
62
|
-
return await responseDataPromise;
|
|
63
|
-
}, backoff);
|
|
64
|
-
}
|
|
65
|
-
httpUtils2.getBuffer = getBuffer;
|
|
66
|
-
async function getText(url, backoff) {
|
|
67
|
-
const buffer = await getBuffer(url, backoff);
|
|
68
|
-
return buffer.toString("utf-8");
|
|
69
|
-
}
|
|
70
|
-
httpUtils2.getText = getText;
|
|
71
|
-
async function getJSON(url) {
|
|
72
|
-
return JSON.parse(await getText(url));
|
|
73
|
-
}
|
|
74
|
-
httpUtils2.getJSON = getJSON;
|
|
75
|
-
async function postText(url, text, backoff) {
|
|
76
|
-
const headers = {
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
79
|
-
};
|
|
80
|
-
return await retryWithBackoff(async () => {
|
|
81
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
82
|
-
request.write(text);
|
|
83
|
-
request.end();
|
|
84
|
-
return await responseDataPromise;
|
|
85
|
-
}, backoff);
|
|
86
|
-
}
|
|
87
|
-
httpUtils2.postText = postText;
|
|
88
|
-
async function postJSON(url, json, backoff) {
|
|
89
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
90
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
91
|
-
}
|
|
92
|
-
httpUtils2.postJSON = postJSON;
|
|
93
|
-
})(httpUtils || (httpUtils = {}));
|
|
94
|
-
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");
|
|
95
|
-
function shell(command, args, options) {
|
|
96
|
-
try {
|
|
97
|
-
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
98
|
-
if (result.status !== 0) {
|
|
99
|
-
return void 0;
|
|
100
|
-
}
|
|
101
|
-
return result.stdout.trim();
|
|
102
|
-
} catch (e) {
|
|
103
|
-
console.error(e);
|
|
104
|
-
return void 0;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function computeGitRoot(somePathInsideGitRepo) {
|
|
108
|
-
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
109
|
-
cwd: somePathInsideGitRepo,
|
|
110
|
-
encoding: "utf-8"
|
|
111
|
-
});
|
|
112
|
-
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
113
|
-
return normalizePath(root);
|
|
114
|
-
}
|
|
115
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
116
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
117
|
-
function normalizePath(aPath) {
|
|
118
|
-
if (IS_WIN32_PATH.test(aPath)) {
|
|
119
|
-
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
120
|
-
}
|
|
121
|
-
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
122
|
-
return "/" + aPath[0] + aPath.substring(2);
|
|
123
|
-
return aPath;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// src/flakinessConfig.ts
|
|
127
|
-
function createConfigPath(dir) {
|
|
128
|
-
return path2.join(dir, ".flakiness", "config.json");
|
|
129
|
-
}
|
|
130
|
-
var gConfigPath;
|
|
131
|
-
function ensureConfigPath() {
|
|
132
|
-
if (!gConfigPath)
|
|
133
|
-
gConfigPath = computeConfigPath();
|
|
134
|
-
return gConfigPath;
|
|
135
|
-
}
|
|
136
|
-
function computeConfigPath() {
|
|
137
|
-
for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
|
|
138
|
-
const configPath = createConfigPath(p);
|
|
139
|
-
if (fs.existsSync(configPath))
|
|
140
|
-
return configPath;
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
const gitRoot = computeGitRoot(process.cwd());
|
|
144
|
-
return createConfigPath(gitRoot);
|
|
145
|
-
} catch (e) {
|
|
146
|
-
return createConfigPath(process.cwd());
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
var FlakinessConfig = class _FlakinessConfig {
|
|
150
|
-
constructor(_configPath, _config) {
|
|
151
|
-
this._configPath = _configPath;
|
|
152
|
-
this._config = _config;
|
|
153
|
-
}
|
|
154
|
-
static async load() {
|
|
155
|
-
const configPath = ensureConfigPath();
|
|
156
|
-
const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
157
|
-
const json = data ? JSON.parse(data) : {};
|
|
158
|
-
return new _FlakinessConfig(configPath, json);
|
|
159
|
-
}
|
|
160
|
-
static async projectOrDie(session) {
|
|
161
|
-
const config = await _FlakinessConfig.load();
|
|
162
|
-
const projectPublicId = config.projectPublicId();
|
|
163
|
-
if (!projectPublicId)
|
|
164
|
-
throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
|
|
165
|
-
const project = await session.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
|
|
166
|
-
if (!project)
|
|
167
|
-
throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
|
|
168
|
-
return project;
|
|
169
|
-
}
|
|
170
|
-
static createEmpty() {
|
|
171
|
-
return new _FlakinessConfig(ensureConfigPath(), {});
|
|
172
|
-
}
|
|
173
|
-
path() {
|
|
174
|
-
return this._configPath;
|
|
175
|
-
}
|
|
176
|
-
projectPublicId() {
|
|
177
|
-
return this._config.projectPublicId;
|
|
178
|
-
}
|
|
179
|
-
setProjectPublicId(projectId) {
|
|
180
|
-
this._config.projectPublicId = projectId;
|
|
181
|
-
}
|
|
182
|
-
async save() {
|
|
183
|
-
await fs.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
|
|
184
|
-
await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// src/cli/cmd-link.ts
|
|
189
|
-
async function cmdLink(session, slug) {
|
|
190
|
-
const [orgSlug, projectSlug] = slug.split("/");
|
|
191
|
-
const project = await session.api.project.findProject.GET({
|
|
192
|
-
orgSlug,
|
|
193
|
-
projectSlug
|
|
194
|
-
});
|
|
195
|
-
if (!project) {
|
|
196
|
-
console.log(`Failed to find project ${slug}`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
const config = FlakinessConfig.createEmpty();
|
|
200
|
-
config.setProjectPublicId(project.projectPublicId);
|
|
201
|
-
await config.save();
|
|
202
|
-
console.log(`\u2713 Linked to ${session.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
203
|
-
}
|
|
204
|
-
export {
|
|
205
|
-
cmdLink
|
|
206
|
-
};
|
|
207
|
-
//# sourceMappingURL=cmd-link.js.map
|