@flakiness/sdk 0.147.0 → 0.149.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/README.md +95 -2
- package/lib/{httpUtils.js → _internalUtils.js} +70 -13
- package/lib/_stable-hash.d.js +1 -0
- package/lib/browser.js +154 -0
- package/lib/ciUtils.js +42 -0
- package/lib/createEnvironment.js +92 -1
- package/lib/createTestStepSnippets.js +17 -122
- package/lib/flakinessProjectConfig.js +376 -27
- package/lib/gitWorktree.js +312 -0
- package/lib/index.js +839 -601
- package/lib/normalizeReport.js +114 -0
- package/lib/reportUtils.js +382 -111
- package/lib/reportUtilsBrowser.js +138 -0
- package/lib/showReport.js +522 -368
- package/lib/staticServer.js +149 -0
- package/lib/stripAnsi.js +9 -0
- package/lib/systemUtilizationSampler.js +21 -0
- package/lib/{reportUploader.js → uploadReport.js} +67 -63
- package/lib/visitTests.js +19 -0
- package/lib/writeReport.js +31 -0
- package/package.json +6 -17
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/browser/index.js +0 -127
- package/lib/git.js +0 -55
- package/lib/localGit.js +0 -48
- package/lib/localReportApi.js +0 -275
- package/lib/localReportServer.js +0 -351
- package/lib/pathutils.js +0 -20
- package/lib/utils.js +0 -44
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/staticServer.ts
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as http from "http";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
var log = debug("fk:static_server");
|
|
7
|
+
var StaticServer = class {
|
|
8
|
+
_server;
|
|
9
|
+
_absoluteFolderPath;
|
|
10
|
+
_pathPrefix;
|
|
11
|
+
_cors;
|
|
12
|
+
_mimeTypes = {
|
|
13
|
+
".html": "text/html",
|
|
14
|
+
".js": "text/javascript",
|
|
15
|
+
".css": "text/css",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".gif": "image/gif",
|
|
20
|
+
".svg": "image/svg+xml",
|
|
21
|
+
".ico": "image/x-icon",
|
|
22
|
+
".txt": "text/plain"
|
|
23
|
+
};
|
|
24
|
+
constructor(pathPrefix, folderPath, cors) {
|
|
25
|
+
this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
|
|
26
|
+
this._absoluteFolderPath = path.resolve(folderPath);
|
|
27
|
+
this._cors = cors;
|
|
28
|
+
this._server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
29
|
+
}
|
|
30
|
+
port() {
|
|
31
|
+
const address = this._server.address();
|
|
32
|
+
if (!address)
|
|
33
|
+
return void 0;
|
|
34
|
+
return address.port;
|
|
35
|
+
}
|
|
36
|
+
address() {
|
|
37
|
+
const address = this._server.address();
|
|
38
|
+
if (!address)
|
|
39
|
+
return void 0;
|
|
40
|
+
const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
41
|
+
return `http://${displayHost}:${address.port}${this._pathPrefix}`;
|
|
42
|
+
}
|
|
43
|
+
async _startServer(port, host) {
|
|
44
|
+
let okListener;
|
|
45
|
+
let errListener;
|
|
46
|
+
const result = new Promise((resolve2, reject) => {
|
|
47
|
+
okListener = resolve2;
|
|
48
|
+
errListener = reject;
|
|
49
|
+
}).finally(() => {
|
|
50
|
+
this._server.removeListener("listening", okListener);
|
|
51
|
+
this._server.removeListener("error", errListener);
|
|
52
|
+
});
|
|
53
|
+
this._server.once("listening", okListener);
|
|
54
|
+
this._server.once("error", errListener);
|
|
55
|
+
this._server.listen(port, host);
|
|
56
|
+
await result;
|
|
57
|
+
log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
|
|
58
|
+
}
|
|
59
|
+
async start(port, host = "127.0.0.1") {
|
|
60
|
+
if (port === 0) {
|
|
61
|
+
await this._startServer(port, host);
|
|
62
|
+
return this.address();
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < 20; ++i) {
|
|
65
|
+
const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
|
|
66
|
+
if (!err)
|
|
67
|
+
return this.address();
|
|
68
|
+
if (err.code !== "EADDRINUSE")
|
|
69
|
+
throw err;
|
|
70
|
+
log("Port %d is busy (EADDRINUSE). Trying next port...", port);
|
|
71
|
+
port = port + 1;
|
|
72
|
+
if (port > 65535)
|
|
73
|
+
port = 4e3;
|
|
74
|
+
}
|
|
75
|
+
log("All sequential ports busy. Falling back to random port.");
|
|
76
|
+
await this._startServer(0, host);
|
|
77
|
+
return this.address();
|
|
78
|
+
}
|
|
79
|
+
stop() {
|
|
80
|
+
return new Promise((resolve2, reject) => {
|
|
81
|
+
this._server.close((err) => {
|
|
82
|
+
if (err) {
|
|
83
|
+
log("Error stopping server: %o", err);
|
|
84
|
+
reject(err);
|
|
85
|
+
} else {
|
|
86
|
+
log("Server stopped.");
|
|
87
|
+
resolve2();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
_errorResponse(req, res, code, text) {
|
|
93
|
+
res.writeHead(code, { "Content-Type": "text/plain" });
|
|
94
|
+
res.end(text);
|
|
95
|
+
log(`[${code}] ${req.method} ${req.url}`);
|
|
96
|
+
}
|
|
97
|
+
_handleRequest(req, res) {
|
|
98
|
+
const { url, method } = req;
|
|
99
|
+
if (this._cors) {
|
|
100
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
101
|
+
res.setHeader("Access-Control-Allow-Origin", this._cors);
|
|
102
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
103
|
+
if (req.method === "OPTIONS") {
|
|
104
|
+
res.writeHead(204);
|
|
105
|
+
res.end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (method !== "GET") {
|
|
110
|
+
this._errorResponse(req, res, 405, "Method Not Allowed");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
|
|
114
|
+
res.on("close", () => {
|
|
115
|
+
if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
|
|
116
|
+
});
|
|
117
|
+
if (!url || !url.startsWith(this._pathPrefix)) {
|
|
118
|
+
this._errorResponse(req, res, 404, "Not Found");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const relativePath = url.slice(this._pathPrefix.length);
|
|
122
|
+
const safeSuffix = path.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
|
|
123
|
+
const filePath = path.join(this._absoluteFolderPath, safeSuffix);
|
|
124
|
+
if (!filePath.startsWith(this._absoluteFolderPath)) {
|
|
125
|
+
this._errorResponse(req, res, 403, "Forbidden");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
fs.stat(filePath, (err, stats) => {
|
|
129
|
+
if (err || !stats.isFile()) {
|
|
130
|
+
this._errorResponse(req, res, 404, "File Not Found");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
134
|
+
const contentType = this._mimeTypes[ext] || "application/octet-stream";
|
|
135
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
136
|
+
log(`[200] ${req.method} ${req.url} -> ${filePath}`);
|
|
137
|
+
const readStream = fs.createReadStream(filePath);
|
|
138
|
+
readStream.pipe(res);
|
|
139
|
+
readStream.on("error", (err2) => {
|
|
140
|
+
log("Stream error: %o", err2);
|
|
141
|
+
res.end();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
export {
|
|
147
|
+
StaticServer
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=staticServer.js.map
|
package/lib/stripAnsi.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/stripAnsi.ts
|
|
2
|
+
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");
|
|
3
|
+
function stripAnsi(str) {
|
|
4
|
+
return str.replace(ansiRegex, "");
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
stripAnsi
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=stripAnsi.js.map
|
|
@@ -44,9 +44,24 @@ function toFKUtilization(sample, previous) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
var SystemUtilizationSampler = class {
|
|
47
|
+
/**
|
|
48
|
+
* The accumulated system utilization data.
|
|
49
|
+
*
|
|
50
|
+
* This object is populated as samples are collected and can be directly included in
|
|
51
|
+
* Flakiness reports. It contains:
|
|
52
|
+
* - `samples` - Array of utilization samples with CPU/memory percentages and durations
|
|
53
|
+
* - `startTimestamp` - Timestamp when sampling began
|
|
54
|
+
* - `totalMemoryBytes` - Total system memory in bytes
|
|
55
|
+
*/
|
|
47
56
|
result;
|
|
48
57
|
_lastSample = getSystemUtilization();
|
|
49
58
|
_timer;
|
|
59
|
+
/**
|
|
60
|
+
* Creates a new SystemUtilizationSampler and starts sampling immediately.
|
|
61
|
+
*
|
|
62
|
+
* The first sample is collected after 50ms, and subsequent samples are collected
|
|
63
|
+
* every 1000ms. Call `dispose()` to stop sampling and clean up resources.
|
|
64
|
+
*/
|
|
50
65
|
constructor() {
|
|
51
66
|
this.result = {
|
|
52
67
|
samples: [],
|
|
@@ -61,6 +76,12 @@ var SystemUtilizationSampler = class {
|
|
|
61
76
|
this._lastSample = sample;
|
|
62
77
|
this._timer = setTimeout(this._addSample.bind(this), 1e3);
|
|
63
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Stops sampling and cleans up resources.
|
|
81
|
+
*
|
|
82
|
+
* Call this method when you're done collecting utilization data to stop the sampling
|
|
83
|
+
* timer and prevent memory leaks. The `result` object remains accessible after disposal.
|
|
84
|
+
*/
|
|
64
85
|
dispose() {
|
|
65
86
|
clearTimeout(this._timer);
|
|
66
87
|
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
1
|
+
// src/uploadReport.ts
|
|
3
2
|
import assert from "assert";
|
|
4
|
-
import
|
|
5
|
-
import fs from "fs";
|
|
3
|
+
import fs2 from "fs";
|
|
6
4
|
import { URL } from "url";
|
|
7
5
|
|
|
8
|
-
// src/
|
|
6
|
+
// src/_internalUtils.ts
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import fs from "fs";
|
|
9
9
|
import http from "http";
|
|
10
10
|
import https from "https";
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import util from "util";
|
|
12
|
+
import zlib from "zlib";
|
|
13
|
+
var asyncBrotliCompress = util.promisify(zlib.brotliCompress);
|
|
14
|
+
async function compressTextAsync(text) {
|
|
15
|
+
return asyncBrotliCompress(text, {
|
|
16
|
+
chunkSize: 32 * 1024,
|
|
17
|
+
params: {
|
|
18
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 6,
|
|
19
|
+
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
13
23
|
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
14
24
|
function errorText(error) {
|
|
15
25
|
return FLAKINESS_DBG ? error.stack : error.message;
|
|
@@ -30,10 +40,6 @@ async function retryWithBackoff(job, backoff = []) {
|
|
|
30
40
|
}
|
|
31
41
|
return await job();
|
|
32
42
|
}
|
|
33
|
-
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");
|
|
34
|
-
|
|
35
|
-
// src/httpUtils.ts
|
|
36
|
-
var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
|
|
37
43
|
var httpUtils;
|
|
38
44
|
((httpUtils2) => {
|
|
39
45
|
function createRequest({ url, method = "get", headers = {} }) {
|
|
@@ -96,8 +102,11 @@ var httpUtils;
|
|
|
96
102
|
}
|
|
97
103
|
httpUtils2.postJSON = postJSON;
|
|
98
104
|
})(httpUtils || (httpUtils = {}));
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
function sha1Text(data) {
|
|
106
|
+
const hash = crypto.createHash("sha1");
|
|
107
|
+
hash.update(data);
|
|
108
|
+
return hash.digest("hex");
|
|
109
|
+
}
|
|
101
110
|
function sha1File(filePath) {
|
|
102
111
|
return new Promise((resolve, reject) => {
|
|
103
112
|
const hash = crypto.createHash("sha1");
|
|
@@ -113,64 +122,60 @@ function sha1File(filePath) {
|
|
|
113
122
|
});
|
|
114
123
|
});
|
|
115
124
|
}
|
|
125
|
+
|
|
126
|
+
// src/uploadReport.ts
|
|
116
127
|
async function createFileAttachment(contentType, filePath) {
|
|
117
128
|
return {
|
|
129
|
+
type: "file",
|
|
118
130
|
contentType,
|
|
119
131
|
id: await sha1File(filePath),
|
|
120
132
|
path: filePath
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
async function createDataAttachment(contentType, data) {
|
|
124
|
-
const hash = crypto.createHash("sha1");
|
|
125
|
-
hash.update(data);
|
|
126
|
-
const id = hash.digest("hex");
|
|
127
136
|
return {
|
|
137
|
+
type: "buffer",
|
|
128
138
|
contentType,
|
|
129
|
-
id,
|
|
139
|
+
id: sha1Text(data),
|
|
130
140
|
body: data
|
|
131
141
|
};
|
|
132
142
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
143
|
+
async function uploadReport(report, attachments, options) {
|
|
144
|
+
const flakinessAccessToken = options?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
145
|
+
const flakinessEndpoint = options?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
|
|
146
|
+
const logger = options?.logger ?? console;
|
|
147
|
+
if (!flakinessAccessToken) {
|
|
148
|
+
const reason = "No FLAKINESS_ACCESS_TOKEN found";
|
|
149
|
+
if (process.env.CI)
|
|
150
|
+
logger.warn(`[flakiness.io] \u26A0 Skipping upload: ${reason}`);
|
|
151
|
+
return { status: "skipped", reason };
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (!uploaderOptions) {
|
|
144
|
-
if (process.env.CI)
|
|
145
|
-
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
146
|
-
return void 0;
|
|
147
|
-
}
|
|
148
|
-
const uploader = new _ReportUploader(uploaderOptions);
|
|
149
|
-
const upload = uploader.createUpload(options.report, options.attachments);
|
|
153
|
+
try {
|
|
154
|
+
const upload = new ReportUpload(report, attachments, { flakinessAccessToken, flakinessEndpoint });
|
|
150
155
|
const uploadResult = await upload.upload();
|
|
151
156
|
if (!uploadResult.success) {
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
const errorMessage = uploadResult.message || "Unknown upload error";
|
|
158
|
+
logger.error(`[flakiness.io] \u2715 Failed to upload: ${errorMessage}`);
|
|
159
|
+
if (options?.throwOnFailure)
|
|
160
|
+
throw new Error(`Flakiness upload failed: ${errorMessage}`);
|
|
161
|
+
return { status: "failed", error: errorMessage };
|
|
154
162
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
logger.log(`[flakiness.io] \u2713 Uploaded to ${uploadResult.reportUrl}`);
|
|
164
|
+
return { status: "success", reportUrl: uploadResult.reportUrl };
|
|
165
|
+
} catch (e) {
|
|
166
|
+
const errorMessage = e.message || String(e);
|
|
167
|
+
logger.error(`[flakiness.io] \u2715 Unexpected error during upload: ${errorMessage}`);
|
|
168
|
+
if (options?.throwOnFailure)
|
|
169
|
+
throw e;
|
|
170
|
+
return { status: "failed", error: errorMessage };
|
|
158
171
|
}
|
|
159
|
-
|
|
160
|
-
constructor(options) {
|
|
161
|
-
this._options = options;
|
|
162
|
-
}
|
|
163
|
-
createUpload(report, attachments) {
|
|
164
|
-
const upload = new ReportUpload(this._options, report, attachments);
|
|
165
|
-
return upload;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
172
|
+
}
|
|
168
173
|
var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
|
|
169
174
|
var ReportUpload = class {
|
|
170
175
|
_report;
|
|
171
176
|
_attachments;
|
|
172
177
|
_options;
|
|
173
|
-
constructor(
|
|
178
|
+
constructor(report, attachments, options) {
|
|
174
179
|
this._options = options;
|
|
175
180
|
this._report = report;
|
|
176
181
|
this._attachments = attachments;
|
|
@@ -196,7 +201,7 @@ var ReportUpload = class {
|
|
|
196
201
|
error
|
|
197
202
|
}));
|
|
198
203
|
}
|
|
199
|
-
async upload(
|
|
204
|
+
async upload() {
|
|
200
205
|
const response = await this._api("/api/upload/start", this._options.flakinessAccessToken);
|
|
201
206
|
if (response?.error || !response.result)
|
|
202
207
|
return { success: false, message: response.error };
|
|
@@ -208,19 +213,19 @@ var ReportUpload = class {
|
|
|
208
213
|
return { success: false, message: attachmentsPresignedUrls.error };
|
|
209
214
|
const attachments = new Map(attachmentsPresignedUrls.result.map((a) => [a.attachmentId, a.presignedUrl]));
|
|
210
215
|
await Promise.all([
|
|
211
|
-
this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl
|
|
216
|
+
this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl),
|
|
212
217
|
...this._attachments.map((attachment) => {
|
|
213
218
|
const uploadURL = attachments.get(attachment.id);
|
|
214
219
|
if (!uploadURL)
|
|
215
220
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
216
|
-
return this._uploadAttachment(attachment, uploadURL
|
|
221
|
+
return this._uploadAttachment(attachment, uploadURL);
|
|
217
222
|
})
|
|
218
223
|
]);
|
|
219
224
|
await this._api("/api/upload/finish", response.result.uploadToken);
|
|
220
225
|
return { success: true, reportUrl: webUrl };
|
|
221
226
|
}
|
|
222
|
-
async _uploadReport(data, uploadUrl
|
|
223
|
-
const compressed =
|
|
227
|
+
async _uploadReport(data, uploadUrl) {
|
|
228
|
+
const compressed = await compressTextAsync(data);
|
|
224
229
|
const headers = {
|
|
225
230
|
"Content-Type": "application/json",
|
|
226
231
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -237,30 +242,29 @@ var ReportUpload = class {
|
|
|
237
242
|
await responseDataPromise;
|
|
238
243
|
}, HTTP_BACKOFF);
|
|
239
244
|
}
|
|
240
|
-
async _uploadAttachment(attachment, uploadUrl
|
|
245
|
+
async _uploadAttachment(attachment, uploadUrl) {
|
|
241
246
|
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
242
247
|
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
243
|
-
if (!compressable && attachment.
|
|
244
|
-
const attachmentPath = attachment.path;
|
|
248
|
+
if (!compressable && attachment.type === "file") {
|
|
245
249
|
await retryWithBackoff(async () => {
|
|
246
250
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
247
251
|
url: uploadUrl,
|
|
248
252
|
headers: {
|
|
249
253
|
"Content-Type": attachment.contentType,
|
|
250
|
-
"Content-Length": (await
|
|
254
|
+
"Content-Length": (await fs2.promises.stat(attachment.path)).size + ""
|
|
251
255
|
},
|
|
252
256
|
method: "put"
|
|
253
257
|
});
|
|
254
|
-
|
|
258
|
+
fs2.createReadStream(attachment.path).pipe(request);
|
|
255
259
|
await responseDataPromise;
|
|
256
260
|
}, HTTP_BACKOFF);
|
|
257
261
|
return;
|
|
258
262
|
}
|
|
259
|
-
let buffer = attachment.
|
|
263
|
+
let buffer = attachment.type === "buffer" ? attachment.body : await fs2.promises.readFile(attachment.path);
|
|
260
264
|
assert(buffer);
|
|
261
265
|
const encoding = compressable ? "br" : void 0;
|
|
262
266
|
if (compressable)
|
|
263
|
-
buffer =
|
|
267
|
+
buffer = await compressTextAsync(buffer);
|
|
264
268
|
const headers = {
|
|
265
269
|
"Content-Type": attachment.contentType,
|
|
266
270
|
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
@@ -279,8 +283,8 @@ var ReportUpload = class {
|
|
|
279
283
|
}
|
|
280
284
|
};
|
|
281
285
|
export {
|
|
282
|
-
ReportUploader,
|
|
283
286
|
createDataAttachment,
|
|
284
|
-
createFileAttachment
|
|
287
|
+
createFileAttachment,
|
|
288
|
+
uploadReport
|
|
285
289
|
};
|
|
286
|
-
//# sourceMappingURL=
|
|
290
|
+
//# sourceMappingURL=uploadReport.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/visitTests.ts
|
|
2
|
+
function visitTests(report, testVisitor) {
|
|
3
|
+
function visitSuite(suite, parents) {
|
|
4
|
+
parents.push(suite);
|
|
5
|
+
for (const test of suite.tests ?? [])
|
|
6
|
+
testVisitor(test, parents);
|
|
7
|
+
for (const childSuite of suite.suites ?? [])
|
|
8
|
+
visitSuite(childSuite, parents);
|
|
9
|
+
parents.pop();
|
|
10
|
+
}
|
|
11
|
+
for (const test of report.tests ?? [])
|
|
12
|
+
testVisitor(test, []);
|
|
13
|
+
for (const suite of report.suites)
|
|
14
|
+
visitSuite(suite, []);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
visitTests
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=visitTests.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/writeReport.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
async function writeReport(report, attachments, outputFolder) {
|
|
5
|
+
const reportPath = path.join(outputFolder, "report.json");
|
|
6
|
+
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
7
|
+
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
8
|
+
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
9
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
10
|
+
if (attachments.length)
|
|
11
|
+
await fs.promises.mkdir(attachmentsFolder);
|
|
12
|
+
const movedAttachments = [];
|
|
13
|
+
for (const attachment of attachments) {
|
|
14
|
+
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
15
|
+
if (attachment.type === "file")
|
|
16
|
+
await fs.promises.cp(attachment.path, attachmentPath);
|
|
17
|
+
else if (attachment.type === "buffer")
|
|
18
|
+
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
19
|
+
movedAttachments.push({
|
|
20
|
+
type: "file",
|
|
21
|
+
contentType: attachment.contentType,
|
|
22
|
+
id: attachment.id,
|
|
23
|
+
path: attachmentPath
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return movedAttachments;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
writeReport
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=writeReport.js.map
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flakiness/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.149.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"exports": {
|
|
6
|
-
"./localReportApi": {
|
|
7
|
-
"types": "./types/src/localReportApi.d.ts"
|
|
8
|
-
},
|
|
9
6
|
".": {
|
|
10
7
|
"types": "./types/src/index.d.ts",
|
|
11
8
|
"import": "./lib/index.js",
|
|
12
9
|
"require": "./lib/index.js"
|
|
13
10
|
},
|
|
14
11
|
"./browser": {
|
|
15
|
-
"types": "./types/src/browser
|
|
16
|
-
"import": "./lib/browser
|
|
17
|
-
"require": "./lib/browser
|
|
12
|
+
"types": "./types/src/browser.d.ts",
|
|
13
|
+
"import": "./lib/browser.js",
|
|
14
|
+
"require": "./lib/browser.js"
|
|
18
15
|
}
|
|
19
16
|
},
|
|
20
17
|
"type": "module",
|
|
@@ -25,22 +22,14 @@
|
|
|
25
22
|
"author": "Degu Labs, Inc",
|
|
26
23
|
"license": "Fair Source 100",
|
|
27
24
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@types/babel__code-frame": "^7.0.6",
|
|
30
|
-
"@types/compression": "^1.8.1",
|
|
31
|
-
"@types/express": "^4.17.20"
|
|
25
|
+
"@types/babel__code-frame": "^7.0.6"
|
|
32
26
|
},
|
|
33
27
|
"dependencies": {
|
|
34
28
|
"@babel/code-frame": "^7.26.2",
|
|
35
29
|
"@flakiness/flakiness-report": "^0.16.0",
|
|
36
|
-
"@flakiness/shared": "0.147.0",
|
|
37
|
-
"body-parser": "^1.20.3",
|
|
38
30
|
"chalk": "^5.6.2",
|
|
39
|
-
"compression": "^1.8.1",
|
|
40
31
|
"debug": "^4.3.7",
|
|
41
|
-
"express": "^4.21.2",
|
|
42
|
-
"express-async-errors": "^3.1.1",
|
|
43
32
|
"open": "^10.2.0",
|
|
44
|
-
"
|
|
33
|
+
"stable-hash": "^0.0.6"
|
|
45
34
|
}
|
|
46
35
|
}
|