@flakiness/sdk 0.148.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 +368 -25
- package/lib/gitWorktree.js +312 -0
- package/lib/index.js +827 -514
- package/lib/normalizeReport.js +114 -0
- package/lib/reportUtils.js +382 -111
- package/lib/reportUtilsBrowser.js +138 -0
- package/lib/showReport.js +401 -47
- 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 +5 -7
- 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/pathutils.js +0 -20
- package/lib/utils.js +0 -44
package/lib/showReport.js
CHANGED
|
@@ -1,32 +1,97 @@
|
|
|
1
1
|
// src/showReport.ts
|
|
2
|
-
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import open from "open";
|
|
5
4
|
|
|
6
|
-
// src/
|
|
7
|
-
import fs from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
|
|
10
|
-
// src/git.ts
|
|
11
|
-
import assert from "assert";
|
|
12
|
-
|
|
13
|
-
// src/pathutils.ts
|
|
14
|
-
import { posix as posixPath, win32 as win32Path } from "path";
|
|
15
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
16
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
17
|
-
function normalizePath(aPath) {
|
|
18
|
-
if (IS_WIN32_PATH.test(aPath)) {
|
|
19
|
-
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
20
|
-
}
|
|
21
|
-
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
22
|
-
return "/" + aPath[0] + aPath.substring(2);
|
|
23
|
-
return aPath;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// src/utils.ts
|
|
5
|
+
// src/_internalUtils.ts
|
|
27
6
|
import { spawnSync } from "child_process";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import http from "http";
|
|
9
|
+
import https from "https";
|
|
10
|
+
import util from "util";
|
|
11
|
+
import zlib from "zlib";
|
|
12
|
+
var asyncBrotliCompress = util.promisify(zlib.brotliCompress);
|
|
28
13
|
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
29
|
-
|
|
14
|
+
function errorText(error) {
|
|
15
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
16
|
+
}
|
|
17
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
18
|
+
for (const timeout of backoff) {
|
|
19
|
+
try {
|
|
20
|
+
return await job();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (e instanceof AggregateError)
|
|
23
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
24
|
+
else if (e instanceof Error)
|
|
25
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
26
|
+
else
|
|
27
|
+
console.error(`[flakiness.io err]`, e);
|
|
28
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return await job();
|
|
32
|
+
}
|
|
33
|
+
var httpUtils;
|
|
34
|
+
((httpUtils2) => {
|
|
35
|
+
function createRequest({ url, method = "get", headers = {} }) {
|
|
36
|
+
let resolve2;
|
|
37
|
+
let reject;
|
|
38
|
+
const responseDataPromise = new Promise((a, b) => {
|
|
39
|
+
resolve2 = a;
|
|
40
|
+
reject = b;
|
|
41
|
+
});
|
|
42
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
43
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
44
|
+
const request = protocol.request(url, { method, headers }, (res) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
47
|
+
res.on("end", () => {
|
|
48
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
49
|
+
resolve2(Buffer.concat(chunks));
|
|
50
|
+
else
|
|
51
|
+
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
52
|
+
});
|
|
53
|
+
res.on("error", (error) => reject(error));
|
|
54
|
+
});
|
|
55
|
+
request.on("error", reject);
|
|
56
|
+
return { request, responseDataPromise };
|
|
57
|
+
}
|
|
58
|
+
httpUtils2.createRequest = createRequest;
|
|
59
|
+
async function getBuffer(url, backoff) {
|
|
60
|
+
return await retryWithBackoff(async () => {
|
|
61
|
+
const { request, responseDataPromise } = createRequest({ url });
|
|
62
|
+
request.end();
|
|
63
|
+
return await responseDataPromise;
|
|
64
|
+
}, backoff);
|
|
65
|
+
}
|
|
66
|
+
httpUtils2.getBuffer = getBuffer;
|
|
67
|
+
async function getText(url, backoff) {
|
|
68
|
+
const buffer = await getBuffer(url, backoff);
|
|
69
|
+
return buffer.toString("utf-8");
|
|
70
|
+
}
|
|
71
|
+
httpUtils2.getText = getText;
|
|
72
|
+
async function getJSON(url) {
|
|
73
|
+
return JSON.parse(await getText(url));
|
|
74
|
+
}
|
|
75
|
+
httpUtils2.getJSON = getJSON;
|
|
76
|
+
async function postText(url, text, backoff) {
|
|
77
|
+
const headers = {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"Content-Length": Buffer.byteLength(text) + ""
|
|
80
|
+
};
|
|
81
|
+
return await retryWithBackoff(async () => {
|
|
82
|
+
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
83
|
+
request.write(text);
|
|
84
|
+
request.end();
|
|
85
|
+
return await responseDataPromise;
|
|
86
|
+
}, backoff);
|
|
87
|
+
}
|
|
88
|
+
httpUtils2.postText = postText;
|
|
89
|
+
async function postJSON(url, json, backoff) {
|
|
90
|
+
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
91
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
92
|
+
}
|
|
93
|
+
httpUtils2.postJSON = postJSON;
|
|
94
|
+
})(httpUtils || (httpUtils = {}));
|
|
30
95
|
function shell(command, args, options) {
|
|
31
96
|
try {
|
|
32
97
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -39,15 +104,227 @@ function shell(command, args, options) {
|
|
|
39
104
|
return void 0;
|
|
40
105
|
}
|
|
41
106
|
}
|
|
107
|
+
function randomUUIDBase62() {
|
|
108
|
+
const BASE62_CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
109
|
+
let num = BigInt("0x" + crypto.randomUUID().replace(/-/g, ""));
|
|
110
|
+
if (num === 0n)
|
|
111
|
+
return BASE62_CHARSET[0];
|
|
112
|
+
const chars = [];
|
|
113
|
+
while (num > 0n) {
|
|
114
|
+
const remainder = Number(num % 62n);
|
|
115
|
+
num /= 62n;
|
|
116
|
+
chars.push(BASE62_CHARSET[remainder]);
|
|
117
|
+
}
|
|
118
|
+
return chars.reverse().join("");
|
|
119
|
+
}
|
|
42
120
|
|
|
43
|
-
// src/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
// src/flakinessProjectConfig.ts
|
|
122
|
+
import fs from "fs";
|
|
123
|
+
import path from "path";
|
|
124
|
+
|
|
125
|
+
// src/gitWorktree.ts
|
|
126
|
+
import assert from "assert";
|
|
127
|
+
import { exec } from "child_process";
|
|
128
|
+
import debug from "debug";
|
|
129
|
+
import { posix as posixPath, win32 as win32Path } from "path";
|
|
130
|
+
import { promisify } from "util";
|
|
131
|
+
var log = debug("fk:git");
|
|
132
|
+
var execAsync = promisify(exec);
|
|
133
|
+
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
134
|
+
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
135
|
+
function toPosixAbsolutePath(absolutePath) {
|
|
136
|
+
if (IS_WIN32_PATH.test(absolutePath)) {
|
|
137
|
+
absolutePath = absolutePath.split(win32Path.sep).join(posixPath.sep);
|
|
138
|
+
}
|
|
139
|
+
if (IS_ALMOST_POSIX_PATH.test(absolutePath))
|
|
140
|
+
return "/" + absolutePath[0] + absolutePath.substring(2);
|
|
141
|
+
return absolutePath;
|
|
142
|
+
}
|
|
143
|
+
function toNativeAbsolutePath(posix) {
|
|
144
|
+
if (process.platform !== "win32")
|
|
145
|
+
return posix;
|
|
146
|
+
assert(posix.startsWith("/"), "The path must be absolute");
|
|
147
|
+
const m = posix.match(/^\/([a-zA-Z])(\/.*)?$/);
|
|
148
|
+
assert(m, `Invalid POSIX path: ${posix}`);
|
|
149
|
+
const drive = m[1];
|
|
150
|
+
const rest = (m[2] ?? "").split(posixPath.sep).join(win32Path.sep);
|
|
151
|
+
return drive.toUpperCase() + ":" + rest;
|
|
152
|
+
}
|
|
153
|
+
var GitWorktree = class _GitWorktree {
|
|
154
|
+
constructor(_gitRoot) {
|
|
155
|
+
this._gitRoot = _gitRoot;
|
|
156
|
+
this._posixGitRoot = toPosixAbsolutePath(this._gitRoot);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Creates a GitWorktree instance from any path inside a git repository.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} somePathInsideGitRepo - Any path (file or directory) within a git repository.
|
|
162
|
+
* Can be absolute or relative. The function will locate the git root directory.
|
|
163
|
+
*
|
|
164
|
+
* @returns {GitWorktree} A new GitWorktree instance bound to the discovered git root.
|
|
165
|
+
*
|
|
166
|
+
* @throws {Error} Throws if the path is not inside a git repository or if git commands fail.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const worktree = GitWorktree.create('./src/my-test.ts');
|
|
171
|
+
* const gitRoot = worktree.rootPath();
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
static create(somePathInsideGitRepo) {
|
|
175
|
+
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
176
|
+
cwd: somePathInsideGitRepo,
|
|
177
|
+
encoding: "utf-8"
|
|
178
|
+
});
|
|
179
|
+
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
180
|
+
return new _GitWorktree(root);
|
|
181
|
+
}
|
|
182
|
+
_posixGitRoot;
|
|
183
|
+
/**
|
|
184
|
+
* Returns the native absolute path of the git repository root directory.
|
|
185
|
+
*
|
|
186
|
+
* @returns {string} Native absolute path to the git root. Format matches the current platform
|
|
187
|
+
* (Windows or POSIX).
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const root = worktree.rootPath();
|
|
192
|
+
* // On Windows: 'D:\project'
|
|
193
|
+
* // On Unix: '/project'
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
rootPath() {
|
|
197
|
+
return this._gitRoot;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the commit ID (SHA-1 hash) of the current HEAD commit.
|
|
201
|
+
*
|
|
202
|
+
* @returns {FlakinessReport.CommitId} Full 40-character commit hash of the HEAD commit.
|
|
203
|
+
*
|
|
204
|
+
* @throws {Error} Throws if git command fails or repository is in an invalid state.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* const commitId = worktree.headCommitId();
|
|
209
|
+
* // Returns: 'a1b2c3d4e5f6...' (40-character SHA-1)
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
headCommitId() {
|
|
213
|
+
const sha = shell(`git`, ["rev-parse", "HEAD"], {
|
|
214
|
+
cwd: this._gitRoot,
|
|
215
|
+
encoding: "utf-8"
|
|
216
|
+
});
|
|
217
|
+
assert(sha, `FAILED: git rev-parse HEAD @ ${this._gitRoot}`);
|
|
218
|
+
return sha.trim();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Converts a native absolute path to a git-relative POSIX path.
|
|
222
|
+
*
|
|
223
|
+
* Takes any absolute path (Windows or POSIX format) and converts it to a POSIX path
|
|
224
|
+
* relative to the git repository root. This is essential for Flakiness reports where
|
|
225
|
+
* all file paths must be git-relative and use POSIX separators.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} absolutePath - Native absolute path to convert. Can be in Windows format
|
|
228
|
+
* (e.g., `D:\project\src\test.ts`) or POSIX format (e.g., `/project/src/test.ts`).
|
|
229
|
+
*
|
|
230
|
+
* @returns {FlakinessReport.GitFilePath} POSIX path relative to git root (e.g., `src/test.ts`).
|
|
231
|
+
* Returns an empty string if the path is the git root itself.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* const gitPath = worktree.gitPath('/Users/project/src/test.ts');
|
|
236
|
+
* // Returns: 'src/test.ts'
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
gitPath(absolutePath) {
|
|
240
|
+
return posixPath.relative(this._posixGitRoot, toPosixAbsolutePath(absolutePath));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Converts a git-relative POSIX path to a native absolute path.
|
|
244
|
+
*
|
|
245
|
+
* Takes a POSIX path relative to the git root and converts it to the native absolute path
|
|
246
|
+
* format for the current platform (Windows or POSIX). This is the inverse of `gitPath()`.
|
|
247
|
+
*
|
|
248
|
+
* @param {FlakinessReport.GitFilePath} relativePath - POSIX path relative to git root
|
|
249
|
+
* (e.g., `src/test.ts`).
|
|
250
|
+
*
|
|
251
|
+
* @returns {string} Native absolute path. On Windows, returns Windows format (e.g., `D:\project\src\test.ts`).
|
|
252
|
+
* On POSIX systems, returns POSIX format (e.g., `/project/src/test.ts`).
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const absolutePath = worktree.absolutePath('src/test.ts');
|
|
257
|
+
* // On Windows: 'D:\project\src\test.ts'
|
|
258
|
+
* // On Unix: '/project/src/test.ts'
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
absolutePath(relativePath) {
|
|
262
|
+
return toNativeAbsolutePath(posixPath.join(this._posixGitRoot, relativePath));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Lists recent commits from the repository.
|
|
266
|
+
*
|
|
267
|
+
* Retrieves commit information including commit ID, timestamp, author, message, and parent commits.
|
|
268
|
+
* Note: CI environments often have shallow checkouts with limited history, which may affect
|
|
269
|
+
* the number of commits returned.
|
|
270
|
+
*
|
|
271
|
+
* @param {number} count - Maximum number of commits to retrieve, starting from HEAD.
|
|
272
|
+
*
|
|
273
|
+
* @returns {Promise<GitCommit[]>} Promise that resolves to an array of commit objects, ordered
|
|
274
|
+
* from most recent to oldest. Each commit includes:
|
|
275
|
+
* - `commitId` - Full commit hash
|
|
276
|
+
* - `timestamp` - Commit timestamp in milliseconds since Unix epoch
|
|
277
|
+
* - `message` - Commit message (subject line)
|
|
278
|
+
* - `author` - Author name
|
|
279
|
+
* - `parents` - Array of parent commit IDs
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* const commits = await worktree.listCommits(10);
|
|
284
|
+
* console.log(`Latest commit: ${commits[0].message}`);
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
async listCommits(count) {
|
|
288
|
+
return await listCommits(this._gitRoot, "HEAD", count);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
async function listCommits(gitRoot, head, count) {
|
|
292
|
+
const FIELD_SEPARATOR = "|~|";
|
|
293
|
+
const RECORD_SEPARATOR = "\0";
|
|
294
|
+
const prettyFormat = [
|
|
295
|
+
"%H",
|
|
296
|
+
// Full commit hash
|
|
297
|
+
"%ct",
|
|
298
|
+
// Commit timestamp (Unix seconds)
|
|
299
|
+
"%an",
|
|
300
|
+
// Author name
|
|
301
|
+
"%s",
|
|
302
|
+
// Subject line
|
|
303
|
+
"%P"
|
|
304
|
+
// Parent hashes (space-separated)
|
|
305
|
+
].join(FIELD_SEPARATOR);
|
|
306
|
+
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
307
|
+
try {
|
|
308
|
+
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
309
|
+
if (!stdout) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
313
|
+
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
314
|
+
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
315
|
+
return {
|
|
316
|
+
commitId,
|
|
317
|
+
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
318
|
+
author,
|
|
319
|
+
message,
|
|
320
|
+
parents,
|
|
321
|
+
walkIndex: 0
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
log(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
51
328
|
}
|
|
52
329
|
|
|
53
330
|
// src/flakinessProjectConfig.ts
|
|
@@ -67,8 +344,8 @@ function computeConfigPath() {
|
|
|
67
344
|
return configPath;
|
|
68
345
|
}
|
|
69
346
|
try {
|
|
70
|
-
const
|
|
71
|
-
return createConfigPath(
|
|
347
|
+
const worktree = GitWorktree.create(process.cwd());
|
|
348
|
+
return createConfigPath(worktree.rootPath());
|
|
72
349
|
} catch (e) {
|
|
73
350
|
return createConfigPath(process.cwd());
|
|
74
351
|
}
|
|
@@ -78,33 +355,110 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
78
355
|
this._configPath = _configPath;
|
|
79
356
|
this._config = _config;
|
|
80
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Loads the Flakiness project configuration from disk.
|
|
360
|
+
*
|
|
361
|
+
* Searches for an existing `.flakiness/config.json` file starting from the current working
|
|
362
|
+
* directory and walking up the directory tree. If no config exists, it determines the
|
|
363
|
+
* appropriate location (git root or current directory) for future saves.
|
|
364
|
+
*
|
|
365
|
+
* @returns {Promise<FlakinessProjectConfig>} Promise that resolves to a FlakinessProjectConfig
|
|
366
|
+
* instance. If no config file exists, returns an instance with default/empty values.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const config = await FlakinessProjectConfig.load();
|
|
371
|
+
* const projectId = config.projectPublicId();
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
81
374
|
static async load() {
|
|
82
375
|
const configPath = ensureConfigPath();
|
|
83
376
|
const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
84
377
|
const json = data ? JSON.parse(data) : {};
|
|
85
378
|
return new _FlakinessProjectConfig(configPath, json);
|
|
86
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Creates a new empty Flakiness project configuration.
|
|
382
|
+
*
|
|
383
|
+
* Creates a configuration instance with no values set. Use this when you want to build
|
|
384
|
+
* a configuration from scratch. Call `save()` to persist it to disk.
|
|
385
|
+
*
|
|
386
|
+
* @returns {FlakinessProjectConfig} A new empty configuration instance.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```typescript
|
|
390
|
+
* const config = FlakinessProjectConfig.createEmpty();
|
|
391
|
+
* config.setProjectPublicId('my-project-id');
|
|
392
|
+
* await config.save();
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
87
395
|
static createEmpty() {
|
|
88
396
|
return new _FlakinessProjectConfig(ensureConfigPath(), {});
|
|
89
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Returns the absolute path to the configuration file.
|
|
400
|
+
*
|
|
401
|
+
* @returns {string} Absolute path to `.flakiness/config.json`.
|
|
402
|
+
*/
|
|
90
403
|
path() {
|
|
91
404
|
return this._configPath;
|
|
92
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Returns the project's public ID, if configured.
|
|
408
|
+
*
|
|
409
|
+
* The project public ID is used to associate reports with a specific Flakiness.io project.
|
|
410
|
+
*
|
|
411
|
+
* @returns {string | undefined} Project public ID, or `undefined` if not set.
|
|
412
|
+
*/
|
|
93
413
|
projectPublicId() {
|
|
94
414
|
return this._config.projectPublicId;
|
|
95
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Returns the report viewer URL, either custom or default.
|
|
418
|
+
*
|
|
419
|
+
* @returns {string} Custom report viewer URL if configured, otherwise the default
|
|
420
|
+
* `https://report.flakiness.io`.
|
|
421
|
+
*/
|
|
96
422
|
reportViewerUrl() {
|
|
97
423
|
return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
|
|
98
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Sets or clears the custom report viewer URL.
|
|
427
|
+
*
|
|
428
|
+
* @param {string | undefined} url - Custom report viewer URL to use, or `undefined` to
|
|
429
|
+
* clear and use the default URL.
|
|
430
|
+
*/
|
|
99
431
|
setCustomReportViewerUrl(url) {
|
|
100
432
|
if (url)
|
|
101
433
|
this._config.customReportViewerUrl = url;
|
|
102
434
|
else
|
|
103
435
|
delete this._config.customReportViewerUrl;
|
|
104
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Sets the project's public ID.
|
|
439
|
+
*
|
|
440
|
+
* @param {string | undefined} projectId - Project public ID to set, or `undefined` to clear.
|
|
441
|
+
*/
|
|
105
442
|
setProjectPublicId(projectId) {
|
|
106
443
|
this._config.projectPublicId = projectId;
|
|
107
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Saves the configuration to disk.
|
|
447
|
+
*
|
|
448
|
+
* Writes the current configuration values to `.flakiness/config.json`. Creates the
|
|
449
|
+
* `.flakiness` directory if it doesn't exist.
|
|
450
|
+
*
|
|
451
|
+
* @returns {Promise<void>} Promise that resolves when the file has been written.
|
|
452
|
+
*
|
|
453
|
+
* @throws {Error} Throws if unable to create directories or write the file.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* const config = await FlakinessProjectConfig.load();
|
|
458
|
+
* config.setProjectPublicId('my-project');
|
|
459
|
+
* await config.save();
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
108
462
|
async save() {
|
|
109
463
|
await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
|
|
110
464
|
await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
@@ -112,11 +466,11 @@ var FlakinessProjectConfig = class _FlakinessProjectConfig {
|
|
|
112
466
|
};
|
|
113
467
|
|
|
114
468
|
// src/staticServer.ts
|
|
115
|
-
import
|
|
469
|
+
import debug2 from "debug";
|
|
116
470
|
import * as fs2 from "fs";
|
|
117
|
-
import * as
|
|
471
|
+
import * as http2 from "http";
|
|
118
472
|
import * as path2 from "path";
|
|
119
|
-
var
|
|
473
|
+
var log2 = debug2("fk:static_server");
|
|
120
474
|
var StaticServer = class {
|
|
121
475
|
_server;
|
|
122
476
|
_absoluteFolderPath;
|
|
@@ -138,7 +492,7 @@ var StaticServer = class {
|
|
|
138
492
|
this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
|
|
139
493
|
this._absoluteFolderPath = path2.resolve(folderPath);
|
|
140
494
|
this._cors = cors;
|
|
141
|
-
this._server =
|
|
495
|
+
this._server = http2.createServer((req, res) => this._handleRequest(req, res));
|
|
142
496
|
}
|
|
143
497
|
port() {
|
|
144
498
|
const address = this._server.address();
|
|
@@ -167,7 +521,7 @@ var StaticServer = class {
|
|
|
167
521
|
this._server.once("error", errListener);
|
|
168
522
|
this._server.listen(port, host);
|
|
169
523
|
await result;
|
|
170
|
-
|
|
524
|
+
log2('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
|
|
171
525
|
}
|
|
172
526
|
async start(port, host = "127.0.0.1") {
|
|
173
527
|
if (port === 0) {
|
|
@@ -180,12 +534,12 @@ var StaticServer = class {
|
|
|
180
534
|
return this.address();
|
|
181
535
|
if (err.code !== "EADDRINUSE")
|
|
182
536
|
throw err;
|
|
183
|
-
|
|
537
|
+
log2("Port %d is busy (EADDRINUSE). Trying next port...", port);
|
|
184
538
|
port = port + 1;
|
|
185
539
|
if (port > 65535)
|
|
186
540
|
port = 4e3;
|
|
187
541
|
}
|
|
188
|
-
|
|
542
|
+
log2("All sequential ports busy. Falling back to random port.");
|
|
189
543
|
await this._startServer(0, host);
|
|
190
544
|
return this.address();
|
|
191
545
|
}
|
|
@@ -193,10 +547,10 @@ var StaticServer = class {
|
|
|
193
547
|
return new Promise((resolve2, reject) => {
|
|
194
548
|
this._server.close((err) => {
|
|
195
549
|
if (err) {
|
|
196
|
-
|
|
550
|
+
log2("Error stopping server: %o", err);
|
|
197
551
|
reject(err);
|
|
198
552
|
} else {
|
|
199
|
-
|
|
553
|
+
log2("Server stopped.");
|
|
200
554
|
resolve2();
|
|
201
555
|
}
|
|
202
556
|
});
|
|
@@ -205,7 +559,7 @@ var StaticServer = class {
|
|
|
205
559
|
_errorResponse(req, res, code, text) {
|
|
206
560
|
res.writeHead(code, { "Content-Type": "text/plain" });
|
|
207
561
|
res.end(text);
|
|
208
|
-
|
|
562
|
+
log2(`[${code}] ${req.method} ${req.url}`);
|
|
209
563
|
}
|
|
210
564
|
_handleRequest(req, res) {
|
|
211
565
|
const { url, method } = req;
|
|
@@ -223,9 +577,9 @@ var StaticServer = class {
|
|
|
223
577
|
this._errorResponse(req, res, 405, "Method Not Allowed");
|
|
224
578
|
return;
|
|
225
579
|
}
|
|
226
|
-
req.on("aborted", () =>
|
|
580
|
+
req.on("aborted", () => log2(`ABORTED ${req.method} ${req.url}`));
|
|
227
581
|
res.on("close", () => {
|
|
228
|
-
if (!res.headersSent)
|
|
582
|
+
if (!res.headersSent) log2(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
|
|
229
583
|
});
|
|
230
584
|
if (!url || !url.startsWith(this._pathPrefix)) {
|
|
231
585
|
this._errorResponse(req, res, 404, "Not Found");
|
|
@@ -246,11 +600,11 @@ var StaticServer = class {
|
|
|
246
600
|
const ext = path2.extname(filePath).toLowerCase();
|
|
247
601
|
const contentType = this._mimeTypes[ext] || "application/octet-stream";
|
|
248
602
|
res.writeHead(200, { "Content-Type": contentType });
|
|
249
|
-
|
|
603
|
+
log2(`[200] ${req.method} ${req.url} -> ${filePath}`);
|
|
250
604
|
const readStream = fs2.createReadStream(filePath);
|
|
251
605
|
readStream.pipe(res);
|
|
252
606
|
readStream.on("error", (err2) => {
|
|
253
|
-
|
|
607
|
+
log2("Stream error: %o", err2);
|
|
254
608
|
res.end();
|
|
255
609
|
});
|
|
256
610
|
});
|
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
|
}
|