@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,933 @@
|
|
|
1
|
+
// src/playwright-test.ts
|
|
2
|
+
import { FlakinessReport as FK } from "@flakiness/report";
|
|
3
|
+
import { Multimap } from "@flakiness/shared/common/multimap.js";
|
|
4
|
+
import fs4 from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
// src/createTestStepSnippets.ts
|
|
8
|
+
import { codeFrameColumns } from "@babel/code-frame";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
function createTestStepSnippets(filepathToSteps) {
|
|
11
|
+
for (const [filepath, steps] of filepathToSteps) {
|
|
12
|
+
let source;
|
|
13
|
+
try {
|
|
14
|
+
source = fs.readFileSync(filepath, "utf-8");
|
|
15
|
+
} catch (e) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const lines = source.split("\n").length;
|
|
19
|
+
const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
|
|
20
|
+
const highlightedLines = highlighted.split("\n");
|
|
21
|
+
const lineWithArrow = highlightedLines[highlightedLines.length - 1];
|
|
22
|
+
for (const step of steps) {
|
|
23
|
+
if (!step.location)
|
|
24
|
+
continue;
|
|
25
|
+
if (step.location.line < 2 || step.location.line >= lines)
|
|
26
|
+
continue;
|
|
27
|
+
const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
|
|
28
|
+
const index = lineWithArrow.indexOf("^");
|
|
29
|
+
const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
|
|
30
|
+
snippetLines.splice(2, 0, shiftedArrow);
|
|
31
|
+
step.snippet = snippetLines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/reportUploader.ts
|
|
37
|
+
import fs3 from "fs";
|
|
38
|
+
import { URL as URL2 } from "url";
|
|
39
|
+
import { brotliCompressSync as brotliCompressSync2 } from "zlib";
|
|
40
|
+
|
|
41
|
+
// ../server/lib/common/typedHttp.js
|
|
42
|
+
var TypedHTTP;
|
|
43
|
+
((TypedHTTP2) => {
|
|
44
|
+
TypedHTTP2.StatusCodes = {
|
|
45
|
+
Informational: {
|
|
46
|
+
CONTINUE: 100,
|
|
47
|
+
SWITCHING_PROTOCOLS: 101,
|
|
48
|
+
PROCESSING: 102,
|
|
49
|
+
EARLY_HINTS: 103
|
|
50
|
+
},
|
|
51
|
+
Success: {
|
|
52
|
+
OK: 200,
|
|
53
|
+
CREATED: 201,
|
|
54
|
+
ACCEPTED: 202,
|
|
55
|
+
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
56
|
+
NO_CONTENT: 204,
|
|
57
|
+
RESET_CONTENT: 205,
|
|
58
|
+
PARTIAL_CONTENT: 206,
|
|
59
|
+
MULTI_STATUS: 207
|
|
60
|
+
},
|
|
61
|
+
Redirection: {
|
|
62
|
+
MULTIPLE_CHOICES: 300,
|
|
63
|
+
MOVED_PERMANENTLY: 301,
|
|
64
|
+
MOVED_TEMPORARILY: 302,
|
|
65
|
+
SEE_OTHER: 303,
|
|
66
|
+
NOT_MODIFIED: 304,
|
|
67
|
+
USE_PROXY: 305,
|
|
68
|
+
TEMPORARY_REDIRECT: 307,
|
|
69
|
+
PERMANENT_REDIRECT: 308
|
|
70
|
+
},
|
|
71
|
+
ClientErrors: {
|
|
72
|
+
BAD_REQUEST: 400,
|
|
73
|
+
UNAUTHORIZED: 401,
|
|
74
|
+
PAYMENT_REQUIRED: 402,
|
|
75
|
+
FORBIDDEN: 403,
|
|
76
|
+
NOT_FOUND: 404,
|
|
77
|
+
METHOD_NOT_ALLOWED: 405,
|
|
78
|
+
NOT_ACCEPTABLE: 406,
|
|
79
|
+
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
80
|
+
REQUEST_TIMEOUT: 408,
|
|
81
|
+
CONFLICT: 409,
|
|
82
|
+
GONE: 410,
|
|
83
|
+
LENGTH_REQUIRED: 411,
|
|
84
|
+
PRECONDITION_FAILED: 412,
|
|
85
|
+
REQUEST_TOO_LONG: 413,
|
|
86
|
+
REQUEST_URI_TOO_LONG: 414,
|
|
87
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
88
|
+
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
89
|
+
EXPECTATION_FAILED: 417,
|
|
90
|
+
IM_A_TEAPOT: 418,
|
|
91
|
+
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
92
|
+
METHOD_FAILURE: 420,
|
|
93
|
+
MISDIRECTED_REQUEST: 421,
|
|
94
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
95
|
+
LOCKED: 423,
|
|
96
|
+
FAILED_DEPENDENCY: 424,
|
|
97
|
+
UPGRADE_REQUIRED: 426,
|
|
98
|
+
PRECONDITION_REQUIRED: 428,
|
|
99
|
+
TOO_MANY_REQUESTS: 429,
|
|
100
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
101
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
102
|
+
},
|
|
103
|
+
ServerErrors: {
|
|
104
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
105
|
+
NOT_IMPLEMENTED: 501,
|
|
106
|
+
BAD_GATEWAY: 502,
|
|
107
|
+
SERVICE_UNAVAILABLE: 503,
|
|
108
|
+
GATEWAY_TIMEOUT: 504,
|
|
109
|
+
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
110
|
+
INSUFFICIENT_STORAGE: 507,
|
|
111
|
+
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const AllErrorCodes = {
|
|
115
|
+
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
116
|
+
...TypedHTTP2.StatusCodes.ServerErrors
|
|
117
|
+
};
|
|
118
|
+
class HttpError extends Error {
|
|
119
|
+
constructor(status, message) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.status = status;
|
|
122
|
+
}
|
|
123
|
+
static withCode(code, message) {
|
|
124
|
+
const statusCode = AllErrorCodes[code];
|
|
125
|
+
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
126
|
+
return new HttpError(statusCode, message ?? defaultMessage);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
TypedHTTP2.HttpError = HttpError;
|
|
130
|
+
function isInformationalResponse(response) {
|
|
131
|
+
return response.status >= 100 && response.status < 200;
|
|
132
|
+
}
|
|
133
|
+
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
134
|
+
function isSuccessResponse(response) {
|
|
135
|
+
return response.status >= 200 && response.status < 300;
|
|
136
|
+
}
|
|
137
|
+
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
138
|
+
function isRedirectResponse(response) {
|
|
139
|
+
return response.status >= 300 && response.status < 400;
|
|
140
|
+
}
|
|
141
|
+
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
142
|
+
function isErrorResponse(response) {
|
|
143
|
+
return response.status >= 400 && response.status < 600;
|
|
144
|
+
}
|
|
145
|
+
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
146
|
+
function info(status) {
|
|
147
|
+
return { status };
|
|
148
|
+
}
|
|
149
|
+
TypedHTTP2.info = info;
|
|
150
|
+
function ok(data, status) {
|
|
151
|
+
return {
|
|
152
|
+
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
153
|
+
data
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
TypedHTTP2.ok = ok;
|
|
157
|
+
function redirect(url, status = 302) {
|
|
158
|
+
return { status, url };
|
|
159
|
+
}
|
|
160
|
+
TypedHTTP2.redirect = redirect;
|
|
161
|
+
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
162
|
+
return { status, message };
|
|
163
|
+
}
|
|
164
|
+
TypedHTTP2.error = error;
|
|
165
|
+
class Router {
|
|
166
|
+
constructor(_resolveContext) {
|
|
167
|
+
this._resolveContext = _resolveContext;
|
|
168
|
+
}
|
|
169
|
+
static create() {
|
|
170
|
+
return new Router(async (e) => e.ctx);
|
|
171
|
+
}
|
|
172
|
+
rawMethod(method, route) {
|
|
173
|
+
return {
|
|
174
|
+
[method]: {
|
|
175
|
+
method,
|
|
176
|
+
input: route.input,
|
|
177
|
+
etag: route.etag,
|
|
178
|
+
resolveContext: this._resolveContext,
|
|
179
|
+
handler: route.handler
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
get(route) {
|
|
184
|
+
return this.rawMethod("GET", {
|
|
185
|
+
...route,
|
|
186
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
post(route) {
|
|
190
|
+
return this.rawMethod("POST", {
|
|
191
|
+
...route,
|
|
192
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
use(resolveContext) {
|
|
196
|
+
return new Router(async (options) => {
|
|
197
|
+
const m = await this._resolveContext(options);
|
|
198
|
+
return resolveContext({ ...options, ctx: m });
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
TypedHTTP2.Router = Router;
|
|
203
|
+
function createClient(base, fetchCallback) {
|
|
204
|
+
function buildUrl(path2, input, options) {
|
|
205
|
+
const method = path2.at(-1);
|
|
206
|
+
const url = new URL(path2.slice(0, path2.length - 1).join("/"), base);
|
|
207
|
+
const signal = options?.signal;
|
|
208
|
+
let body = void 0;
|
|
209
|
+
if (method === "GET" && input)
|
|
210
|
+
url.searchParams.set("input", JSON.stringify(input));
|
|
211
|
+
else if (method !== "GET" && input)
|
|
212
|
+
body = JSON.stringify(input);
|
|
213
|
+
return {
|
|
214
|
+
url,
|
|
215
|
+
method,
|
|
216
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
217
|
+
body,
|
|
218
|
+
signal
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function createProxy(path2 = []) {
|
|
222
|
+
return new Proxy(() => {
|
|
223
|
+
}, {
|
|
224
|
+
get(target, prop) {
|
|
225
|
+
if (typeof prop === "symbol")
|
|
226
|
+
return void 0;
|
|
227
|
+
if (prop === "prepare")
|
|
228
|
+
return (input, options) => buildUrl(path2, input, options);
|
|
229
|
+
const newPath = [...path2, prop];
|
|
230
|
+
return createProxy(newPath);
|
|
231
|
+
},
|
|
232
|
+
apply(target, thisArg, args) {
|
|
233
|
+
const options = buildUrl(path2, args[0], args[1]);
|
|
234
|
+
return fetchCallback(options.url, {
|
|
235
|
+
method: options.method,
|
|
236
|
+
body: options.body,
|
|
237
|
+
headers: options.headers,
|
|
238
|
+
signal: options.signal
|
|
239
|
+
}).then(async (response) => {
|
|
240
|
+
if (response.status >= 200 && response.status < 300) {
|
|
241
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
242
|
+
const text = await response.text();
|
|
243
|
+
return text.length ? JSON.parse(text) : void 0;
|
|
244
|
+
}
|
|
245
|
+
return await response.blob();
|
|
246
|
+
}
|
|
247
|
+
if (response.status >= 400 && response.status < 600) {
|
|
248
|
+
const text = await response.text();
|
|
249
|
+
if (text)
|
|
250
|
+
throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
|
|
251
|
+
else
|
|
252
|
+
throw new Error(`HTTP request failed with status ${response.status}`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return createProxy();
|
|
259
|
+
}
|
|
260
|
+
TypedHTTP2.createClient = createClient;
|
|
261
|
+
})(TypedHTTP || (TypedHTTP = {}));
|
|
262
|
+
|
|
263
|
+
// src/utils.ts
|
|
264
|
+
import assert from "assert";
|
|
265
|
+
import { spawnSync } from "child_process";
|
|
266
|
+
import crypto from "crypto";
|
|
267
|
+
import fs2 from "fs";
|
|
268
|
+
import http from "http";
|
|
269
|
+
import https from "https";
|
|
270
|
+
import os from "os";
|
|
271
|
+
import { posix as posixPath, win32 as win32Path } from "path";
|
|
272
|
+
import util from "util";
|
|
273
|
+
import zlib from "zlib";
|
|
274
|
+
var gzipAsync = util.promisify(zlib.gzip);
|
|
275
|
+
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
276
|
+
var gunzipSync = zlib.gunzipSync;
|
|
277
|
+
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
278
|
+
var brotliCompressSync = zlib.brotliCompressSync;
|
|
279
|
+
async function existsAsync(aPath) {
|
|
280
|
+
return fs2.promises.stat(aPath).then(() => true).catch((e) => false);
|
|
281
|
+
}
|
|
282
|
+
function extractEnvConfiguration() {
|
|
283
|
+
const ENV_PREFIX = "FK_ENV_";
|
|
284
|
+
return Object.fromEntries(
|
|
285
|
+
Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
function sha1File(filePath) {
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const hash = crypto.createHash("sha1");
|
|
291
|
+
const stream = fs2.createReadStream(filePath);
|
|
292
|
+
stream.on("data", (chunk) => {
|
|
293
|
+
hash.update(chunk);
|
|
294
|
+
});
|
|
295
|
+
stream.on("end", () => {
|
|
296
|
+
resolve(hash.digest("hex"));
|
|
297
|
+
});
|
|
298
|
+
stream.on("error", (err) => {
|
|
299
|
+
reject(err);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function sha1Buffer(data) {
|
|
304
|
+
const hash = crypto.createHash("sha1");
|
|
305
|
+
hash.update(data);
|
|
306
|
+
return hash.digest("hex");
|
|
307
|
+
}
|
|
308
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
309
|
+
for (const timeout of backoff) {
|
|
310
|
+
try {
|
|
311
|
+
return await job();
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (e instanceof AggregateError)
|
|
314
|
+
console.error(`[flakiness.io err]`, e.errors[0].message);
|
|
315
|
+
else if (e instanceof Error)
|
|
316
|
+
console.error(`[flakiness.io err]`, e.message);
|
|
317
|
+
else
|
|
318
|
+
console.error(`[flakiness.io err]`, e);
|
|
319
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return await job();
|
|
323
|
+
}
|
|
324
|
+
var httpUtils;
|
|
325
|
+
((httpUtils2) => {
|
|
326
|
+
function createRequest({ url, method = "get", headers = {} }) {
|
|
327
|
+
let resolve;
|
|
328
|
+
let reject;
|
|
329
|
+
const responseDataPromise = new Promise((a, b) => {
|
|
330
|
+
resolve = a;
|
|
331
|
+
reject = b;
|
|
332
|
+
});
|
|
333
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
334
|
+
const request = protocol.request(url, { method, headers }, (res) => {
|
|
335
|
+
const chunks = [];
|
|
336
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
337
|
+
res.on("end", () => {
|
|
338
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
339
|
+
resolve(Buffer.concat(chunks));
|
|
340
|
+
else
|
|
341
|
+
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
342
|
+
});
|
|
343
|
+
res.on("error", (error) => reject(error));
|
|
344
|
+
});
|
|
345
|
+
request.on("error", reject);
|
|
346
|
+
return { request, responseDataPromise };
|
|
347
|
+
}
|
|
348
|
+
httpUtils2.createRequest = createRequest;
|
|
349
|
+
async function getBuffer(url, backoff) {
|
|
350
|
+
return await retryWithBackoff(async () => {
|
|
351
|
+
const { request, responseDataPromise } = createRequest({ url });
|
|
352
|
+
request.end();
|
|
353
|
+
return await responseDataPromise;
|
|
354
|
+
}, backoff);
|
|
355
|
+
}
|
|
356
|
+
httpUtils2.getBuffer = getBuffer;
|
|
357
|
+
async function getText(url, backoff) {
|
|
358
|
+
const buffer = await getBuffer(url, backoff);
|
|
359
|
+
return buffer.toString("utf-8");
|
|
360
|
+
}
|
|
361
|
+
httpUtils2.getText = getText;
|
|
362
|
+
async function getJSON(url) {
|
|
363
|
+
return JSON.parse(await getText(url));
|
|
364
|
+
}
|
|
365
|
+
httpUtils2.getJSON = getJSON;
|
|
366
|
+
async function postText(url, text, backoff) {
|
|
367
|
+
const headers = {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
"Content-Length": Buffer.byteLength(text) + ""
|
|
370
|
+
};
|
|
371
|
+
return await retryWithBackoff(async () => {
|
|
372
|
+
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
373
|
+
request.write(text);
|
|
374
|
+
request.end();
|
|
375
|
+
return await responseDataPromise;
|
|
376
|
+
}, backoff);
|
|
377
|
+
}
|
|
378
|
+
httpUtils2.postText = postText;
|
|
379
|
+
async function postJSON(url, json, backoff) {
|
|
380
|
+
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
381
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
382
|
+
}
|
|
383
|
+
httpUtils2.postJSON = postJSON;
|
|
384
|
+
})(httpUtils || (httpUtils = {}));
|
|
385
|
+
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");
|
|
386
|
+
function stripAnsi(str) {
|
|
387
|
+
return str.replace(ansiRegex, "");
|
|
388
|
+
}
|
|
389
|
+
function shell(command, args, options) {
|
|
390
|
+
try {
|
|
391
|
+
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
392
|
+
if (result.status !== 0) {
|
|
393
|
+
console.log(result);
|
|
394
|
+
console.log(options);
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
return result.stdout.trim();
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log(e);
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function readLinuxOSRelease() {
|
|
404
|
+
const osReleaseText = fs2.readFileSync("/etc/os-release", "utf-8");
|
|
405
|
+
return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
406
|
+
line = line.trim();
|
|
407
|
+
let [key, value] = line.split("=");
|
|
408
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
409
|
+
value = value.substring(1, value.length - 1);
|
|
410
|
+
return [key, value];
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
function osLinuxInfo() {
|
|
414
|
+
const arch = shell(`uname`, [`-m`]);
|
|
415
|
+
const osReleaseMap = readLinuxOSRelease();
|
|
416
|
+
const name = osReleaseMap.get("name") ?? shell(`uname`);
|
|
417
|
+
const version = osReleaseMap.get("version_id");
|
|
418
|
+
return { name, arch, version };
|
|
419
|
+
}
|
|
420
|
+
function osDarwinInfo() {
|
|
421
|
+
const name = "macos";
|
|
422
|
+
const arch = shell(`uname`, [`-m`]);
|
|
423
|
+
const version = shell(`sw_vers`, [`-productVersion`]);
|
|
424
|
+
return { name, arch, version };
|
|
425
|
+
}
|
|
426
|
+
function osWinInfo() {
|
|
427
|
+
const name = "win";
|
|
428
|
+
const arch = process.arch;
|
|
429
|
+
const version = os.release();
|
|
430
|
+
return { name, arch, version };
|
|
431
|
+
}
|
|
432
|
+
function getOSInfo() {
|
|
433
|
+
if (process.platform === "darwin")
|
|
434
|
+
return osDarwinInfo();
|
|
435
|
+
if (process.platform === "win32")
|
|
436
|
+
return osWinInfo();
|
|
437
|
+
return osLinuxInfo();
|
|
438
|
+
}
|
|
439
|
+
function inferRunUrl() {
|
|
440
|
+
if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
|
441
|
+
return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
function gitCommitInfo(gitRepo) {
|
|
445
|
+
const sha = shell(`git`, ["rev-parse", "HEAD"], {
|
|
446
|
+
cwd: gitRepo,
|
|
447
|
+
encoding: "utf-8"
|
|
448
|
+
});
|
|
449
|
+
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
450
|
+
return sha.trim();
|
|
451
|
+
}
|
|
452
|
+
function computeGitRoot(somePathInsideGitRepo) {
|
|
453
|
+
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
454
|
+
cwd: somePathInsideGitRepo,
|
|
455
|
+
encoding: "utf-8"
|
|
456
|
+
});
|
|
457
|
+
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
458
|
+
return normalizePath(root);
|
|
459
|
+
}
|
|
460
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
461
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
462
|
+
function normalizePath(aPath) {
|
|
463
|
+
if (IS_WIN32_PATH.test(aPath)) {
|
|
464
|
+
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
465
|
+
}
|
|
466
|
+
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
467
|
+
return "/" + aPath[0] + aPath.substring(2);
|
|
468
|
+
return aPath;
|
|
469
|
+
}
|
|
470
|
+
function gitFilePath(gitRoot, absolutePath) {
|
|
471
|
+
return posixPath.relative(gitRoot, absolutePath);
|
|
472
|
+
}
|
|
473
|
+
function parseDurationMS(value) {
|
|
474
|
+
if (isNaN(value))
|
|
475
|
+
throw new Error("Duration cannot be NaN");
|
|
476
|
+
if (value < 0)
|
|
477
|
+
throw new Error(`Duration cannot be less than 0, found ${value}`);
|
|
478
|
+
return value | 0;
|
|
479
|
+
}
|
|
480
|
+
function createEnvironments(projects) {
|
|
481
|
+
const envConfiguration = extractEnvConfiguration();
|
|
482
|
+
const osInfo = getOSInfo();
|
|
483
|
+
let uniqueNames = /* @__PURE__ */ new Set();
|
|
484
|
+
const result = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const project of projects) {
|
|
486
|
+
let defaultName = project.name;
|
|
487
|
+
if (!defaultName.trim())
|
|
488
|
+
defaultName = "anonymous";
|
|
489
|
+
let name = defaultName;
|
|
490
|
+
for (let i = 2; uniqueNames.has(name); ++i)
|
|
491
|
+
name = `${defaultName}-${i}`;
|
|
492
|
+
uniqueNames.add(defaultName);
|
|
493
|
+
result.set(project, {
|
|
494
|
+
name,
|
|
495
|
+
systemData: {
|
|
496
|
+
osArch: osInfo.arch,
|
|
497
|
+
osName: osInfo.name,
|
|
498
|
+
osVersion: osInfo.version
|
|
499
|
+
},
|
|
500
|
+
userSuppliedData: {
|
|
501
|
+
...envConfiguration,
|
|
502
|
+
...project.metadata
|
|
503
|
+
},
|
|
504
|
+
opaqueData: {
|
|
505
|
+
project
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/serverapi.ts
|
|
513
|
+
function createServerAPI(endpoint, options) {
|
|
514
|
+
endpoint += "/api/";
|
|
515
|
+
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
516
|
+
...init,
|
|
517
|
+
headers: {
|
|
518
|
+
...init.headers,
|
|
519
|
+
"Authorization": `Bearer ${options.auth}`
|
|
520
|
+
}
|
|
521
|
+
}) : fetch;
|
|
522
|
+
if (options?.retries)
|
|
523
|
+
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
524
|
+
return TypedHTTP.createClient(endpoint, fetcher);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/reportUploader.ts
|
|
528
|
+
var ReportUploader = class _ReportUploader {
|
|
529
|
+
static optionsFromEnv(overrides) {
|
|
530
|
+
const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
531
|
+
if (!flakinessAccessToken)
|
|
532
|
+
return void 0;
|
|
533
|
+
const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
|
|
534
|
+
return { flakinessAccessToken, flakinessEndpoint };
|
|
535
|
+
}
|
|
536
|
+
static async upload(options) {
|
|
537
|
+
const uploaderOptions = _ReportUploader.optionsFromEnv(options);
|
|
538
|
+
if (!uploaderOptions) {
|
|
539
|
+
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
540
|
+
return void 0;
|
|
541
|
+
}
|
|
542
|
+
const uploader = new _ReportUploader(uploaderOptions);
|
|
543
|
+
const upload = uploader.createUpload(options.report, options.attachments);
|
|
544
|
+
const uploadResult = await upload.upload();
|
|
545
|
+
if (!uploadResult.success) {
|
|
546
|
+
options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
|
|
547
|
+
return { errorMessage: uploadResult.message };
|
|
548
|
+
}
|
|
549
|
+
options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
|
|
550
|
+
if (uploadResult.reportUrl)
|
|
551
|
+
options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
|
|
552
|
+
}
|
|
553
|
+
_options;
|
|
554
|
+
constructor(options) {
|
|
555
|
+
this._options = options;
|
|
556
|
+
}
|
|
557
|
+
createUpload(report, attachments) {
|
|
558
|
+
const upload = new ReportUpload(this._options, report, attachments);
|
|
559
|
+
return upload;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
|
|
563
|
+
var ReportUpload = class {
|
|
564
|
+
_report;
|
|
565
|
+
_attachments;
|
|
566
|
+
_options;
|
|
567
|
+
_api;
|
|
568
|
+
constructor(options, report, attachments) {
|
|
569
|
+
this._options = options;
|
|
570
|
+
this._report = report;
|
|
571
|
+
this._attachments = attachments;
|
|
572
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
573
|
+
}
|
|
574
|
+
async upload(options) {
|
|
575
|
+
const response = await this._api.run.startUpload.POST({
|
|
576
|
+
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
577
|
+
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
578
|
+
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
579
|
+
if (response?.error || !response.result)
|
|
580
|
+
return { success: false, message: `flakiness.io returned error: ${response.error.message}` };
|
|
581
|
+
await Promise.all([
|
|
582
|
+
this._uploadReport(JSON.stringify(this._report), response.result.report_upload_url, options?.syncCompression ?? false),
|
|
583
|
+
...this._attachments.map((attachment) => {
|
|
584
|
+
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
585
|
+
if (!uploadURL)
|
|
586
|
+
throw new Error("Internal error: missing upload URL for attachment!");
|
|
587
|
+
return this._uploadAttachment(attachment, uploadURL);
|
|
588
|
+
})
|
|
589
|
+
]);
|
|
590
|
+
const response2 = await this._api.run.completeUpload.POST({
|
|
591
|
+
upload_token: response.result.upload_token
|
|
592
|
+
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
593
|
+
const url = response2?.result?.report_url ? new URL2(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
594
|
+
return { success: true, reportUrl: url };
|
|
595
|
+
}
|
|
596
|
+
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
597
|
+
const compressed = syncCompression ? brotliCompressSync2(data) : await brotliCompressAsync(data);
|
|
598
|
+
const headers = {
|
|
599
|
+
"Content-Type": "application/json",
|
|
600
|
+
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
601
|
+
"Content-Encoding": "br"
|
|
602
|
+
};
|
|
603
|
+
await retryWithBackoff(async () => {
|
|
604
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
605
|
+
url: uploadUrl,
|
|
606
|
+
headers,
|
|
607
|
+
method: "put"
|
|
608
|
+
});
|
|
609
|
+
request.write(compressed);
|
|
610
|
+
request.end();
|
|
611
|
+
await responseDataPromise;
|
|
612
|
+
}, HTTP_BACKOFF);
|
|
613
|
+
}
|
|
614
|
+
async _uploadAttachment(attachment, uploadUrl) {
|
|
615
|
+
const bytesLength = attachment.path ? (await fs3.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
|
|
616
|
+
const headers = {
|
|
617
|
+
"Content-Type": attachment.contentType,
|
|
618
|
+
"Content-Length": bytesLength + ""
|
|
619
|
+
};
|
|
620
|
+
await retryWithBackoff(async () => {
|
|
621
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
622
|
+
url: uploadUrl,
|
|
623
|
+
headers,
|
|
624
|
+
method: "put"
|
|
625
|
+
});
|
|
626
|
+
if (attachment.path) {
|
|
627
|
+
fs3.createReadStream(attachment.path).pipe(request);
|
|
628
|
+
} else {
|
|
629
|
+
if (attachment.body)
|
|
630
|
+
request.write(attachment.body);
|
|
631
|
+
request.end();
|
|
632
|
+
}
|
|
633
|
+
await responseDataPromise;
|
|
634
|
+
}, HTTP_BACKOFF);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/systemUtilizationSampler.ts
|
|
639
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
640
|
+
import os2 from "os";
|
|
641
|
+
function getAvailableMemMacOS() {
|
|
642
|
+
const lines = spawnSync2("vm_stat", { encoding: "utf8" }).stdout.trim().split("\n");
|
|
643
|
+
const pageSize = parseInt(lines[0].match(/page size of (\d+) bytes/)[1], 10);
|
|
644
|
+
if (isNaN(pageSize)) {
|
|
645
|
+
console.warn("[flakiness.io] Error detecting macos page size");
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
let totalFree = 0;
|
|
649
|
+
for (const line of lines) {
|
|
650
|
+
if (/Pages (free|inactive|speculative):/.test(line)) {
|
|
651
|
+
const match = line.match(/\d+/);
|
|
652
|
+
if (match)
|
|
653
|
+
totalFree += parseInt(match[0], 10);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return totalFree * pageSize;
|
|
657
|
+
}
|
|
658
|
+
function getSystemUtilization() {
|
|
659
|
+
let idleTicks = 0;
|
|
660
|
+
let totalTicks = 0;
|
|
661
|
+
for (const cpu of os2.cpus()) {
|
|
662
|
+
totalTicks += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
|
|
663
|
+
idleTicks += cpu.times.idle;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
idleTicks,
|
|
667
|
+
totalTicks,
|
|
668
|
+
timestamp: Date.now(),
|
|
669
|
+
freeBytes: os2.platform() === "darwin" ? getAvailableMemMacOS() : os2.freemem()
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function toFKUtilization(sample, previous) {
|
|
673
|
+
const idleTicks = sample.idleTicks - previous.idleTicks;
|
|
674
|
+
const totalTicks = sample.totalTicks - previous.totalTicks;
|
|
675
|
+
const cpuUtilization = Math.floor((1 - idleTicks / totalTicks) * 1e4) / 100;
|
|
676
|
+
const memoryUtilization = Math.floor((1 - sample.freeBytes / os2.totalmem()) * 1e4) / 100;
|
|
677
|
+
return {
|
|
678
|
+
cpuUtilization,
|
|
679
|
+
memoryUtilization,
|
|
680
|
+
dts: sample.timestamp - previous.timestamp
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
var SystemUtilizationSampler = class {
|
|
684
|
+
result;
|
|
685
|
+
_lastSample = getSystemUtilization();
|
|
686
|
+
_timer;
|
|
687
|
+
constructor() {
|
|
688
|
+
this.result = {
|
|
689
|
+
samples: [],
|
|
690
|
+
startTimestamp: this._lastSample.timestamp,
|
|
691
|
+
totalMemoryBytes: os2.totalmem()
|
|
692
|
+
};
|
|
693
|
+
this._timer = setTimeout(this._addSample.bind(this), 50);
|
|
694
|
+
}
|
|
695
|
+
_addSample() {
|
|
696
|
+
const sample = getSystemUtilization();
|
|
697
|
+
this.result.samples.push(toFKUtilization(sample, this._lastSample));
|
|
698
|
+
this._lastSample = sample;
|
|
699
|
+
this._timer = setTimeout(this._addSample.bind(this), 1e3);
|
|
700
|
+
}
|
|
701
|
+
dispose() {
|
|
702
|
+
clearTimeout(this._timer);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// src/playwright-test.ts
|
|
707
|
+
var FlakinessReporter = class {
|
|
708
|
+
constructor(_options = {}) {
|
|
709
|
+
this._options = _options;
|
|
710
|
+
}
|
|
711
|
+
_config;
|
|
712
|
+
_rootSuite;
|
|
713
|
+
_results = new Multimap();
|
|
714
|
+
_unattributedErrors = [];
|
|
715
|
+
_filepathToSteps = new Multimap();
|
|
716
|
+
_systemUtilizationSampler = new SystemUtilizationSampler();
|
|
717
|
+
printsToStdio() {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
onBegin(config, suite) {
|
|
721
|
+
this._config = config;
|
|
722
|
+
this._rootSuite = suite;
|
|
723
|
+
}
|
|
724
|
+
onError(error) {
|
|
725
|
+
this._unattributedErrors.push(error);
|
|
726
|
+
}
|
|
727
|
+
onTestBegin(test) {
|
|
728
|
+
}
|
|
729
|
+
onTestEnd(test, result) {
|
|
730
|
+
this._results.set(test, result);
|
|
731
|
+
}
|
|
732
|
+
async _toFKSuites(context, pwSuite) {
|
|
733
|
+
const location = pwSuite.location;
|
|
734
|
+
if (pwSuite.type === "root" || pwSuite.type === "project" || !location)
|
|
735
|
+
return (await Promise.all(pwSuite.suites.map((suite) => this._toFKSuites(context, suite)))).flat();
|
|
736
|
+
let type = "suite";
|
|
737
|
+
if (pwSuite.type === "file")
|
|
738
|
+
type = "file";
|
|
739
|
+
else if (pwSuite.type === "describe" && !pwSuite.title)
|
|
740
|
+
type = "anonymous suite";
|
|
741
|
+
return [{
|
|
742
|
+
type,
|
|
743
|
+
title: pwSuite.title,
|
|
744
|
+
location: this._createLocation(context, location),
|
|
745
|
+
suites: (await Promise.all(pwSuite.suites.map((suite) => this._toFKSuites(context, suite)))).flat(),
|
|
746
|
+
tests: await Promise.all(pwSuite.tests.map((test) => this._toFKTest(context, test)))
|
|
747
|
+
}];
|
|
748
|
+
}
|
|
749
|
+
async _toFKTest(context, pwTest) {
|
|
750
|
+
return {
|
|
751
|
+
title: pwTest.title,
|
|
752
|
+
// Playwright Test tags must start with '@' so we cut it off.
|
|
753
|
+
tags: pwTest.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
|
|
754
|
+
location: this._createLocation(context, pwTest.location),
|
|
755
|
+
// de-duplication of tests will happen later, so here we will have all attempts.
|
|
756
|
+
attempts: await Promise.all(this._results.getAll(pwTest).map((result) => this._toFKRunAttempt(context, pwTest, result)))
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async _toFKRunAttempt(context, pwTest, result) {
|
|
760
|
+
const attachments = [];
|
|
761
|
+
const attempt = {
|
|
762
|
+
timeout: parseDurationMS(pwTest.timeout),
|
|
763
|
+
annotations: pwTest.annotations.map((annotation) => ({
|
|
764
|
+
type: annotation.type,
|
|
765
|
+
description: annotation.description,
|
|
766
|
+
location: annotation.location ? this._createLocation(context, annotation.location) : void 0
|
|
767
|
+
})),
|
|
768
|
+
environmentIdx: context.project2environmentIdx.get(pwTest.parent.project()),
|
|
769
|
+
expectedStatus: pwTest.expectedStatus,
|
|
770
|
+
parallelIndex: result.parallelIndex,
|
|
771
|
+
status: result.status,
|
|
772
|
+
errors: result.errors && result.errors.length ? result.errors.map((error) => this._toFKTestError(context, error)) : void 0,
|
|
773
|
+
stdout: result.stdout ? result.stdout.map(toSTDIOEntry) : void 0,
|
|
774
|
+
stderr: result.stderr ? result.stderr.map(toSTDIOEntry) : void 0,
|
|
775
|
+
steps: result.steps ? result.steps.map((jsonTestStep) => this._toFKTestStep(context, jsonTestStep)) : void 0,
|
|
776
|
+
startTimestamp: +result.startTime,
|
|
777
|
+
duration: +result.duration,
|
|
778
|
+
attachments
|
|
779
|
+
};
|
|
780
|
+
await Promise.all((result.attachments ?? []).map(async (jsonAttachment) => {
|
|
781
|
+
if (jsonAttachment.path && !await existsAsync(jsonAttachment.path)) {
|
|
782
|
+
context.unaccessibleAttachmentPaths.push(jsonAttachment.path);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const id = jsonAttachment.path ? await sha1File(jsonAttachment.path) : sha1Buffer(jsonAttachment.body ?? "");
|
|
786
|
+
context.attachments.set(id, {
|
|
787
|
+
contentType: jsonAttachment.contentType,
|
|
788
|
+
id,
|
|
789
|
+
body: jsonAttachment.body,
|
|
790
|
+
path: jsonAttachment.path
|
|
791
|
+
});
|
|
792
|
+
attachments.push({
|
|
793
|
+
id,
|
|
794
|
+
name: jsonAttachment.name,
|
|
795
|
+
contentType: jsonAttachment.contentType
|
|
796
|
+
});
|
|
797
|
+
}));
|
|
798
|
+
return attempt;
|
|
799
|
+
}
|
|
800
|
+
_toFKTestStep(context, pwStep) {
|
|
801
|
+
const step = {
|
|
802
|
+
// NOTE: jsonStep.duration was -1 in some playwright versions
|
|
803
|
+
duration: parseDurationMS(Math.max(pwStep.duration, 0)),
|
|
804
|
+
title: pwStep.title,
|
|
805
|
+
location: pwStep.location ? this._createLocation(context, pwStep.location) : void 0
|
|
806
|
+
};
|
|
807
|
+
if (pwStep.location) {
|
|
808
|
+
const resolvedPath = path.resolve(pwStep.location.file);
|
|
809
|
+
this._filepathToSteps.set(resolvedPath, step);
|
|
810
|
+
}
|
|
811
|
+
if (pwStep.error)
|
|
812
|
+
step.error = this._toFKTestError(context, pwStep.error);
|
|
813
|
+
if (pwStep.steps)
|
|
814
|
+
step.steps = pwStep.steps.map((childJSONStep) => this._toFKTestStep(context, childJSONStep));
|
|
815
|
+
return step;
|
|
816
|
+
}
|
|
817
|
+
_createLocation(context, pwLocation) {
|
|
818
|
+
return {
|
|
819
|
+
file: gitFilePath(context.gitRoot, normalizePath(pwLocation.file)),
|
|
820
|
+
line: pwLocation.line,
|
|
821
|
+
column: pwLocation.column
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
_toFKTestError(context, pwError) {
|
|
825
|
+
return {
|
|
826
|
+
location: pwError.location ? this._createLocation(context, pwError.location) : void 0,
|
|
827
|
+
message: stripAnsi(pwError.message ?? "").split("\n")[0],
|
|
828
|
+
snippet: pwError.snippet,
|
|
829
|
+
stack: pwError.stack,
|
|
830
|
+
value: pwError.value
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async onEnd(result) {
|
|
834
|
+
this._systemUtilizationSampler.dispose();
|
|
835
|
+
if (!this._config || !this._rootSuite)
|
|
836
|
+
throw new Error("ERROR: failed to resolve config");
|
|
837
|
+
const uploadOptions = ReportUploader.optionsFromEnv({
|
|
838
|
+
flakinessAccessToken: this._options.token,
|
|
839
|
+
flakinessEndpoint: this._options.endpoint
|
|
840
|
+
});
|
|
841
|
+
if (!uploadOptions) {
|
|
842
|
+
console.log(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
let commitId;
|
|
846
|
+
try {
|
|
847
|
+
commitId = gitCommitInfo(this._config.rootDir);
|
|
848
|
+
} catch (e) {
|
|
849
|
+
console.log(`[flakiness.io] Uploading skipped since failed to get commit info - is this a git repo?`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const gitRoot = normalizePath(computeGitRoot(this._config.rootDir));
|
|
853
|
+
const configPath = this._config.configFile ? gitFilePath(gitRoot, normalizePath(this._config.configFile)) : void 0;
|
|
854
|
+
const context = {
|
|
855
|
+
project2environmentIdx: /* @__PURE__ */ new Map(),
|
|
856
|
+
testBaseDir: normalizePath(this._config.rootDir),
|
|
857
|
+
gitRoot,
|
|
858
|
+
attachments: /* @__PURE__ */ new Map(),
|
|
859
|
+
unaccessibleAttachmentPaths: []
|
|
860
|
+
};
|
|
861
|
+
const environmentsMap = createEnvironments(this._config.projects);
|
|
862
|
+
if (this._options.collectBrowserVersions) {
|
|
863
|
+
try {
|
|
864
|
+
let playwrightPath = fs4.realpathSync(process.argv[1]);
|
|
865
|
+
while (path.basename(playwrightPath) !== "test")
|
|
866
|
+
playwrightPath = path.dirname(playwrightPath);
|
|
867
|
+
const module = await import(path.join(playwrightPath, "index.js"));
|
|
868
|
+
for (const [project, env] of environmentsMap) {
|
|
869
|
+
const { browserName = "chromium", channel, headless } = project.use;
|
|
870
|
+
let browserType;
|
|
871
|
+
switch (browserName) {
|
|
872
|
+
case "chromium":
|
|
873
|
+
browserType = module.default.chromium;
|
|
874
|
+
break;
|
|
875
|
+
case "firefox":
|
|
876
|
+
browserType = module.default.firefox;
|
|
877
|
+
break;
|
|
878
|
+
case "webkit":
|
|
879
|
+
browserType = module.default.webkit;
|
|
880
|
+
break;
|
|
881
|
+
default:
|
|
882
|
+
throw new Error(`Unsupported browser: ${browserName}`);
|
|
883
|
+
}
|
|
884
|
+
const browser = await browserType.launch({ channel, headless });
|
|
885
|
+
const version = browser.version();
|
|
886
|
+
await browser.close();
|
|
887
|
+
env.userSuppliedData ??= {};
|
|
888
|
+
env.userSuppliedData["browser"] = (channel ?? browserName).toLowerCase().trim() + " " + version;
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
console.error("[flakiness.io] failed to resolve browser version", e);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const environments = [...environmentsMap.values()];
|
|
895
|
+
for (let envIdx = 0; envIdx < environments.length; ++envIdx)
|
|
896
|
+
context.project2environmentIdx.set(this._config.projects[envIdx], envIdx);
|
|
897
|
+
const relatedCommitIds = this._options.relatedCommitIds;
|
|
898
|
+
const report = FK.dedupeSuitesTestsEnvironments({
|
|
899
|
+
category: "playwright",
|
|
900
|
+
commitId,
|
|
901
|
+
relatedCommitIds,
|
|
902
|
+
systemUtilization: this._systemUtilizationSampler.result,
|
|
903
|
+
configPath,
|
|
904
|
+
url: inferRunUrl(),
|
|
905
|
+
environments,
|
|
906
|
+
suites: await this._toFKSuites(context, this._rootSuite),
|
|
907
|
+
opaqueData: this._config,
|
|
908
|
+
unattributedErrors: this._unattributedErrors.map((e) => this._toFKTestError(context, e)),
|
|
909
|
+
duration: parseDurationMS(result.duration),
|
|
910
|
+
startTimestamp: +result.startTime
|
|
911
|
+
});
|
|
912
|
+
createTestStepSnippets(this._filepathToSteps);
|
|
913
|
+
for (const unaccessibleAttachment of context.unaccessibleAttachmentPaths)
|
|
914
|
+
console.warn(`[flakiness.io] WARN: cannot access attachment ${unaccessibleAttachment}`);
|
|
915
|
+
const uploadError = await ReportUploader.upload({
|
|
916
|
+
report,
|
|
917
|
+
attachments: [...context.attachments.values()],
|
|
918
|
+
...uploadOptions,
|
|
919
|
+
log: console.log
|
|
920
|
+
});
|
|
921
|
+
if (uploadError)
|
|
922
|
+
return { status: "failed" };
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
function toSTDIOEntry(data) {
|
|
926
|
+
if (Buffer.isBuffer(data))
|
|
927
|
+
return { buffer: data.toString("base64") };
|
|
928
|
+
return { text: data };
|
|
929
|
+
}
|
|
930
|
+
export {
|
|
931
|
+
FlakinessReporter as default
|
|
932
|
+
};
|
|
933
|
+
//# sourceMappingURL=playwright-test.js.map
|