@flakiness/sdk 0.95.0 → 0.96.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/cli/cli.js +1199 -380
- package/lib/cli/cmd-convert.js +8 -9
- package/lib/cli/cmd-download.js +9 -459
- package/lib/cli/cmd-link.js +83 -268
- package/lib/cli/cmd-login.js +18 -230
- package/lib/cli/cmd-logout.js +22 -230
- package/lib/cli/cmd-serve.js +479 -1
- package/lib/cli/cmd-status.js +83 -268
- package/lib/cli/cmd-unlink.js +67 -32
- package/lib/cli/cmd-upload-playwright-json.js +49 -253
- package/lib/cli/cmd-upload.js +90 -287
- package/lib/cli/cmd-whoami.js +16 -230
- package/lib/{flakinessLink.js → flakinessConfig.js} +64 -33
- package/lib/flakinessSession.js +16 -230
- package/lib/junit.js +8 -9
- package/lib/localGit.js +43 -0
- package/lib/localReportApi.js +40 -0
- package/lib/localReportServer.js +300 -0
- package/lib/playwright-test.js +470 -309
- package/lib/playwrightJSONReport.js +11 -12
- package/lib/reportUploader.js +45 -247
- package/lib/serverapi.js +10 -230
- package/lib/utils.js +45 -15
- package/package.json +15 -5
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cmd-upload.js
CHANGED
|
@@ -1,256 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/cmd-upload.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import path from "path";
|
|
4
|
+
import fs3 from "fs/promises";
|
|
5
|
+
import path2 from "path";
|
|
7
6
|
|
|
8
7
|
// src/reportUploader.ts
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
8
|
+
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
9
|
+
import assert from "assert";
|
|
10
|
+
import fs2 from "fs";
|
|
11
|
+
import { URL } from "url";
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
((TypedHTTP2) => {
|
|
16
|
-
TypedHTTP2.StatusCodes = {
|
|
17
|
-
Informational: {
|
|
18
|
-
CONTINUE: 100,
|
|
19
|
-
SWITCHING_PROTOCOLS: 101,
|
|
20
|
-
PROCESSING: 102,
|
|
21
|
-
EARLY_HINTS: 103
|
|
22
|
-
},
|
|
23
|
-
Success: {
|
|
24
|
-
OK: 200,
|
|
25
|
-
CREATED: 201,
|
|
26
|
-
ACCEPTED: 202,
|
|
27
|
-
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
28
|
-
NO_CONTENT: 204,
|
|
29
|
-
RESET_CONTENT: 205,
|
|
30
|
-
PARTIAL_CONTENT: 206,
|
|
31
|
-
MULTI_STATUS: 207
|
|
32
|
-
},
|
|
33
|
-
Redirection: {
|
|
34
|
-
MULTIPLE_CHOICES: 300,
|
|
35
|
-
MOVED_PERMANENTLY: 301,
|
|
36
|
-
MOVED_TEMPORARILY: 302,
|
|
37
|
-
SEE_OTHER: 303,
|
|
38
|
-
NOT_MODIFIED: 304,
|
|
39
|
-
USE_PROXY: 305,
|
|
40
|
-
TEMPORARY_REDIRECT: 307,
|
|
41
|
-
PERMANENT_REDIRECT: 308
|
|
42
|
-
},
|
|
43
|
-
ClientErrors: {
|
|
44
|
-
BAD_REQUEST: 400,
|
|
45
|
-
UNAUTHORIZED: 401,
|
|
46
|
-
PAYMENT_REQUIRED: 402,
|
|
47
|
-
FORBIDDEN: 403,
|
|
48
|
-
NOT_FOUND: 404,
|
|
49
|
-
METHOD_NOT_ALLOWED: 405,
|
|
50
|
-
NOT_ACCEPTABLE: 406,
|
|
51
|
-
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
52
|
-
REQUEST_TIMEOUT: 408,
|
|
53
|
-
CONFLICT: 409,
|
|
54
|
-
GONE: 410,
|
|
55
|
-
LENGTH_REQUIRED: 411,
|
|
56
|
-
PRECONDITION_FAILED: 412,
|
|
57
|
-
REQUEST_TOO_LONG: 413,
|
|
58
|
-
REQUEST_URI_TOO_LONG: 414,
|
|
59
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
60
|
-
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
61
|
-
EXPECTATION_FAILED: 417,
|
|
62
|
-
IM_A_TEAPOT: 418,
|
|
63
|
-
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
64
|
-
METHOD_FAILURE: 420,
|
|
65
|
-
MISDIRECTED_REQUEST: 421,
|
|
66
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
67
|
-
LOCKED: 423,
|
|
68
|
-
FAILED_DEPENDENCY: 424,
|
|
69
|
-
UPGRADE_REQUIRED: 426,
|
|
70
|
-
PRECONDITION_REQUIRED: 428,
|
|
71
|
-
TOO_MANY_REQUESTS: 429,
|
|
72
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
73
|
-
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
74
|
-
},
|
|
75
|
-
ServerErrors: {
|
|
76
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
77
|
-
NOT_IMPLEMENTED: 501,
|
|
78
|
-
BAD_GATEWAY: 502,
|
|
79
|
-
SERVICE_UNAVAILABLE: 503,
|
|
80
|
-
GATEWAY_TIMEOUT: 504,
|
|
81
|
-
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
82
|
-
INSUFFICIENT_STORAGE: 507,
|
|
83
|
-
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const AllErrorCodes = {
|
|
87
|
-
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
88
|
-
...TypedHTTP2.StatusCodes.ServerErrors
|
|
89
|
-
};
|
|
90
|
-
class HttpError extends Error {
|
|
91
|
-
constructor(status, message) {
|
|
92
|
-
super(message);
|
|
93
|
-
this.status = status;
|
|
94
|
-
}
|
|
95
|
-
static withCode(code, message) {
|
|
96
|
-
const statusCode = AllErrorCodes[code];
|
|
97
|
-
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
98
|
-
return new HttpError(statusCode, message ?? defaultMessage);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
TypedHTTP2.HttpError = HttpError;
|
|
102
|
-
function isInformationalResponse(response) {
|
|
103
|
-
return response.status >= 100 && response.status < 200;
|
|
104
|
-
}
|
|
105
|
-
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
106
|
-
function isSuccessResponse(response) {
|
|
107
|
-
return response.status >= 200 && response.status < 300;
|
|
108
|
-
}
|
|
109
|
-
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
110
|
-
function isRedirectResponse(response) {
|
|
111
|
-
return response.status >= 300 && response.status < 400;
|
|
112
|
-
}
|
|
113
|
-
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
114
|
-
function isErrorResponse(response) {
|
|
115
|
-
return response.status >= 400 && response.status < 600;
|
|
116
|
-
}
|
|
117
|
-
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
118
|
-
function info(status) {
|
|
119
|
-
return { status };
|
|
120
|
-
}
|
|
121
|
-
TypedHTTP2.info = info;
|
|
122
|
-
function ok(data, status) {
|
|
123
|
-
return {
|
|
124
|
-
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
125
|
-
data
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
TypedHTTP2.ok = ok;
|
|
129
|
-
function redirect(url, status = 302) {
|
|
130
|
-
return { status, url };
|
|
131
|
-
}
|
|
132
|
-
TypedHTTP2.redirect = redirect;
|
|
133
|
-
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
134
|
-
return { status, message };
|
|
135
|
-
}
|
|
136
|
-
TypedHTTP2.error = error;
|
|
137
|
-
class Router {
|
|
138
|
-
constructor(_resolveContext) {
|
|
139
|
-
this._resolveContext = _resolveContext;
|
|
140
|
-
}
|
|
141
|
-
static create() {
|
|
142
|
-
return new Router(async (e) => e.ctx);
|
|
143
|
-
}
|
|
144
|
-
rawMethod(method, route) {
|
|
145
|
-
return {
|
|
146
|
-
[method]: {
|
|
147
|
-
method,
|
|
148
|
-
input: route.input,
|
|
149
|
-
etag: route.etag,
|
|
150
|
-
resolveContext: this._resolveContext,
|
|
151
|
-
handler: route.handler
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
get(route) {
|
|
156
|
-
return this.rawMethod("GET", {
|
|
157
|
-
...route,
|
|
158
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
post(route) {
|
|
162
|
-
return this.rawMethod("POST", {
|
|
163
|
-
...route,
|
|
164
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
use(resolveContext) {
|
|
168
|
-
return new Router(async (options) => {
|
|
169
|
-
const m = await this._resolveContext(options);
|
|
170
|
-
return resolveContext({ ...options, ctx: m });
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
TypedHTTP2.Router = Router;
|
|
175
|
-
function createClient(base, fetchCallback) {
|
|
176
|
-
function buildUrl(path2, input, options) {
|
|
177
|
-
const method = path2.at(-1);
|
|
178
|
-
const url = new URL(path2.slice(0, path2.length - 1).join("/"), base);
|
|
179
|
-
const signal = options?.signal;
|
|
180
|
-
let body = void 0;
|
|
181
|
-
if (method === "GET" && input)
|
|
182
|
-
url.searchParams.set("input", JSON.stringify(input));
|
|
183
|
-
else if (method !== "GET" && input)
|
|
184
|
-
body = JSON.stringify(input);
|
|
185
|
-
return {
|
|
186
|
-
url,
|
|
187
|
-
method,
|
|
188
|
-
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
189
|
-
body,
|
|
190
|
-
signal
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function createProxy(path2 = []) {
|
|
194
|
-
return new Proxy(() => {
|
|
195
|
-
}, {
|
|
196
|
-
get(target, prop) {
|
|
197
|
-
if (typeof prop === "symbol")
|
|
198
|
-
return void 0;
|
|
199
|
-
if (prop === "prepare")
|
|
200
|
-
return (input, options) => buildUrl(path2, input, options);
|
|
201
|
-
const newPath = [...path2, prop];
|
|
202
|
-
return createProxy(newPath);
|
|
203
|
-
},
|
|
204
|
-
apply(target, thisArg, args) {
|
|
205
|
-
const options = buildUrl(path2, args[0], args[1]);
|
|
206
|
-
return fetchCallback(options.url, {
|
|
207
|
-
method: options.method,
|
|
208
|
-
body: options.body,
|
|
209
|
-
headers: options.headers,
|
|
210
|
-
signal: options.signal
|
|
211
|
-
}).then(async (response) => {
|
|
212
|
-
if (response.status >= 200 && response.status < 300) {
|
|
213
|
-
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
214
|
-
const text = await response.text();
|
|
215
|
-
return text.length ? JSON.parse(text) : void 0;
|
|
216
|
-
}
|
|
217
|
-
return await response.blob();
|
|
218
|
-
}
|
|
219
|
-
if (response.status >= 400 && response.status < 600) {
|
|
220
|
-
const text = await response.text();
|
|
221
|
-
if (text)
|
|
222
|
-
throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
|
|
223
|
-
else
|
|
224
|
-
throw new Error(`HTTP request failed with status ${response.status}`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
return createProxy();
|
|
231
|
-
}
|
|
232
|
-
TypedHTTP2.createClient = createClient;
|
|
233
|
-
})(TypedHTTP || (TypedHTTP = {}));
|
|
13
|
+
// src/serverapi.ts
|
|
14
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
234
15
|
|
|
235
16
|
// src/utils.ts
|
|
17
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
18
|
+
import fs from "fs";
|
|
236
19
|
import http from "http";
|
|
237
20
|
import https from "https";
|
|
238
|
-
import
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
244
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
21
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
22
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
23
|
+
function errorText(error) {
|
|
24
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
25
|
+
}
|
|
245
26
|
async function retryWithBackoff(job, backoff = []) {
|
|
246
27
|
for (const timeout of backoff) {
|
|
247
28
|
try {
|
|
248
29
|
return await job();
|
|
249
30
|
} catch (e) {
|
|
250
31
|
if (e instanceof AggregateError)
|
|
251
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
32
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
252
33
|
else if (e instanceof Error)
|
|
253
|
-
console.error(`[flakiness.io err]`, e
|
|
34
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
254
35
|
else
|
|
255
36
|
console.error(`[flakiness.io err]`, e);
|
|
256
37
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -268,6 +49,7 @@ var httpUtils;
|
|
|
268
49
|
reject = b;
|
|
269
50
|
});
|
|
270
51
|
const protocol = url.startsWith("https") ? https : http;
|
|
52
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
271
53
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
272
54
|
const chunks = [];
|
|
273
55
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -320,6 +102,40 @@ var httpUtils;
|
|
|
320
102
|
httpUtils2.postJSON = postJSON;
|
|
321
103
|
})(httpUtils || (httpUtils = {}));
|
|
322
104
|
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");
|
|
105
|
+
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
106
|
+
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
107
|
+
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
108
|
+
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
109
|
+
const missingAttachments = /* @__PURE__ */ new Set();
|
|
110
|
+
FlakinessReport.visitTests(report, (test) => {
|
|
111
|
+
for (const attempt of test.attempts) {
|
|
112
|
+
for (const attachment of attempt.attachments ?? []) {
|
|
113
|
+
const attachmentPath = filenameToPath.get(attachment.id);
|
|
114
|
+
if (!attachmentPath) {
|
|
115
|
+
missingAttachments.add(attachment.id);
|
|
116
|
+
} else {
|
|
117
|
+
attachmentIdToPath.set(attachment.id, {
|
|
118
|
+
contentType: attachment.contentType,
|
|
119
|
+
id: attachment.id,
|
|
120
|
+
path: attachmentPath
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
127
|
+
}
|
|
128
|
+
async function listFilesRecursively(dir, result = []) {
|
|
129
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const fullPath = path.join(dir, entry.name);
|
|
132
|
+
if (entry.isDirectory())
|
|
133
|
+
await listFilesRecursively(fullPath, result);
|
|
134
|
+
else
|
|
135
|
+
result.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
323
139
|
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
324
140
|
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
325
141
|
|
|
@@ -383,11 +199,10 @@ var ReportUpload = class {
|
|
|
383
199
|
this._options = options;
|
|
384
200
|
this._report = report;
|
|
385
201
|
this._attachments = attachments;
|
|
386
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
202
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
387
203
|
}
|
|
388
204
|
async upload(options) {
|
|
389
205
|
const response = await this._api.run.startUpload.POST({
|
|
390
|
-
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
391
206
|
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
392
207
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
393
208
|
if (response?.error || !response.result)
|
|
@@ -398,17 +213,17 @@ var ReportUpload = class {
|
|
|
398
213
|
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
399
214
|
if (!uploadURL)
|
|
400
215
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
401
|
-
return this._uploadAttachment(attachment, uploadURL);
|
|
216
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
402
217
|
})
|
|
403
218
|
]);
|
|
404
219
|
const response2 = await this._api.run.completeUpload.POST({
|
|
405
220
|
upload_token: response.result.upload_token
|
|
406
221
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
407
|
-
const url = response2?.result?.report_url ? new
|
|
222
|
+
const url = response2?.result?.report_url ? new URL(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
408
223
|
return { success: true, reportUrl: url };
|
|
409
224
|
}
|
|
410
225
|
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
411
|
-
const compressed = syncCompression ?
|
|
226
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
412
227
|
const headers = {
|
|
413
228
|
"Content-Type": "application/json",
|
|
414
229
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -425,11 +240,34 @@ var ReportUpload = class {
|
|
|
425
240
|
await responseDataPromise;
|
|
426
241
|
}, HTTP_BACKOFF);
|
|
427
242
|
}
|
|
428
|
-
async _uploadAttachment(attachment, uploadUrl) {
|
|
429
|
-
const
|
|
243
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
244
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
245
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
246
|
+
if (!compressable && attachment.path) {
|
|
247
|
+
const attachmentPath = attachment.path;
|
|
248
|
+
await retryWithBackoff(async () => {
|
|
249
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
250
|
+
url: uploadUrl,
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": attachment.contentType,
|
|
253
|
+
"Content-Length": (await fs2.promises.stat(attachmentPath)).size + ""
|
|
254
|
+
},
|
|
255
|
+
method: "put"
|
|
256
|
+
});
|
|
257
|
+
fs2.createReadStream(attachmentPath).pipe(request);
|
|
258
|
+
await responseDataPromise;
|
|
259
|
+
}, HTTP_BACKOFF);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs2.promises.readFile(attachment.path) : void 0;
|
|
263
|
+
assert(buffer);
|
|
264
|
+
const encoding = compressable ? "br" : void 0;
|
|
265
|
+
if (compressable)
|
|
266
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
430
267
|
const headers = {
|
|
431
268
|
"Content-Type": attachment.contentType,
|
|
432
|
-
"Content-Length":
|
|
269
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
270
|
+
"Content-Encoding": encoding
|
|
433
271
|
};
|
|
434
272
|
await retryWithBackoff(async () => {
|
|
435
273
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
@@ -437,13 +275,8 @@ var ReportUpload = class {
|
|
|
437
275
|
headers,
|
|
438
276
|
method: "put"
|
|
439
277
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else {
|
|
443
|
-
if (attachment.body)
|
|
444
|
-
request.write(attachment.body);
|
|
445
|
-
request.end();
|
|
446
|
-
}
|
|
278
|
+
request.write(buffer);
|
|
279
|
+
request.end();
|
|
447
280
|
await responseDataPromise;
|
|
448
281
|
}, HTTP_BACKOFF);
|
|
449
282
|
}
|
|
@@ -451,34 +284,15 @@ var ReportUpload = class {
|
|
|
451
284
|
|
|
452
285
|
// src/cli/cmd-upload.ts
|
|
453
286
|
async function cmdUpload(relativePath, options) {
|
|
454
|
-
const fullPath =
|
|
455
|
-
if (!await
|
|
287
|
+
const fullPath = path2.resolve(relativePath);
|
|
288
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
456
289
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
457
290
|
process.exit(1);
|
|
458
291
|
}
|
|
459
|
-
const
|
|
460
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
461
|
-
const attachmentIdToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
462
|
-
const text = await fs2.readFile(fullPath, "utf-8");
|
|
292
|
+
const text = await fs3.readFile(fullPath, "utf-8");
|
|
463
293
|
const report = JSON.parse(text);
|
|
464
|
-
const
|
|
465
|
-
const missingAttachments =
|
|
466
|
-
FlakinessReport.visitTests(report, (test) => {
|
|
467
|
-
for (const attempt of test.attempts) {
|
|
468
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
469
|
-
const file = attachmentIdToPath.get(attachment.id);
|
|
470
|
-
if (!file) {
|
|
471
|
-
missingAttachments.push(attachment);
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
attachments.push({
|
|
475
|
-
contentType: attachment.contentType,
|
|
476
|
-
id: attachment.id,
|
|
477
|
-
path: file
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
});
|
|
294
|
+
const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
|
|
295
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
482
296
|
if (missingAttachments.length && !options.ignoreMissingAttachments) {
|
|
483
297
|
console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
|
|
484
298
|
process.exit(1);
|
|
@@ -487,7 +301,7 @@ async function cmdUpload(relativePath, options) {
|
|
|
487
301
|
flakinessAccessToken: options.accessToken,
|
|
488
302
|
flakinessEndpoint: options.endpoint
|
|
489
303
|
});
|
|
490
|
-
const upload = uploader.createUpload(report,
|
|
304
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
491
305
|
const uploadResult = await upload.upload();
|
|
492
306
|
if (!uploadResult.success) {
|
|
493
307
|
console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
@@ -495,17 +309,6 @@ async function cmdUpload(relativePath, options) {
|
|
|
495
309
|
console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
496
310
|
}
|
|
497
311
|
}
|
|
498
|
-
async function listFilesRecursively(dir, result = []) {
|
|
499
|
-
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
500
|
-
for (const entry of entries) {
|
|
501
|
-
const fullPath = path.join(dir, entry.name);
|
|
502
|
-
if (entry.isDirectory())
|
|
503
|
-
await listFilesRecursively(fullPath, result);
|
|
504
|
-
else
|
|
505
|
-
result.push(fullPath);
|
|
506
|
-
}
|
|
507
|
-
return result;
|
|
508
|
-
}
|
|
509
312
|
export {
|
|
510
313
|
cmdUpload
|
|
511
314
|
};
|