@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.
@@ -1,256 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cmd-upload.ts
4
- import { FlakinessReport } from "@flakiness/report";
5
- import fs2 from "fs/promises";
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 fs from "fs";
10
- import { URL as URL2 } from "url";
11
- import { brotliCompressSync as brotliCompressSync2 } from "zlib";
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
- // ../server/lib/common/typedHttp.js
14
- var TypedHTTP;
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 util from "util";
239
- import zlib from "zlib";
240
- var gzipAsync = util.promisify(zlib.gzip);
241
- var gunzipAsync = util.promisify(zlib.gunzip);
242
- var gunzipSync = zlib.gunzipSync;
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].message);
32
+ console.error(`[flakiness.io err]`, errorText(e.errors[0]));
252
33
  else if (e instanceof Error)
253
- console.error(`[flakiness.io err]`, e.message);
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 URL2(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
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 ? brotliCompressSync2(data) : await brotliCompressAsync(data);
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 bytesLength = attachment.path ? (await fs.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
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": bytesLength + ""
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
- if (attachment.path) {
441
- fs.createReadStream(attachment.path).pipe(request);
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 = path.resolve(relativePath);
455
- if (!await fs2.access(fullPath, fs2.constants.F_OK).then(() => true).catch(() => false)) {
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 attachmentsDir = options.attachmentsDir ?? path.dirname(fullPath);
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 attachments = [];
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, attachments);
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
  };