@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.
@@ -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
 
@@ -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
- options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
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 URL2(response2?.result.report_url, this._options.flakinessEndpoint).toString() : void 0;
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 ? brotliCompressSync2(data) : await brotliCompressAsync(data);
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 bytesLength = attachment.path ? (await fs.promises.stat(attachment.path)).size : attachment.body ? Buffer.byteLength(attachment.body) : 0;
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": bytesLength + ""
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
- 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
- }
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 = path.resolve(relativePath);
455
- if (!await fs2.access(fullPath, fs2.constants.F_OK).then(() => true).catch(() => false)) {
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 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");
293
+ const text = await fs3.readFile(fullPath, "utf-8");
463
294
  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
- });
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, attachments);
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
  };