@flakiness/sdk 0.95.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 +45 -0
- package/README.md +30 -0
- package/lib/cli/cli.js +1435 -0
- package/lib/cli/cmd-convert.js +413 -0
- package/lib/cli/cmd-download.js +494 -0
- package/lib/cli/cmd-link.js +463 -0
- package/lib/cli/cmd-login.js +414 -0
- package/lib/cli/cmd-logout.js +378 -0
- package/lib/cli/cmd-serve.js +7 -0
- package/lib/cli/cmd-status.js +460 -0
- package/lib/cli/cmd-unlink.js +166 -0
- package/lib/cli/cmd-upload-playwright-json.js +818 -0
- package/lib/cli/cmd-upload.js +512 -0
- package/lib/cli/cmd-whoami.js +387 -0
- package/lib/createTestStepSnippets.js +32 -0
- package/lib/flakinessLink.js +161 -0
- package/lib/flakinessSession.js +373 -0
- package/lib/junit.js +311 -0
- package/lib/playwright-test.js +933 -0
- package/lib/playwrightJSONReport.js +432 -0
- package/lib/reportUploader.js +447 -0
- package/lib/serverapi.js +331 -0
- package/lib/systemUtilizationSampler.js +71 -0
- package/lib/utils.js +323 -0
- package/package.json +58 -0
- package/types/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/flakinessSession.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// ../server/lib/common/typedHttp.js
|
|
7
|
+
var TypedHTTP;
|
|
8
|
+
((TypedHTTP2) => {
|
|
9
|
+
TypedHTTP2.StatusCodes = {
|
|
10
|
+
Informational: {
|
|
11
|
+
CONTINUE: 100,
|
|
12
|
+
SWITCHING_PROTOCOLS: 101,
|
|
13
|
+
PROCESSING: 102,
|
|
14
|
+
EARLY_HINTS: 103
|
|
15
|
+
},
|
|
16
|
+
Success: {
|
|
17
|
+
OK: 200,
|
|
18
|
+
CREATED: 201,
|
|
19
|
+
ACCEPTED: 202,
|
|
20
|
+
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
21
|
+
NO_CONTENT: 204,
|
|
22
|
+
RESET_CONTENT: 205,
|
|
23
|
+
PARTIAL_CONTENT: 206,
|
|
24
|
+
MULTI_STATUS: 207
|
|
25
|
+
},
|
|
26
|
+
Redirection: {
|
|
27
|
+
MULTIPLE_CHOICES: 300,
|
|
28
|
+
MOVED_PERMANENTLY: 301,
|
|
29
|
+
MOVED_TEMPORARILY: 302,
|
|
30
|
+
SEE_OTHER: 303,
|
|
31
|
+
NOT_MODIFIED: 304,
|
|
32
|
+
USE_PROXY: 305,
|
|
33
|
+
TEMPORARY_REDIRECT: 307,
|
|
34
|
+
PERMANENT_REDIRECT: 308
|
|
35
|
+
},
|
|
36
|
+
ClientErrors: {
|
|
37
|
+
BAD_REQUEST: 400,
|
|
38
|
+
UNAUTHORIZED: 401,
|
|
39
|
+
PAYMENT_REQUIRED: 402,
|
|
40
|
+
FORBIDDEN: 403,
|
|
41
|
+
NOT_FOUND: 404,
|
|
42
|
+
METHOD_NOT_ALLOWED: 405,
|
|
43
|
+
NOT_ACCEPTABLE: 406,
|
|
44
|
+
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
45
|
+
REQUEST_TIMEOUT: 408,
|
|
46
|
+
CONFLICT: 409,
|
|
47
|
+
GONE: 410,
|
|
48
|
+
LENGTH_REQUIRED: 411,
|
|
49
|
+
PRECONDITION_FAILED: 412,
|
|
50
|
+
REQUEST_TOO_LONG: 413,
|
|
51
|
+
REQUEST_URI_TOO_LONG: 414,
|
|
52
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
53
|
+
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
54
|
+
EXPECTATION_FAILED: 417,
|
|
55
|
+
IM_A_TEAPOT: 418,
|
|
56
|
+
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
57
|
+
METHOD_FAILURE: 420,
|
|
58
|
+
MISDIRECTED_REQUEST: 421,
|
|
59
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
60
|
+
LOCKED: 423,
|
|
61
|
+
FAILED_DEPENDENCY: 424,
|
|
62
|
+
UPGRADE_REQUIRED: 426,
|
|
63
|
+
PRECONDITION_REQUIRED: 428,
|
|
64
|
+
TOO_MANY_REQUESTS: 429,
|
|
65
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
66
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
67
|
+
},
|
|
68
|
+
ServerErrors: {
|
|
69
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
70
|
+
NOT_IMPLEMENTED: 501,
|
|
71
|
+
BAD_GATEWAY: 502,
|
|
72
|
+
SERVICE_UNAVAILABLE: 503,
|
|
73
|
+
GATEWAY_TIMEOUT: 504,
|
|
74
|
+
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
75
|
+
INSUFFICIENT_STORAGE: 507,
|
|
76
|
+
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const AllErrorCodes = {
|
|
80
|
+
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
81
|
+
...TypedHTTP2.StatusCodes.ServerErrors
|
|
82
|
+
};
|
|
83
|
+
class HttpError extends Error {
|
|
84
|
+
constructor(status, message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.status = status;
|
|
87
|
+
}
|
|
88
|
+
static withCode(code, message) {
|
|
89
|
+
const statusCode = AllErrorCodes[code];
|
|
90
|
+
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
91
|
+
return new HttpError(statusCode, message ?? defaultMessage);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
TypedHTTP2.HttpError = HttpError;
|
|
95
|
+
function isInformationalResponse(response) {
|
|
96
|
+
return response.status >= 100 && response.status < 200;
|
|
97
|
+
}
|
|
98
|
+
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
99
|
+
function isSuccessResponse(response) {
|
|
100
|
+
return response.status >= 200 && response.status < 300;
|
|
101
|
+
}
|
|
102
|
+
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
103
|
+
function isRedirectResponse(response) {
|
|
104
|
+
return response.status >= 300 && response.status < 400;
|
|
105
|
+
}
|
|
106
|
+
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
107
|
+
function isErrorResponse(response) {
|
|
108
|
+
return response.status >= 400 && response.status < 600;
|
|
109
|
+
}
|
|
110
|
+
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
111
|
+
function info(status) {
|
|
112
|
+
return { status };
|
|
113
|
+
}
|
|
114
|
+
TypedHTTP2.info = info;
|
|
115
|
+
function ok(data, status) {
|
|
116
|
+
return {
|
|
117
|
+
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
118
|
+
data
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
TypedHTTP2.ok = ok;
|
|
122
|
+
function redirect(url, status = 302) {
|
|
123
|
+
return { status, url };
|
|
124
|
+
}
|
|
125
|
+
TypedHTTP2.redirect = redirect;
|
|
126
|
+
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
127
|
+
return { status, message };
|
|
128
|
+
}
|
|
129
|
+
TypedHTTP2.error = error;
|
|
130
|
+
class Router {
|
|
131
|
+
constructor(_resolveContext) {
|
|
132
|
+
this._resolveContext = _resolveContext;
|
|
133
|
+
}
|
|
134
|
+
static create() {
|
|
135
|
+
return new Router(async (e) => e.ctx);
|
|
136
|
+
}
|
|
137
|
+
rawMethod(method, route) {
|
|
138
|
+
return {
|
|
139
|
+
[method]: {
|
|
140
|
+
method,
|
|
141
|
+
input: route.input,
|
|
142
|
+
etag: route.etag,
|
|
143
|
+
resolveContext: this._resolveContext,
|
|
144
|
+
handler: route.handler
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
get(route) {
|
|
149
|
+
return this.rawMethod("GET", {
|
|
150
|
+
...route,
|
|
151
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
post(route) {
|
|
155
|
+
return this.rawMethod("POST", {
|
|
156
|
+
...route,
|
|
157
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
use(resolveContext) {
|
|
161
|
+
return new Router(async (options) => {
|
|
162
|
+
const m = await this._resolveContext(options);
|
|
163
|
+
return resolveContext({ ...options, ctx: m });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
TypedHTTP2.Router = Router;
|
|
168
|
+
function createClient(base, fetchCallback) {
|
|
169
|
+
function buildUrl(path2, input, options) {
|
|
170
|
+
const method = path2.at(-1);
|
|
171
|
+
const url = new URL(path2.slice(0, path2.length - 1).join("/"), base);
|
|
172
|
+
const signal = options?.signal;
|
|
173
|
+
let body = void 0;
|
|
174
|
+
if (method === "GET" && input)
|
|
175
|
+
url.searchParams.set("input", JSON.stringify(input));
|
|
176
|
+
else if (method !== "GET" && input)
|
|
177
|
+
body = JSON.stringify(input);
|
|
178
|
+
return {
|
|
179
|
+
url,
|
|
180
|
+
method,
|
|
181
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
182
|
+
body,
|
|
183
|
+
signal
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createProxy(path2 = []) {
|
|
187
|
+
return new Proxy(() => {
|
|
188
|
+
}, {
|
|
189
|
+
get(target, prop) {
|
|
190
|
+
if (typeof prop === "symbol")
|
|
191
|
+
return void 0;
|
|
192
|
+
if (prop === "prepare")
|
|
193
|
+
return (input, options) => buildUrl(path2, input, options);
|
|
194
|
+
const newPath = [...path2, prop];
|
|
195
|
+
return createProxy(newPath);
|
|
196
|
+
},
|
|
197
|
+
apply(target, thisArg, args) {
|
|
198
|
+
const options = buildUrl(path2, args[0], args[1]);
|
|
199
|
+
return fetchCallback(options.url, {
|
|
200
|
+
method: options.method,
|
|
201
|
+
body: options.body,
|
|
202
|
+
headers: options.headers,
|
|
203
|
+
signal: options.signal
|
|
204
|
+
}).then(async (response) => {
|
|
205
|
+
if (response.status >= 200 && response.status < 300) {
|
|
206
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
return text.length ? JSON.parse(text) : void 0;
|
|
209
|
+
}
|
|
210
|
+
return await response.blob();
|
|
211
|
+
}
|
|
212
|
+
if (response.status >= 400 && response.status < 600) {
|
|
213
|
+
const text = await response.text();
|
|
214
|
+
if (text)
|
|
215
|
+
throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
|
|
216
|
+
else
|
|
217
|
+
throw new Error(`HTTP request failed with status ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return createProxy();
|
|
224
|
+
}
|
|
225
|
+
TypedHTTP2.createClient = createClient;
|
|
226
|
+
})(TypedHTTP || (TypedHTTP = {}));
|
|
227
|
+
|
|
228
|
+
// src/utils.ts
|
|
229
|
+
import http from "http";
|
|
230
|
+
import https from "https";
|
|
231
|
+
import util from "util";
|
|
232
|
+
import zlib from "zlib";
|
|
233
|
+
var gzipAsync = util.promisify(zlib.gzip);
|
|
234
|
+
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
235
|
+
var gunzipSync = zlib.gunzipSync;
|
|
236
|
+
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
237
|
+
var brotliCompressSync = zlib.brotliCompressSync;
|
|
238
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
239
|
+
for (const timeout of backoff) {
|
|
240
|
+
try {
|
|
241
|
+
return await job();
|
|
242
|
+
} catch (e) {
|
|
243
|
+
if (e instanceof AggregateError)
|
|
244
|
+
console.error(`[flakiness.io err]`, e.errors[0].message);
|
|
245
|
+
else if (e instanceof Error)
|
|
246
|
+
console.error(`[flakiness.io err]`, e.message);
|
|
247
|
+
else
|
|
248
|
+
console.error(`[flakiness.io err]`, e);
|
|
249
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return await job();
|
|
253
|
+
}
|
|
254
|
+
var httpUtils;
|
|
255
|
+
((httpUtils2) => {
|
|
256
|
+
function createRequest({ url, method = "get", headers = {} }) {
|
|
257
|
+
let resolve;
|
|
258
|
+
let reject;
|
|
259
|
+
const responseDataPromise = new Promise((a, b) => {
|
|
260
|
+
resolve = a;
|
|
261
|
+
reject = b;
|
|
262
|
+
});
|
|
263
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
264
|
+
const request = protocol.request(url, { method, headers }, (res) => {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
267
|
+
res.on("end", () => {
|
|
268
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
269
|
+
resolve(Buffer.concat(chunks));
|
|
270
|
+
else
|
|
271
|
+
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
272
|
+
});
|
|
273
|
+
res.on("error", (error) => reject(error));
|
|
274
|
+
});
|
|
275
|
+
request.on("error", reject);
|
|
276
|
+
return { request, responseDataPromise };
|
|
277
|
+
}
|
|
278
|
+
httpUtils2.createRequest = createRequest;
|
|
279
|
+
async function getBuffer(url, backoff) {
|
|
280
|
+
return await retryWithBackoff(async () => {
|
|
281
|
+
const { request, responseDataPromise } = createRequest({ url });
|
|
282
|
+
request.end();
|
|
283
|
+
return await responseDataPromise;
|
|
284
|
+
}, backoff);
|
|
285
|
+
}
|
|
286
|
+
httpUtils2.getBuffer = getBuffer;
|
|
287
|
+
async function getText(url, backoff) {
|
|
288
|
+
const buffer = await getBuffer(url, backoff);
|
|
289
|
+
return buffer.toString("utf-8");
|
|
290
|
+
}
|
|
291
|
+
httpUtils2.getText = getText;
|
|
292
|
+
async function getJSON(url) {
|
|
293
|
+
return JSON.parse(await getText(url));
|
|
294
|
+
}
|
|
295
|
+
httpUtils2.getJSON = getJSON;
|
|
296
|
+
async function postText(url, text, backoff) {
|
|
297
|
+
const headers = {
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
"Content-Length": Buffer.byteLength(text) + ""
|
|
300
|
+
};
|
|
301
|
+
return await retryWithBackoff(async () => {
|
|
302
|
+
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
303
|
+
request.write(text);
|
|
304
|
+
request.end();
|
|
305
|
+
return await responseDataPromise;
|
|
306
|
+
}, backoff);
|
|
307
|
+
}
|
|
308
|
+
httpUtils2.postText = postText;
|
|
309
|
+
async function postJSON(url, json, backoff) {
|
|
310
|
+
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
311
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
312
|
+
}
|
|
313
|
+
httpUtils2.postJSON = postJSON;
|
|
314
|
+
})(httpUtils || (httpUtils = {}));
|
|
315
|
+
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");
|
|
316
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
317
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
318
|
+
|
|
319
|
+
// src/serverapi.ts
|
|
320
|
+
function createServerAPI(endpoint, options) {
|
|
321
|
+
endpoint += "/api/";
|
|
322
|
+
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
323
|
+
...init,
|
|
324
|
+
headers: {
|
|
325
|
+
...init.headers,
|
|
326
|
+
"Authorization": `Bearer ${options.auth}`
|
|
327
|
+
}
|
|
328
|
+
}) : fetch;
|
|
329
|
+
if (options?.retries)
|
|
330
|
+
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
331
|
+
return TypedHTTP.createClient(endpoint, fetcher);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/flakinessSession.ts
|
|
335
|
+
var CONFIG_DIR = (() => {
|
|
336
|
+
const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
|
|
337
|
+
return configDir;
|
|
338
|
+
})();
|
|
339
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
340
|
+
var FlakinessSession = class _FlakinessSession {
|
|
341
|
+
constructor(_config) {
|
|
342
|
+
this._config = _config;
|
|
343
|
+
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
344
|
+
}
|
|
345
|
+
static async load() {
|
|
346
|
+
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
347
|
+
if (!data)
|
|
348
|
+
return void 0;
|
|
349
|
+
const json = JSON.parse(data);
|
|
350
|
+
return new _FlakinessSession(json);
|
|
351
|
+
}
|
|
352
|
+
static async remove() {
|
|
353
|
+
await fs.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
354
|
+
}
|
|
355
|
+
api;
|
|
356
|
+
endpoint() {
|
|
357
|
+
return this._config.endpoint;
|
|
358
|
+
}
|
|
359
|
+
path() {
|
|
360
|
+
return CONFIG_PATH;
|
|
361
|
+
}
|
|
362
|
+
sessionToken() {
|
|
363
|
+
return this._config.token;
|
|
364
|
+
}
|
|
365
|
+
async save() {
|
|
366
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
367
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
export {
|
|
371
|
+
FlakinessSession
|
|
372
|
+
};
|
|
373
|
+
//# sourceMappingURL=flakinessSession.js.map
|
package/lib/junit.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/junit.ts
|
|
2
|
+
import { FlakinessReport as FK } from "@flakiness/report";
|
|
3
|
+
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
4
|
+
import assert from "assert";
|
|
5
|
+
import fs2 from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
// src/utils.ts
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import http from "http";
|
|
12
|
+
import https from "https";
|
|
13
|
+
import util from "util";
|
|
14
|
+
import zlib from "zlib";
|
|
15
|
+
var gzipAsync = util.promisify(zlib.gzip);
|
|
16
|
+
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
17
|
+
var gunzipSync = zlib.gunzipSync;
|
|
18
|
+
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
19
|
+
var brotliCompressSync = zlib.brotliCompressSync;
|
|
20
|
+
function sha1File(filePath) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const hash = crypto.createHash("sha1");
|
|
23
|
+
const stream = fs.createReadStream(filePath);
|
|
24
|
+
stream.on("data", (chunk) => {
|
|
25
|
+
hash.update(chunk);
|
|
26
|
+
});
|
|
27
|
+
stream.on("end", () => {
|
|
28
|
+
resolve(hash.digest("hex"));
|
|
29
|
+
});
|
|
30
|
+
stream.on("error", (err) => {
|
|
31
|
+
reject(err);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function sha1Buffer(data) {
|
|
36
|
+
const hash = crypto.createHash("sha1");
|
|
37
|
+
hash.update(data);
|
|
38
|
+
return hash.digest("hex");
|
|
39
|
+
}
|
|
40
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
41
|
+
for (const timeout of backoff) {
|
|
42
|
+
try {
|
|
43
|
+
return await job();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e instanceof AggregateError)
|
|
46
|
+
console.error(`[flakiness.io err]`, e.errors[0].message);
|
|
47
|
+
else if (e instanceof Error)
|
|
48
|
+
console.error(`[flakiness.io err]`, e.message);
|
|
49
|
+
else
|
|
50
|
+
console.error(`[flakiness.io err]`, e);
|
|
51
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return await job();
|
|
55
|
+
}
|
|
56
|
+
var httpUtils;
|
|
57
|
+
((httpUtils2) => {
|
|
58
|
+
function createRequest({ url, method = "get", headers = {} }) {
|
|
59
|
+
let resolve;
|
|
60
|
+
let reject;
|
|
61
|
+
const responseDataPromise = new Promise((a, b) => {
|
|
62
|
+
resolve = a;
|
|
63
|
+
reject = b;
|
|
64
|
+
});
|
|
65
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
66
|
+
const request = protocol.request(url, { method, headers }, (res) => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
69
|
+
res.on("end", () => {
|
|
70
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
71
|
+
resolve(Buffer.concat(chunks));
|
|
72
|
+
else
|
|
73
|
+
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
74
|
+
});
|
|
75
|
+
res.on("error", (error) => reject(error));
|
|
76
|
+
});
|
|
77
|
+
request.on("error", reject);
|
|
78
|
+
return { request, responseDataPromise };
|
|
79
|
+
}
|
|
80
|
+
httpUtils2.createRequest = createRequest;
|
|
81
|
+
async function getBuffer(url, backoff) {
|
|
82
|
+
return await retryWithBackoff(async () => {
|
|
83
|
+
const { request, responseDataPromise } = createRequest({ url });
|
|
84
|
+
request.end();
|
|
85
|
+
return await responseDataPromise;
|
|
86
|
+
}, backoff);
|
|
87
|
+
}
|
|
88
|
+
httpUtils2.getBuffer = getBuffer;
|
|
89
|
+
async function getText(url, backoff) {
|
|
90
|
+
const buffer = await getBuffer(url, backoff);
|
|
91
|
+
return buffer.toString("utf-8");
|
|
92
|
+
}
|
|
93
|
+
httpUtils2.getText = getText;
|
|
94
|
+
async function getJSON(url) {
|
|
95
|
+
return JSON.parse(await getText(url));
|
|
96
|
+
}
|
|
97
|
+
httpUtils2.getJSON = getJSON;
|
|
98
|
+
async function postText(url, text, backoff) {
|
|
99
|
+
const headers = {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"Content-Length": Buffer.byteLength(text) + ""
|
|
102
|
+
};
|
|
103
|
+
return await retryWithBackoff(async () => {
|
|
104
|
+
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
105
|
+
request.write(text);
|
|
106
|
+
request.end();
|
|
107
|
+
return await responseDataPromise;
|
|
108
|
+
}, backoff);
|
|
109
|
+
}
|
|
110
|
+
httpUtils2.postText = postText;
|
|
111
|
+
async function postJSON(url, json, backoff) {
|
|
112
|
+
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
113
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
114
|
+
}
|
|
115
|
+
httpUtils2.postJSON = postJSON;
|
|
116
|
+
})(httpUtils || (httpUtils = {}));
|
|
117
|
+
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");
|
|
118
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
119
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
120
|
+
|
|
121
|
+
// src/junit.ts
|
|
122
|
+
function getProperties(element) {
|
|
123
|
+
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
124
|
+
if (!propertiesNodes.length)
|
|
125
|
+
return [];
|
|
126
|
+
const result = [];
|
|
127
|
+
for (const propertiesNode of propertiesNodes) {
|
|
128
|
+
const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
|
|
129
|
+
for (const property of properties) {
|
|
130
|
+
const name = property.attributes["name"];
|
|
131
|
+
const innerText = property.children.find((node) => node instanceof XmlText);
|
|
132
|
+
const value = property.attributes["value"] ?? innerText?.text ?? "";
|
|
133
|
+
result.push([name, value]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function extractErrors(testcase) {
|
|
139
|
+
const xmlErrors = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
|
|
140
|
+
if (!xmlErrors.length)
|
|
141
|
+
return void 0;
|
|
142
|
+
const errors = [];
|
|
143
|
+
for (const xmlErr of xmlErrors) {
|
|
144
|
+
const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
|
|
145
|
+
const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
|
|
146
|
+
const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
|
|
147
|
+
errors.push({
|
|
148
|
+
message,
|
|
149
|
+
stack
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
function extractStdout(testcase, stdio) {
|
|
155
|
+
const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === stdio);
|
|
156
|
+
if (!xmlStdio.length)
|
|
157
|
+
return void 0;
|
|
158
|
+
return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
|
|
159
|
+
text: txtNode.text
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
async function parseAttachment(value) {
|
|
163
|
+
let absolutePath = path.resolve(process.cwd(), value);
|
|
164
|
+
if (fs2.existsSync(absolutePath)) {
|
|
165
|
+
const id = await sha1File(absolutePath);
|
|
166
|
+
return {
|
|
167
|
+
contentType: "image/png",
|
|
168
|
+
path: absolutePath,
|
|
169
|
+
id
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
contentType: "text/plain",
|
|
174
|
+
id: sha1Buffer(value),
|
|
175
|
+
body: Buffer.from(value)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function traverseJUnitReport(context, node) {
|
|
179
|
+
const element = node;
|
|
180
|
+
if (!(element instanceof XmlElement))
|
|
181
|
+
return;
|
|
182
|
+
let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
|
|
183
|
+
if (element.attributes["timestamp"])
|
|
184
|
+
currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
|
|
185
|
+
if (element.name === "testsuite") {
|
|
186
|
+
const file = element.attributes["file"];
|
|
187
|
+
const line = parseInt(element.attributes["line"], 10);
|
|
188
|
+
const name = element.attributes["name"];
|
|
189
|
+
const newSuite = {
|
|
190
|
+
title: name ?? file,
|
|
191
|
+
location: file && !isNaN(line) ? {
|
|
192
|
+
file,
|
|
193
|
+
line,
|
|
194
|
+
column: 1
|
|
195
|
+
} : FK.NO_LOCATION,
|
|
196
|
+
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
197
|
+
suites: [],
|
|
198
|
+
tests: []
|
|
199
|
+
};
|
|
200
|
+
if (currentSuite) {
|
|
201
|
+
currentSuite.suites ??= [];
|
|
202
|
+
currentSuite.suites.push(newSuite);
|
|
203
|
+
} else {
|
|
204
|
+
report.suites.push(newSuite);
|
|
205
|
+
}
|
|
206
|
+
currentSuite = newSuite;
|
|
207
|
+
const userSuppliedData = getProperties(element);
|
|
208
|
+
if (userSuppliedData.length) {
|
|
209
|
+
currentEnv = structuredClone(currentEnv);
|
|
210
|
+
currentEnv.userSuppliedData ??= {};
|
|
211
|
+
for (const [key, value] of userSuppliedData)
|
|
212
|
+
currentEnv.userSuppliedData[key] = value;
|
|
213
|
+
currentEnvIndex = report.environments.push(currentEnv) - 1;
|
|
214
|
+
}
|
|
215
|
+
} else if (element.name === "testcase") {
|
|
216
|
+
assert(currentSuite);
|
|
217
|
+
const file = element.attributes["file"];
|
|
218
|
+
const name = element.attributes["name"];
|
|
219
|
+
const line = parseInt(element.attributes["line"], 10);
|
|
220
|
+
const timeMs = parseFloat(element.attributes["time"]) * 1e3;
|
|
221
|
+
const startTimestamp = currentTimeMs;
|
|
222
|
+
const duration = timeMs;
|
|
223
|
+
currentTimeMs += timeMs;
|
|
224
|
+
const annotations = [];
|
|
225
|
+
const attachments2 = [];
|
|
226
|
+
for (const [key, value] of getProperties(element)) {
|
|
227
|
+
if (key.toLowerCase().startsWith("attachment")) {
|
|
228
|
+
if (context.ignoreAttachments)
|
|
229
|
+
continue;
|
|
230
|
+
const attachment = await parseAttachment(value);
|
|
231
|
+
context.attachments.set(attachment.id, attachment);
|
|
232
|
+
attachments2.push({
|
|
233
|
+
id: attachment.id,
|
|
234
|
+
contentType: attachment.contentType,
|
|
235
|
+
//TODO: better default names for attachments?
|
|
236
|
+
name: attachment.path ? path.basename(attachment.path) : `attachment`
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
annotations.push({
|
|
240
|
+
type: key,
|
|
241
|
+
description: value.length ? value : void 0
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const childElements = element.children.filter((child) => child instanceof XmlElement);
|
|
246
|
+
const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
|
|
247
|
+
if (xmlSkippedAnnotation)
|
|
248
|
+
annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
|
|
249
|
+
const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
|
|
250
|
+
const errors = extractErrors(element);
|
|
251
|
+
const test = {
|
|
252
|
+
title: name,
|
|
253
|
+
location: file && !isNaN(line) ? {
|
|
254
|
+
file,
|
|
255
|
+
line,
|
|
256
|
+
column: 1
|
|
257
|
+
} : FK.NO_LOCATION,
|
|
258
|
+
attempts: [{
|
|
259
|
+
environmentIdx: currentEnvIndex,
|
|
260
|
+
expectedStatus,
|
|
261
|
+
annotations,
|
|
262
|
+
attachments: attachments2,
|
|
263
|
+
startTimestamp,
|
|
264
|
+
duration,
|
|
265
|
+
status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
|
|
266
|
+
errors,
|
|
267
|
+
stdout: extractStdout(element, "system-out"),
|
|
268
|
+
stderr: extractStdout(element, "system-err")
|
|
269
|
+
}]
|
|
270
|
+
};
|
|
271
|
+
currentSuite.tests ??= [];
|
|
272
|
+
currentSuite.tests.push(test);
|
|
273
|
+
}
|
|
274
|
+
context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
|
|
275
|
+
for (const child of element.children)
|
|
276
|
+
await traverseJUnitReport(context, child);
|
|
277
|
+
}
|
|
278
|
+
async function parseJUnit(xmls, options) {
|
|
279
|
+
const report = {
|
|
280
|
+
category: "junit",
|
|
281
|
+
commitId: options.commitId,
|
|
282
|
+
duration: options.runDuration,
|
|
283
|
+
startTimestamp: options.runStartTimestamp,
|
|
284
|
+
url: options.runUrl,
|
|
285
|
+
environments: [options.defaultEnv],
|
|
286
|
+
suites: [],
|
|
287
|
+
unattributedErrors: []
|
|
288
|
+
};
|
|
289
|
+
const context = {
|
|
290
|
+
currentEnv: options.defaultEnv,
|
|
291
|
+
currentEnvIndex: 0,
|
|
292
|
+
currentTimeMs: 0,
|
|
293
|
+
report,
|
|
294
|
+
currentSuite: void 0,
|
|
295
|
+
attachments: /* @__PURE__ */ new Map(),
|
|
296
|
+
ignoreAttachments: !!options.ignoreAttachments
|
|
297
|
+
};
|
|
298
|
+
for (const xml of xmls) {
|
|
299
|
+
const doc = parseXml(xml);
|
|
300
|
+
for (const element of doc.children)
|
|
301
|
+
await traverseJUnitReport(context, element);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
report: FK.dedupeSuitesTestsEnvironments(report),
|
|
305
|
+
attachments: Array.from(context.attachments.values())
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
parseJUnit
|
|
310
|
+
};
|
|
311
|
+
//# sourceMappingURL=junit.js.map
|