@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/playwrightJSONReport.ts
|
|
2
|
-
import { FlakinessReport as FK, FlakinessReport } from "@flakiness/report";
|
|
2
|
+
import { FlakinessReport as FK, FlakinessReport as FlakinessReport2 } from "@flakiness/report";
|
|
3
3
|
import debug from "debug";
|
|
4
4
|
import { posix as posixPath2 } from "path";
|
|
5
5
|
|
|
6
6
|
// src/utils.ts
|
|
7
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
7
8
|
import assert from "assert";
|
|
8
9
|
import { spawnSync } from "child_process";
|
|
9
10
|
import crypto from "crypto";
|
|
@@ -11,14 +12,7 @@ import fs from "fs";
|
|
|
11
12
|
import http from "http";
|
|
12
13
|
import https from "https";
|
|
13
14
|
import os from "os";
|
|
14
|
-
import { posix as posixPath, win32 as win32Path } from "path";
|
|
15
|
-
import util from "util";
|
|
16
|
-
import zlib from "zlib";
|
|
17
|
-
var gzipAsync = util.promisify(zlib.gzip);
|
|
18
|
-
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
19
|
-
var gunzipSync = zlib.gunzipSync;
|
|
20
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
21
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
15
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
22
16
|
async function existsAsync(aPath) {
|
|
23
17
|
return fs.promises.stat(aPath).then(() => true).catch((e) => false);
|
|
24
18
|
}
|
|
@@ -43,6 +37,10 @@ function sha1File(filePath) {
|
|
|
43
37
|
});
|
|
44
38
|
});
|
|
45
39
|
}
|
|
40
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
41
|
+
function errorText(error) {
|
|
42
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
43
|
+
}
|
|
46
44
|
function sha1Buffer(data) {
|
|
47
45
|
const hash = crypto.createHash("sha1");
|
|
48
46
|
hash.update(data);
|
|
@@ -54,9 +52,9 @@ async function retryWithBackoff(job, backoff = []) {
|
|
|
54
52
|
return await job();
|
|
55
53
|
} catch (e) {
|
|
56
54
|
if (e instanceof AggregateError)
|
|
57
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
55
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
58
56
|
else if (e instanceof Error)
|
|
59
|
-
console.error(`[flakiness.io err]`, e
|
|
57
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
60
58
|
else
|
|
61
59
|
console.error(`[flakiness.io err]`, e);
|
|
62
60
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -74,6 +72,7 @@ var httpUtils;
|
|
|
74
72
|
reject = b;
|
|
75
73
|
});
|
|
76
74
|
const protocol = url.startsWith("https") ? https : http;
|
|
75
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
77
76
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
78
77
|
const chunks = [];
|
|
79
78
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -288,7 +287,7 @@ var PlaywrightJSONReport;
|
|
|
288
287
|
};
|
|
289
288
|
const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
|
|
290
289
|
const report = {
|
|
291
|
-
category:
|
|
290
|
+
category: FlakinessReport2.CATEGORY_PLAYWRIGHT,
|
|
292
291
|
commitId: metadata.commitId,
|
|
293
292
|
configPath,
|
|
294
293
|
url: metadata.runURL,
|
package/lib/reportUploader.js
CHANGED
|
@@ -1,249 +1,29 @@
|
|
|
1
1
|
// src/reportUploader.ts
|
|
2
|
+
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
3
|
+
import assert from "assert";
|
|
2
4
|
import fs from "fs";
|
|
3
|
-
import { URL
|
|
4
|
-
import { brotliCompressSync as brotliCompressSync2 } from "zlib";
|
|
5
|
+
import { URL } from "url";
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
|
|
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(path, input, options) {
|
|
170
|
-
const method = path.at(-1);
|
|
171
|
-
const url = new URL(path.slice(0, path.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(path = []) {
|
|
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(path, input, options);
|
|
194
|
-
const newPath = [...path, prop];
|
|
195
|
-
return createProxy(newPath);
|
|
196
|
-
},
|
|
197
|
-
apply(target, thisArg, args) {
|
|
198
|
-
const options = buildUrl(path, 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 = {}));
|
|
7
|
+
// src/serverapi.ts
|
|
8
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
227
9
|
|
|
228
10
|
// src/utils.ts
|
|
11
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
229
12
|
import http from "http";
|
|
230
13
|
import https from "https";
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
var gunzipSync = zlib.gunzipSync;
|
|
236
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
237
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
14
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
15
|
+
function errorText(error) {
|
|
16
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
17
|
+
}
|
|
238
18
|
async function retryWithBackoff(job, backoff = []) {
|
|
239
19
|
for (const timeout of backoff) {
|
|
240
20
|
try {
|
|
241
21
|
return await job();
|
|
242
22
|
} catch (e) {
|
|
243
23
|
if (e instanceof AggregateError)
|
|
244
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
24
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
245
25
|
else if (e instanceof Error)
|
|
246
|
-
console.error(`[flakiness.io err]`, e
|
|
26
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
247
27
|
else
|
|
248
28
|
console.error(`[flakiness.io err]`, e);
|
|
249
29
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -261,6 +41,7 @@ var httpUtils;
|
|
|
261
41
|
reject = b;
|
|
262
42
|
});
|
|
263
43
|
const protocol = url.startsWith("https") ? https : http;
|
|
44
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
264
45
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
265
46
|
const chunks = [];
|
|
266
47
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -376,11 +157,10 @@ var ReportUpload = class {
|
|
|
376
157
|
this._options = options;
|
|
377
158
|
this._report = report;
|
|
378
159
|
this._attachments = attachments;
|
|
379
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
160
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
380
161
|
}
|
|
381
162
|
async upload(options) {
|
|
382
163
|
const response = await this._api.run.startUpload.POST({
|
|
383
|
-
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
384
164
|
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
385
165
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
386
166
|
if (response?.error || !response.result)
|
|
@@ -391,17 +171,17 @@ var ReportUpload = class {
|
|
|
391
171
|
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
392
172
|
if (!uploadURL)
|
|
393
173
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
394
|
-
return this._uploadAttachment(attachment, uploadURL);
|
|
174
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
395
175
|
})
|
|
396
176
|
]);
|
|
397
177
|
const response2 = await this._api.run.completeUpload.POST({
|
|
398
178
|
upload_token: response.result.upload_token
|
|
399
179
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ error: e, result: void 0 }));
|
|
400
|
-
const url = response2?.result?.report_url ? new
|
|
180
|
+
const url = response2?.result?.report_url ? new URL(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
|
|
401
181
|
return { success: true, reportUrl: url };
|
|
402
182
|
}
|
|
403
183
|
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
404
|
-
const compressed = syncCompression ?
|
|
184
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
405
185
|
const headers = {
|
|
406
186
|
"Content-Type": "application/json",
|
|
407
187
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -418,11 +198,34 @@ var ReportUpload = class {
|
|
|
418
198
|
await responseDataPromise;
|
|
419
199
|
}, HTTP_BACKOFF);
|
|
420
200
|
}
|
|
421
|
-
async _uploadAttachment(attachment, uploadUrl) {
|
|
422
|
-
const
|
|
201
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
202
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
203
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
204
|
+
if (!compressable && attachment.path) {
|
|
205
|
+
const attachmentPath = attachment.path;
|
|
206
|
+
await retryWithBackoff(async () => {
|
|
207
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
208
|
+
url: uploadUrl,
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": attachment.contentType,
|
|
211
|
+
"Content-Length": (await fs.promises.stat(attachmentPath)).size + ""
|
|
212
|
+
},
|
|
213
|
+
method: "put"
|
|
214
|
+
});
|
|
215
|
+
fs.createReadStream(attachmentPath).pipe(request);
|
|
216
|
+
await responseDataPromise;
|
|
217
|
+
}, HTTP_BACKOFF);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs.promises.readFile(attachment.path) : void 0;
|
|
221
|
+
assert(buffer);
|
|
222
|
+
const encoding = compressable ? "br" : void 0;
|
|
223
|
+
if (compressable)
|
|
224
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
423
225
|
const headers = {
|
|
424
226
|
"Content-Type": attachment.contentType,
|
|
425
|
-
"Content-Length":
|
|
227
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
228
|
+
"Content-Encoding": encoding
|
|
426
229
|
};
|
|
427
230
|
await retryWithBackoff(async () => {
|
|
428
231
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
@@ -430,13 +233,8 @@ var ReportUpload = class {
|
|
|
430
233
|
headers,
|
|
431
234
|
method: "put"
|
|
432
235
|
});
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
} else {
|
|
436
|
-
if (attachment.body)
|
|
437
|
-
request.write(attachment.body);
|
|
438
|
-
request.end();
|
|
439
|
-
}
|
|
236
|
+
request.write(buffer);
|
|
237
|
+
request.end();
|
|
440
238
|
await responseDataPromise;
|
|
441
239
|
}, HTTP_BACKOFF);
|
|
442
240
|
}
|
package/lib/serverapi.js
CHANGED
|
@@ -1,244 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
((TypedHTTP2) => {
|
|
4
|
-
TypedHTTP2.StatusCodes = {
|
|
5
|
-
Informational: {
|
|
6
|
-
CONTINUE: 100,
|
|
7
|
-
SWITCHING_PROTOCOLS: 101,
|
|
8
|
-
PROCESSING: 102,
|
|
9
|
-
EARLY_HINTS: 103
|
|
10
|
-
},
|
|
11
|
-
Success: {
|
|
12
|
-
OK: 200,
|
|
13
|
-
CREATED: 201,
|
|
14
|
-
ACCEPTED: 202,
|
|
15
|
-
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
16
|
-
NO_CONTENT: 204,
|
|
17
|
-
RESET_CONTENT: 205,
|
|
18
|
-
PARTIAL_CONTENT: 206,
|
|
19
|
-
MULTI_STATUS: 207
|
|
20
|
-
},
|
|
21
|
-
Redirection: {
|
|
22
|
-
MULTIPLE_CHOICES: 300,
|
|
23
|
-
MOVED_PERMANENTLY: 301,
|
|
24
|
-
MOVED_TEMPORARILY: 302,
|
|
25
|
-
SEE_OTHER: 303,
|
|
26
|
-
NOT_MODIFIED: 304,
|
|
27
|
-
USE_PROXY: 305,
|
|
28
|
-
TEMPORARY_REDIRECT: 307,
|
|
29
|
-
PERMANENT_REDIRECT: 308
|
|
30
|
-
},
|
|
31
|
-
ClientErrors: {
|
|
32
|
-
BAD_REQUEST: 400,
|
|
33
|
-
UNAUTHORIZED: 401,
|
|
34
|
-
PAYMENT_REQUIRED: 402,
|
|
35
|
-
FORBIDDEN: 403,
|
|
36
|
-
NOT_FOUND: 404,
|
|
37
|
-
METHOD_NOT_ALLOWED: 405,
|
|
38
|
-
NOT_ACCEPTABLE: 406,
|
|
39
|
-
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
40
|
-
REQUEST_TIMEOUT: 408,
|
|
41
|
-
CONFLICT: 409,
|
|
42
|
-
GONE: 410,
|
|
43
|
-
LENGTH_REQUIRED: 411,
|
|
44
|
-
PRECONDITION_FAILED: 412,
|
|
45
|
-
REQUEST_TOO_LONG: 413,
|
|
46
|
-
REQUEST_URI_TOO_LONG: 414,
|
|
47
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
48
|
-
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
49
|
-
EXPECTATION_FAILED: 417,
|
|
50
|
-
IM_A_TEAPOT: 418,
|
|
51
|
-
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
52
|
-
METHOD_FAILURE: 420,
|
|
53
|
-
MISDIRECTED_REQUEST: 421,
|
|
54
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
55
|
-
LOCKED: 423,
|
|
56
|
-
FAILED_DEPENDENCY: 424,
|
|
57
|
-
UPGRADE_REQUIRED: 426,
|
|
58
|
-
PRECONDITION_REQUIRED: 428,
|
|
59
|
-
TOO_MANY_REQUESTS: 429,
|
|
60
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
61
|
-
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
62
|
-
},
|
|
63
|
-
ServerErrors: {
|
|
64
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
65
|
-
NOT_IMPLEMENTED: 501,
|
|
66
|
-
BAD_GATEWAY: 502,
|
|
67
|
-
SERVICE_UNAVAILABLE: 503,
|
|
68
|
-
GATEWAY_TIMEOUT: 504,
|
|
69
|
-
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
70
|
-
INSUFFICIENT_STORAGE: 507,
|
|
71
|
-
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
const AllErrorCodes = {
|
|
75
|
-
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
76
|
-
...TypedHTTP2.StatusCodes.ServerErrors
|
|
77
|
-
};
|
|
78
|
-
class HttpError extends Error {
|
|
79
|
-
constructor(status, message) {
|
|
80
|
-
super(message);
|
|
81
|
-
this.status = status;
|
|
82
|
-
}
|
|
83
|
-
static withCode(code, message) {
|
|
84
|
-
const statusCode = AllErrorCodes[code];
|
|
85
|
-
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
86
|
-
return new HttpError(statusCode, message ?? defaultMessage);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
TypedHTTP2.HttpError = HttpError;
|
|
90
|
-
function isInformationalResponse(response) {
|
|
91
|
-
return response.status >= 100 && response.status < 200;
|
|
92
|
-
}
|
|
93
|
-
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
94
|
-
function isSuccessResponse(response) {
|
|
95
|
-
return response.status >= 200 && response.status < 300;
|
|
96
|
-
}
|
|
97
|
-
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
98
|
-
function isRedirectResponse(response) {
|
|
99
|
-
return response.status >= 300 && response.status < 400;
|
|
100
|
-
}
|
|
101
|
-
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
102
|
-
function isErrorResponse(response) {
|
|
103
|
-
return response.status >= 400 && response.status < 600;
|
|
104
|
-
}
|
|
105
|
-
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
106
|
-
function info(status) {
|
|
107
|
-
return { status };
|
|
108
|
-
}
|
|
109
|
-
TypedHTTP2.info = info;
|
|
110
|
-
function ok(data, status) {
|
|
111
|
-
return {
|
|
112
|
-
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
113
|
-
data
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
TypedHTTP2.ok = ok;
|
|
117
|
-
function redirect(url, status = 302) {
|
|
118
|
-
return { status, url };
|
|
119
|
-
}
|
|
120
|
-
TypedHTTP2.redirect = redirect;
|
|
121
|
-
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
122
|
-
return { status, message };
|
|
123
|
-
}
|
|
124
|
-
TypedHTTP2.error = error;
|
|
125
|
-
class Router {
|
|
126
|
-
constructor(_resolveContext) {
|
|
127
|
-
this._resolveContext = _resolveContext;
|
|
128
|
-
}
|
|
129
|
-
static create() {
|
|
130
|
-
return new Router(async (e) => e.ctx);
|
|
131
|
-
}
|
|
132
|
-
rawMethod(method, route) {
|
|
133
|
-
return {
|
|
134
|
-
[method]: {
|
|
135
|
-
method,
|
|
136
|
-
input: route.input,
|
|
137
|
-
etag: route.etag,
|
|
138
|
-
resolveContext: this._resolveContext,
|
|
139
|
-
handler: route.handler
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
get(route) {
|
|
144
|
-
return this.rawMethod("GET", {
|
|
145
|
-
...route,
|
|
146
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
post(route) {
|
|
150
|
-
return this.rawMethod("POST", {
|
|
151
|
-
...route,
|
|
152
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
use(resolveContext) {
|
|
156
|
-
return new Router(async (options) => {
|
|
157
|
-
const m = await this._resolveContext(options);
|
|
158
|
-
return resolveContext({ ...options, ctx: m });
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
TypedHTTP2.Router = Router;
|
|
163
|
-
function createClient(base, fetchCallback) {
|
|
164
|
-
function buildUrl(path, input, options) {
|
|
165
|
-
const method = path.at(-1);
|
|
166
|
-
const url = new URL(path.slice(0, path.length - 1).join("/"), base);
|
|
167
|
-
const signal = options?.signal;
|
|
168
|
-
let body = void 0;
|
|
169
|
-
if (method === "GET" && input)
|
|
170
|
-
url.searchParams.set("input", JSON.stringify(input));
|
|
171
|
-
else if (method !== "GET" && input)
|
|
172
|
-
body = JSON.stringify(input);
|
|
173
|
-
return {
|
|
174
|
-
url,
|
|
175
|
-
method,
|
|
176
|
-
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
177
|
-
body,
|
|
178
|
-
signal
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
function createProxy(path = []) {
|
|
182
|
-
return new Proxy(() => {
|
|
183
|
-
}, {
|
|
184
|
-
get(target, prop) {
|
|
185
|
-
if (typeof prop === "symbol")
|
|
186
|
-
return void 0;
|
|
187
|
-
if (prop === "prepare")
|
|
188
|
-
return (input, options) => buildUrl(path, input, options);
|
|
189
|
-
const newPath = [...path, prop];
|
|
190
|
-
return createProxy(newPath);
|
|
191
|
-
},
|
|
192
|
-
apply(target, thisArg, args) {
|
|
193
|
-
const options = buildUrl(path, args[0], args[1]);
|
|
194
|
-
return fetchCallback(options.url, {
|
|
195
|
-
method: options.method,
|
|
196
|
-
body: options.body,
|
|
197
|
-
headers: options.headers,
|
|
198
|
-
signal: options.signal
|
|
199
|
-
}).then(async (response) => {
|
|
200
|
-
if (response.status >= 200 && response.status < 300) {
|
|
201
|
-
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
202
|
-
const text = await response.text();
|
|
203
|
-
return text.length ? JSON.parse(text) : void 0;
|
|
204
|
-
}
|
|
205
|
-
return await response.blob();
|
|
206
|
-
}
|
|
207
|
-
if (response.status >= 400 && response.status < 600) {
|
|
208
|
-
const text = await response.text();
|
|
209
|
-
if (text)
|
|
210
|
-
throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
|
|
211
|
-
else
|
|
212
|
-
throw new Error(`HTTP request failed with status ${response.status}`);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return createProxy();
|
|
219
|
-
}
|
|
220
|
-
TypedHTTP2.createClient = createClient;
|
|
221
|
-
})(TypedHTTP || (TypedHTTP = {}));
|
|
1
|
+
// src/serverapi.ts
|
|
2
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
222
3
|
|
|
223
4
|
// src/utils.ts
|
|
5
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
224
6
|
import http from "http";
|
|
225
7
|
import https from "https";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
var gunzipSync = zlib.gunzipSync;
|
|
231
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
232
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
8
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
9
|
+
function errorText(error) {
|
|
10
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
11
|
+
}
|
|
233
12
|
async function retryWithBackoff(job, backoff = []) {
|
|
234
13
|
for (const timeout of backoff) {
|
|
235
14
|
try {
|
|
236
15
|
return await job();
|
|
237
16
|
} catch (e) {
|
|
238
17
|
if (e instanceof AggregateError)
|
|
239
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
18
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
240
19
|
else if (e instanceof Error)
|
|
241
|
-
console.error(`[flakiness.io err]`, e
|
|
20
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
242
21
|
else
|
|
243
22
|
console.error(`[flakiness.io err]`, e);
|
|
244
23
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -256,6 +35,7 @@ var httpUtils;
|
|
|
256
35
|
reject = b;
|
|
257
36
|
});
|
|
258
37
|
const protocol = url.startsWith("https") ? https : http;
|
|
38
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
259
39
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
260
40
|
const chunks = [];
|
|
261
41
|
res.on("data", (chunk) => chunks.push(chunk));
|