@flakiness/sdk 0.95.0 → 0.97.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 +1229 -386
- package/lib/cli/cmd-convert.js +9 -12
- package/lib/cli/cmd-download.js +9 -459
- package/lib/cli/cmd-link.js +84 -271
- package/lib/cli/cmd-login.js +26 -232
- package/lib/cli/cmd-logout.js +22 -230
- package/lib/cli/cmd-show-report.js +488 -0
- package/lib/cli/cmd-status.js +84 -271
- package/lib/cli/cmd-unlink.js +68 -35
- package/lib/cli/cmd-upload-playwright-json.js +52 -257
- package/lib/cli/cmd-upload.js +92 -288
- package/lib/cli/cmd-whoami.js +16 -230
- package/lib/{flakinessLink.js → flakinessConfig.js} +65 -36
- 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 +496 -318
- package/lib/playwrightJSONReport.js +12 -15
- package/lib/reportUploader.js +47 -248
- package/lib/serverapi.js +10 -230
- package/lib/utils.js +46 -18
- package/package.json +16 -5
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cmd-serve.js +0 -7
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
|
|
|
@@ -350,7 +166,8 @@ var ReportUploader = class _ReportUploader {
|
|
|
350
166
|
static async upload(options) {
|
|
351
167
|
const uploaderOptions = _ReportUploader.optionsFromEnv(options);
|
|
352
168
|
if (!uploaderOptions) {
|
|
353
|
-
|
|
169
|
+
if (process.env.CI)
|
|
170
|
+
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
354
171
|
return void 0;
|
|
355
172
|
}
|
|
356
173
|
const uploader = new _ReportUploader(uploaderOptions);
|
|
@@ -383,11 +200,10 @@ var ReportUpload = class {
|
|
|
383
200
|
this._options = options;
|
|
384
201
|
this._report = report;
|
|
385
202
|
this._attachments = attachments;
|
|
386
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
203
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
387
204
|
}
|
|
388
205
|
async upload(options) {
|
|
389
206
|
const response = await this._api.run.startUpload.POST({
|
|
390
|
-
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
391
207
|
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
392
208
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
393
209
|
if (response?.error || !response.result)
|
|
@@ -398,17 +214,17 @@ var ReportUpload = class {
|
|
|
398
214
|
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
399
215
|
if (!uploadURL)
|
|
400
216
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
401
|
-
return this._uploadAttachment(attachment, uploadURL);
|
|
217
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
402
218
|
})
|
|
403
219
|
]);
|
|
404
220
|
const response2 = await this._api.run.completeUpload.POST({
|
|
405
221
|
upload_token: response.result.upload_token
|
|
406
222
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
407
|
-
const url = response2?.result?.report_url ? new
|
|
223
|
+
const url = response2?.result?.report_url ? new URL(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
408
224
|
return { success: true, reportUrl: url };
|
|
409
225
|
}
|
|
410
226
|
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
411
|
-
const compressed = syncCompression ?
|
|
227
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
412
228
|
const headers = {
|
|
413
229
|
"Content-Type": "application/json",
|
|
414
230
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -425,11 +241,34 @@ var ReportUpload = class {
|
|
|
425
241
|
await responseDataPromise;
|
|
426
242
|
}, HTTP_BACKOFF);
|
|
427
243
|
}
|
|
428
|
-
async _uploadAttachment(attachment, uploadUrl) {
|
|
429
|
-
const
|
|
244
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
245
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
246
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
247
|
+
if (!compressable && attachment.path) {
|
|
248
|
+
const attachmentPath = attachment.path;
|
|
249
|
+
await retryWithBackoff(async () => {
|
|
250
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
251
|
+
url: uploadUrl,
|
|
252
|
+
headers: {
|
|
253
|
+
"Content-Type": attachment.contentType,
|
|
254
|
+
"Content-Length": (await fs2.promises.stat(attachmentPath)).size + ""
|
|
255
|
+
},
|
|
256
|
+
method: "put"
|
|
257
|
+
});
|
|
258
|
+
fs2.createReadStream(attachmentPath).pipe(request);
|
|
259
|
+
await responseDataPromise;
|
|
260
|
+
}, HTTP_BACKOFF);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs2.promises.readFile(attachment.path) : void 0;
|
|
264
|
+
assert(buffer);
|
|
265
|
+
const encoding = compressable ? "br" : void 0;
|
|
266
|
+
if (compressable)
|
|
267
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
430
268
|
const headers = {
|
|
431
269
|
"Content-Type": attachment.contentType,
|
|
432
|
-
"Content-Length":
|
|
270
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
271
|
+
"Content-Encoding": encoding
|
|
433
272
|
};
|
|
434
273
|
await retryWithBackoff(async () => {
|
|
435
274
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
@@ -437,13 +276,8 @@ var ReportUpload = class {
|
|
|
437
276
|
headers,
|
|
438
277
|
method: "put"
|
|
439
278
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else {
|
|
443
|
-
if (attachment.body)
|
|
444
|
-
request.write(attachment.body);
|
|
445
|
-
request.end();
|
|
446
|
-
}
|
|
279
|
+
request.write(buffer);
|
|
280
|
+
request.end();
|
|
447
281
|
await responseDataPromise;
|
|
448
282
|
}, HTTP_BACKOFF);
|
|
449
283
|
}
|
|
@@ -451,34 +285,15 @@ var ReportUpload = class {
|
|
|
451
285
|
|
|
452
286
|
// src/cli/cmd-upload.ts
|
|
453
287
|
async function cmdUpload(relativePath, options) {
|
|
454
|
-
const fullPath =
|
|
455
|
-
if (!await
|
|
288
|
+
const fullPath = path2.resolve(relativePath);
|
|
289
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
456
290
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
457
291
|
process.exit(1);
|
|
458
292
|
}
|
|
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");
|
|
293
|
+
const text = await fs3.readFile(fullPath, "utf-8");
|
|
463
294
|
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
|
-
});
|
|
295
|
+
const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
|
|
296
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
482
297
|
if (missingAttachments.length && !options.ignoreMissingAttachments) {
|
|
483
298
|
console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
|
|
484
299
|
process.exit(1);
|
|
@@ -487,7 +302,7 @@ async function cmdUpload(relativePath, options) {
|
|
|
487
302
|
flakinessAccessToken: options.accessToken,
|
|
488
303
|
flakinessEndpoint: options.endpoint
|
|
489
304
|
});
|
|
490
|
-
const upload = uploader.createUpload(report,
|
|
305
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
491
306
|
const uploadResult = await upload.upload();
|
|
492
307
|
if (!uploadResult.success) {
|
|
493
308
|
console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
@@ -495,17 +310,6 @@ async function cmdUpload(relativePath, options) {
|
|
|
495
310
|
console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
496
311
|
}
|
|
497
312
|
}
|
|
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
313
|
export {
|
|
510
314
|
cmdUpload
|
|
511
315
|
};
|