flakiness 0.211.0 → 0.214.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 +1947 -1367
- package/package.json +4 -4
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cli.js
CHANGED
|
@@ -2,1476 +2,1760 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { showReport } from "@flakiness/sdk";
|
|
5
|
-
import { Command, Option } from "commander";
|
|
6
|
-
import debug2 from "debug";
|
|
7
|
-
import path6 from "path";
|
|
8
|
-
|
|
9
|
-
// ../package.json
|
|
10
|
-
var package_default = {
|
|
11
|
-
name: "flakiness",
|
|
12
|
-
version: "0.211.0",
|
|
13
|
-
type: "module",
|
|
14
|
-
private: true,
|
|
15
|
-
scripts: {
|
|
16
|
-
minor: "./version.mjs minor",
|
|
17
|
-
patch: "./version.mjs patch",
|
|
18
|
-
dev: "pnpm kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./github_webhooks.mts",
|
|
19
|
-
"dev+billing": "pnpm kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe_webhooks.mts ./github_webhooks.mts",
|
|
20
|
-
prod: "pnpm kubik --env-file=.env.prodlocal -w ./cli/build.mts ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts ./github_webhooks.mts",
|
|
21
|
-
build: "pnpm kubik $(find . -name build.mts)",
|
|
22
|
-
perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
|
|
23
|
-
},
|
|
24
|
-
engines: {
|
|
25
|
-
node: ">=24"
|
|
26
|
-
},
|
|
27
|
-
author: "Degu Labs, Inc",
|
|
28
|
-
license: "Fair Source 100",
|
|
29
|
-
devDependencies: {
|
|
30
|
-
"@flakiness/playwright": "catalog:",
|
|
31
|
-
"@playwright/test": "catalog:",
|
|
32
|
-
"@types/node": "^22.19.3",
|
|
33
|
-
esbuild: "^0.27.2",
|
|
34
|
-
glob: "catalog:",
|
|
35
|
-
kubik: "^0.24.0",
|
|
36
|
-
"smee-client": "^5.0.0",
|
|
37
|
-
tsx: "^4.21.0",
|
|
38
|
-
typescript: "^5.9.3"
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// src/userSession.ts
|
|
43
|
-
import fs from "fs/promises";
|
|
44
|
-
import os from "os";
|
|
45
|
-
import path from "path";
|
|
46
5
|
|
|
47
|
-
// ../
|
|
48
|
-
var
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
NO_CONTENT: 204,
|
|
63
|
-
RESET_CONTENT: 205,
|
|
64
|
-
PARTIAL_CONTENT: 206,
|
|
65
|
-
MULTI_STATUS: 207
|
|
66
|
-
},
|
|
67
|
-
Redirection: {
|
|
68
|
-
MULTIPLE_CHOICES: 300,
|
|
69
|
-
MOVED_PERMANENTLY: 301,
|
|
70
|
-
MOVED_TEMPORARILY: 302,
|
|
71
|
-
SEE_OTHER: 303,
|
|
72
|
-
NOT_MODIFIED: 304,
|
|
73
|
-
USE_PROXY: 305,
|
|
74
|
-
TEMPORARY_REDIRECT: 307,
|
|
75
|
-
PERMANENT_REDIRECT: 308
|
|
76
|
-
},
|
|
77
|
-
ClientErrors: {
|
|
78
|
-
BAD_REQUEST: 400,
|
|
79
|
-
UNAUTHORIZED: 401,
|
|
80
|
-
PAYMENT_REQUIRED: 402,
|
|
81
|
-
FORBIDDEN: 403,
|
|
82
|
-
NOT_FOUND: 404,
|
|
83
|
-
METHOD_NOT_ALLOWED: 405,
|
|
84
|
-
NOT_ACCEPTABLE: 406,
|
|
85
|
-
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
86
|
-
REQUEST_TIMEOUT: 408,
|
|
87
|
-
CONFLICT: 409,
|
|
88
|
-
GONE: 410,
|
|
89
|
-
LENGTH_REQUIRED: 411,
|
|
90
|
-
PRECONDITION_FAILED: 412,
|
|
91
|
-
REQUEST_TOO_LONG: 413,
|
|
92
|
-
REQUEST_URI_TOO_LONG: 414,
|
|
93
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
94
|
-
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
95
|
-
EXPECTATION_FAILED: 417,
|
|
96
|
-
IM_A_TEAPOT: 418,
|
|
97
|
-
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
98
|
-
METHOD_FAILURE: 420,
|
|
99
|
-
MISDIRECTED_REQUEST: 421,
|
|
100
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
101
|
-
LOCKED: 423,
|
|
102
|
-
FAILED_DEPENDENCY: 424,
|
|
103
|
-
UPGRADE_REQUIRED: 426,
|
|
104
|
-
PRECONDITION_REQUIRED: 428,
|
|
105
|
-
TOO_MANY_REQUESTS: 429,
|
|
106
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
107
|
-
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
108
|
-
},
|
|
109
|
-
ServerErrors: {
|
|
110
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
111
|
-
NOT_IMPLEMENTED: 501,
|
|
112
|
-
BAD_GATEWAY: 502,
|
|
113
|
-
SERVICE_UNAVAILABLE: 503,
|
|
114
|
-
GATEWAY_TIMEOUT: 504,
|
|
115
|
-
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
116
|
-
INSUFFICIENT_STORAGE: 507,
|
|
117
|
-
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
const AllErrorCodes = {
|
|
121
|
-
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
122
|
-
...TypedHTTP2.StatusCodes.ServerErrors
|
|
123
|
-
};
|
|
124
|
-
function assert2(value, code, message) {
|
|
125
|
-
if (!value)
|
|
126
|
-
throw HttpError.withCode(code, message);
|
|
127
|
-
}
|
|
128
|
-
TypedHTTP2.assert = assert2;
|
|
129
|
-
class HttpError extends Error {
|
|
130
|
-
constructor(status, message) {
|
|
131
|
-
super(message);
|
|
132
|
-
this.status = status;
|
|
6
|
+
// ../node_modules/.pnpm/vlq@2.0.4/node_modules/vlq/src/index.js
|
|
7
|
+
var char_to_integer = {};
|
|
8
|
+
var integer_to_char = {};
|
|
9
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
|
|
10
|
+
char_to_integer[char] = i;
|
|
11
|
+
integer_to_char[i] = char;
|
|
12
|
+
});
|
|
13
|
+
function decode(string) {
|
|
14
|
+
let result = [];
|
|
15
|
+
let shift = 0;
|
|
16
|
+
let value = 0;
|
|
17
|
+
for (let i = 0; i < string.length; i += 1) {
|
|
18
|
+
let integer = char_to_integer[string[i]];
|
|
19
|
+
if (integer === void 0) {
|
|
20
|
+
throw new Error("Invalid character (" + string[i] + ")");
|
|
133
21
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
22
|
+
const has_continuation_bit = integer & 32;
|
|
23
|
+
integer &= 31;
|
|
24
|
+
value += integer << shift;
|
|
25
|
+
if (has_continuation_bit) {
|
|
26
|
+
shift += 5;
|
|
27
|
+
} else {
|
|
28
|
+
const should_negate = value & 1;
|
|
29
|
+
value >>>= 1;
|
|
30
|
+
if (should_negate) {
|
|
31
|
+
result.push(value === 0 ? -2147483648 : -value);
|
|
32
|
+
} else {
|
|
33
|
+
result.push(value);
|
|
34
|
+
}
|
|
35
|
+
value = shift = 0;
|
|
138
36
|
}
|
|
139
37
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function encode(value) {
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
return encode_integer(value);
|
|
144
43
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
44
|
+
let result = "";
|
|
45
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
46
|
+
result += encode_integer(value[i]);
|
|
148
47
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
function encode_integer(num) {
|
|
51
|
+
let result = "";
|
|
52
|
+
if (num < 0) {
|
|
53
|
+
num = -num << 1 | 1;
|
|
54
|
+
} else {
|
|
55
|
+
num <<= 1;
|
|
152
56
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
57
|
+
do {
|
|
58
|
+
let clamped = num & 31;
|
|
59
|
+
num >>>= 5;
|
|
60
|
+
if (num > 0) {
|
|
61
|
+
clamped |= 32;
|
|
62
|
+
}
|
|
63
|
+
result += integer_to_char[clamped];
|
|
64
|
+
} while (num > 0);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ../server/lib/common/heap.js
|
|
69
|
+
var Heap = class _Heap {
|
|
70
|
+
constructor(_cmp, elements = []) {
|
|
71
|
+
this._cmp = _cmp;
|
|
72
|
+
this._heap = elements.map(([element, score]) => ({ element, score }));
|
|
73
|
+
for (let idx = this._heap.length - 1; idx >= 0; --idx)
|
|
74
|
+
this._down(idx);
|
|
156
75
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return { status };
|
|
76
|
+
static createMin(elements = []) {
|
|
77
|
+
return new _Heap((a, b) => a - b, elements);
|
|
160
78
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
165
|
-
contentType,
|
|
166
|
-
data
|
|
167
|
-
};
|
|
79
|
+
static createMax(elements = []) {
|
|
80
|
+
return new _Heap((a, b) => b - a, elements);
|
|
168
81
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
82
|
+
_heap = [];
|
|
83
|
+
_up(idx) {
|
|
84
|
+
const e2 = this._heap[idx];
|
|
85
|
+
while (idx > 0) {
|
|
86
|
+
const parentIdx = idx - 1 >>> 1;
|
|
87
|
+
if (this._cmp(this._heap[parentIdx].score, e2.score) <= 0)
|
|
88
|
+
break;
|
|
89
|
+
this._heap[idx] = this._heap[parentIdx];
|
|
90
|
+
idx = parentIdx;
|
|
91
|
+
}
|
|
92
|
+
this._heap[idx] = e2;
|
|
172
93
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
94
|
+
_down(idx) {
|
|
95
|
+
const N = this._heap.length;
|
|
96
|
+
const e2 = this._heap[idx];
|
|
97
|
+
while (true) {
|
|
98
|
+
const leftIdx = idx * 2 + 1;
|
|
99
|
+
const rightIdx = idx * 2 + 2;
|
|
100
|
+
let smallestIdx;
|
|
101
|
+
if (leftIdx < N && rightIdx < N) {
|
|
102
|
+
smallestIdx = this._cmp(this._heap[leftIdx].score, this._heap[rightIdx].score) < 0 ? leftIdx : rightIdx;
|
|
103
|
+
} else if (leftIdx < N) {
|
|
104
|
+
smallestIdx = leftIdx;
|
|
105
|
+
} else if (rightIdx < N) {
|
|
106
|
+
smallestIdx = rightIdx;
|
|
107
|
+
} else {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (this._cmp(e2.score, this._heap[smallestIdx].score) < 0)
|
|
111
|
+
break;
|
|
112
|
+
this._heap[idx] = this._heap[smallestIdx];
|
|
113
|
+
idx = smallestIdx;
|
|
114
|
+
}
|
|
115
|
+
this._heap[idx] = e2;
|
|
176
116
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this._resolveContext = _resolveContext;
|
|
181
|
-
}
|
|
182
|
-
static create() {
|
|
183
|
-
return new Router(async (e) => e.ctx);
|
|
184
|
-
}
|
|
185
|
-
rawMethod(method, route) {
|
|
186
|
-
return {
|
|
187
|
-
[method]: {
|
|
188
|
-
method,
|
|
189
|
-
input: route.input,
|
|
190
|
-
etag: route.etag,
|
|
191
|
-
resolveContext: this._resolveContext,
|
|
192
|
-
handler: route.handler
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
get(route) {
|
|
197
|
-
return this.rawMethod("GET", {
|
|
198
|
-
...route,
|
|
199
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
post(route) {
|
|
203
|
-
return this.rawMethod("POST", {
|
|
204
|
-
...route,
|
|
205
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
use(resolveContext) {
|
|
209
|
-
return new Router(async (options) => {
|
|
210
|
-
const m = await this._resolveContext(options);
|
|
211
|
-
return resolveContext({ ...options, ctx: m });
|
|
212
|
-
});
|
|
213
|
-
}
|
|
117
|
+
push(element, score) {
|
|
118
|
+
this._heap.push({ element, score });
|
|
119
|
+
this._up(this._heap.length - 1);
|
|
214
120
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
121
|
+
get size() {
|
|
122
|
+
return this._heap.length;
|
|
123
|
+
}
|
|
124
|
+
peekEntry() {
|
|
125
|
+
return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
|
|
126
|
+
}
|
|
127
|
+
popEntry() {
|
|
128
|
+
if (!this._heap.length)
|
|
129
|
+
return void 0;
|
|
130
|
+
const entry = this._heap[0];
|
|
131
|
+
const last = this._heap.pop();
|
|
132
|
+
if (!this._heap.length)
|
|
133
|
+
return [entry.element, entry.score];
|
|
134
|
+
this._heap[0] = last;
|
|
135
|
+
this._down(0);
|
|
136
|
+
return [entry.element, entry.score];
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ../server/lib/common/sequence.js
|
|
141
|
+
var Sequence = class _Sequence {
|
|
142
|
+
constructor(_seek, length) {
|
|
143
|
+
this._seek = _seek;
|
|
144
|
+
this.length = length;
|
|
145
|
+
}
|
|
146
|
+
static fromList(a) {
|
|
147
|
+
return new _Sequence(
|
|
148
|
+
function(pos) {
|
|
149
|
+
return {
|
|
150
|
+
next() {
|
|
151
|
+
if (pos >= a.length)
|
|
152
|
+
return { done: true, value: void 0 };
|
|
153
|
+
return { done: false, value: a[pos++] };
|
|
245
154
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
a.length
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
static chain(seqs) {
|
|
161
|
+
const leftsums = [];
|
|
162
|
+
let length = 0;
|
|
163
|
+
for (let i = 0; i < seqs.length; ++i) {
|
|
164
|
+
length += seqs[i].length;
|
|
165
|
+
leftsums.push(length);
|
|
256
166
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
167
|
+
return new _Sequence(
|
|
168
|
+
function(fromIdx) {
|
|
169
|
+
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
170
|
+
let idx = _Sequence.fromList(leftsums).partitionPoint(((x) => x <= fromIdx));
|
|
171
|
+
if (idx >= seqs.length) {
|
|
172
|
+
return {
|
|
173
|
+
next: () => ({ done: true, value: void 0 })
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
let it = seqs[idx].seek(idx > 0 ? fromIdx - leftsums[idx - 1] : fromIdx);
|
|
177
|
+
return {
|
|
178
|
+
next() {
|
|
179
|
+
let result = it.next();
|
|
180
|
+
while (result.done && ++idx < seqs.length) {
|
|
181
|
+
it = seqs[idx].seek(0);
|
|
182
|
+
result = it.next();
|
|
183
|
+
}
|
|
184
|
+
return result.done ? result : { done: false, value: [result.value, idx] };
|
|
266
185
|
}
|
|
267
|
-
|
|
268
|
-
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
length
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
static merge(sequences, cmp) {
|
|
192
|
+
const length = sequences.reduce((acc, seq) => acc + seq.length, 0);
|
|
193
|
+
return new _Sequence(
|
|
194
|
+
function(fromIdx) {
|
|
195
|
+
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
196
|
+
const offsets = quickAdvance(sequences, cmp, fromIdx);
|
|
197
|
+
const entries = [];
|
|
198
|
+
for (let i = 0; i < sequences.length; ++i) {
|
|
199
|
+
const seq = sequences[i];
|
|
200
|
+
const it = seq.seek(offsets[i]);
|
|
201
|
+
const itval = it.next();
|
|
202
|
+
if (!itval.done)
|
|
203
|
+
entries.push([it, itval.value]);
|
|
269
204
|
}
|
|
270
|
-
|
|
205
|
+
const heap = new Heap(cmp, entries);
|
|
206
|
+
return {
|
|
207
|
+
next() {
|
|
208
|
+
if (!heap.size)
|
|
209
|
+
return { done: true, value: void 0 };
|
|
210
|
+
++fromIdx;
|
|
211
|
+
const [it, e2] = heap.popEntry();
|
|
212
|
+
const itval = it.next();
|
|
213
|
+
if (!itval.done)
|
|
214
|
+
heap.push(it, itval.value);
|
|
215
|
+
return { done: false, value: e2 };
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
length
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
static EMPTY = new _Sequence(function* () {
|
|
223
|
+
}, 0);
|
|
224
|
+
seek(idx) {
|
|
225
|
+
const it = this._seek(idx);
|
|
226
|
+
it[Symbol.iterator] = () => it;
|
|
227
|
+
return it;
|
|
228
|
+
}
|
|
229
|
+
get(idx) {
|
|
230
|
+
return this.seek(idx).next().value;
|
|
231
|
+
}
|
|
232
|
+
map(mapper) {
|
|
233
|
+
const originalSeek = this._seek;
|
|
234
|
+
return new _Sequence(
|
|
235
|
+
function(idx) {
|
|
236
|
+
const it = originalSeek(idx);
|
|
237
|
+
return {
|
|
238
|
+
next() {
|
|
239
|
+
const next = it.next();
|
|
240
|
+
if (next.done)
|
|
241
|
+
return next;
|
|
242
|
+
return { done: false, value: mapper(next.value) };
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
this.length
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
/** Number of elements in sequence that are <= comparator. Only works on sorted sequences. */
|
|
250
|
+
partitionPoint(predicate) {
|
|
251
|
+
let lo = 0, hi = this.length;
|
|
252
|
+
while (lo < hi) {
|
|
253
|
+
const mid = lo + hi >>> 1;
|
|
254
|
+
if (predicate(this.get(mid)))
|
|
255
|
+
lo = mid + 1;
|
|
256
|
+
else
|
|
257
|
+
hi = mid;
|
|
271
258
|
}
|
|
272
|
-
return
|
|
259
|
+
return lo;
|
|
273
260
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
headers: {
|
|
285
|
-
...init.headers,
|
|
286
|
-
"Authorization": `Bearer ${options.auth}`
|
|
261
|
+
};
|
|
262
|
+
function quickAdvance(sequences, cmp, k) {
|
|
263
|
+
const offsets = new Map(sequences.map((s) => [s, 0]));
|
|
264
|
+
while (offsets.size && k > 0) {
|
|
265
|
+
const t2 = offsets.size;
|
|
266
|
+
const x = Math.max(Math.floor(k / t2 / 2), 1);
|
|
267
|
+
const entries = [];
|
|
268
|
+
for (const [seq, offset] of offsets) {
|
|
269
|
+
if (offset + x <= seq.length)
|
|
270
|
+
entries.push([seq, seq.get(offset + x - 1)]);
|
|
287
271
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
return await job();
|
|
297
|
-
} catch (e) {
|
|
298
|
-
if (e instanceof AggregateError)
|
|
299
|
-
console.error(`[flakiness.io err]`, log(e.errors[0]));
|
|
300
|
-
else if (e instanceof Error)
|
|
301
|
-
console.error(`[flakiness.io err]`, log(e));
|
|
272
|
+
const heap = new Heap(cmp, entries);
|
|
273
|
+
while (heap.size && k > 0 && (x === 1 || k >= x * t2)) {
|
|
274
|
+
k -= x;
|
|
275
|
+
const [seq] = heap.popEntry();
|
|
276
|
+
const offset = offsets.get(seq) + x;
|
|
277
|
+
if (offset === seq.length)
|
|
278
|
+
offsets.delete(seq);
|
|
302
279
|
else
|
|
303
|
-
|
|
304
|
-
|
|
280
|
+
offsets.set(seq, offset);
|
|
281
|
+
if (offset + x <= seq.length)
|
|
282
|
+
heap.push(seq, seq.get(offset + x - 1));
|
|
305
283
|
}
|
|
306
284
|
}
|
|
307
|
-
return
|
|
285
|
+
return sequences.map((seq) => offsets.get(seq) ?? seq.length);
|
|
308
286
|
}
|
|
309
287
|
|
|
310
|
-
//
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
constructor(_config) {
|
|
318
|
-
this._config = _config;
|
|
319
|
-
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
320
|
-
}
|
|
321
|
-
static async load() {
|
|
322
|
-
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
323
|
-
if (!data)
|
|
324
|
-
return void 0;
|
|
325
|
-
const json = JSON.parse(data);
|
|
326
|
-
return new _UserSession(json);
|
|
288
|
+
// ../server/lib/common/ranges.js
|
|
289
|
+
var Ranges;
|
|
290
|
+
((Ranges2) => {
|
|
291
|
+
Ranges2.EMPTY = [];
|
|
292
|
+
Ranges2.FULL = [-Infinity, Infinity];
|
|
293
|
+
function isFull(ranges) {
|
|
294
|
+
return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
|
|
327
295
|
}
|
|
328
|
-
|
|
329
|
-
|
|
296
|
+
Ranges2.isFull = isFull;
|
|
297
|
+
function compress(ranges) {
|
|
298
|
+
if (!ranges.length)
|
|
299
|
+
return "";
|
|
300
|
+
if (isInfinite(ranges))
|
|
301
|
+
throw new Error("Compression of infinite ranges is not supported");
|
|
302
|
+
const prepared = [];
|
|
303
|
+
let last = ranges[0] - 1;
|
|
304
|
+
prepared.push(last);
|
|
305
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
306
|
+
if (ranges[i] === ranges[i + 1]) {
|
|
307
|
+
prepared.push(-(ranges[i] - last));
|
|
308
|
+
} else {
|
|
309
|
+
prepared.push(ranges[i] - last);
|
|
310
|
+
prepared.push(ranges[i + 1] - ranges[i]);
|
|
311
|
+
}
|
|
312
|
+
last = ranges[i + 1];
|
|
313
|
+
}
|
|
314
|
+
return encode(prepared);
|
|
330
315
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
316
|
+
Ranges2.compress = compress;
|
|
317
|
+
function decompress(compressed) {
|
|
318
|
+
if (!compressed.length)
|
|
319
|
+
return [];
|
|
320
|
+
const prepared = decode(compressed);
|
|
321
|
+
const result = [];
|
|
322
|
+
let last = prepared[0];
|
|
323
|
+
for (let i = 1; i < prepared.length; ++i) {
|
|
324
|
+
if (prepared[i] < 0) {
|
|
325
|
+
result.push(-prepared[i] + last);
|
|
326
|
+
result.push(-prepared[i] + last);
|
|
327
|
+
last -= prepared[i];
|
|
328
|
+
} else {
|
|
329
|
+
result.push(prepared[i] + last);
|
|
330
|
+
last += prepared[i];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
334
|
}
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
Ranges2.decompress = decompress;
|
|
336
|
+
function toString(ranges) {
|
|
337
|
+
const tokens = [];
|
|
338
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
339
|
+
if (ranges[i] === ranges[i + 1])
|
|
340
|
+
tokens.push(ranges[i]);
|
|
341
|
+
else
|
|
342
|
+
tokens.push(`${ranges[i]}-${ranges[i + 1]}`);
|
|
343
|
+
}
|
|
344
|
+
if (!tokens.length)
|
|
345
|
+
return `[]`;
|
|
346
|
+
return `[ ` + tokens.join(", ") + ` ]`;
|
|
337
347
|
}
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
Ranges2.toString = toString;
|
|
349
|
+
function popInplace(ranges) {
|
|
350
|
+
if (isInfinite(ranges))
|
|
351
|
+
throw new Error("cannot pop from infinite ranges!");
|
|
352
|
+
const last = ranges.at(-1);
|
|
353
|
+
const prelast = ranges.at(-2);
|
|
354
|
+
if (last === void 0 || prelast === void 0)
|
|
355
|
+
return void 0;
|
|
356
|
+
if (last === prelast) {
|
|
357
|
+
ranges.pop();
|
|
358
|
+
ranges.pop();
|
|
359
|
+
} else {
|
|
360
|
+
ranges[ranges.length - 1] = last - 1;
|
|
361
|
+
}
|
|
362
|
+
return last;
|
|
340
363
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
Ranges2.popInplace = popInplace;
|
|
365
|
+
function* iterate(ranges) {
|
|
366
|
+
if (isInfinite(ranges))
|
|
367
|
+
throw new Error("cannot iterate infinite ranges!");
|
|
368
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
369
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
370
|
+
yield j;
|
|
371
|
+
}
|
|
344
372
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
355
|
-
api: createServerAPI(options.endpoint, { auth: options.accessToken }),
|
|
356
|
-
method: "access token",
|
|
357
|
-
accessToken: options.accessToken,
|
|
358
|
-
endpoint: options.endpoint
|
|
359
|
-
};
|
|
373
|
+
Ranges2.iterate = iterate;
|
|
374
|
+
function toSortedList(ranges) {
|
|
375
|
+
if (isInfinite(ranges))
|
|
376
|
+
throw new Error("cannot convert infinite ranges!");
|
|
377
|
+
const list2 = [];
|
|
378
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
379
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
380
|
+
list2.push(j);
|
|
381
|
+
}
|
|
382
|
+
return list2;
|
|
360
383
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
Ranges2.toSortedList = toSortedList;
|
|
385
|
+
function toInt32Array(ranges) {
|
|
386
|
+
if (isInfinite(ranges))
|
|
387
|
+
throw new Error("cannot convert infinite ranges!");
|
|
388
|
+
const result = new Int32Array(cardinality(ranges));
|
|
389
|
+
let idx = 0;
|
|
390
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
391
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
392
|
+
result[idx++] = j;
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
369
395
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
endpoint: options.endpoint
|
|
380
|
-
};
|
|
396
|
+
Ranges2.toInt32Array = toInt32Array;
|
|
397
|
+
function fromList(x) {
|
|
398
|
+
for (let i = 0; i < x.length - 1; ++i) {
|
|
399
|
+
if (x[i] > x[i + 1]) {
|
|
400
|
+
x = x.toSorted((a, b) => a - b);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return fromSortedList(x);
|
|
381
405
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// src/cli/cmd-access.ts
|
|
386
|
-
async function cmdAccess(options) {
|
|
387
|
-
const [orgSlug, projectSlug] = options.flakinessProject.split("/");
|
|
388
|
-
if (!orgSlug || !projectSlug)
|
|
389
|
-
throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
|
|
390
|
-
const auth2 = await authenticate({
|
|
391
|
-
accessToken: options.accessToken,
|
|
392
|
-
endpoint: options.endpoint,
|
|
393
|
-
flakinessProject: options.flakinessProject
|
|
394
|
-
});
|
|
395
|
-
const result = await auth2.api.project.checkAccess.GET({ orgSlug, projectSlug });
|
|
396
|
-
if (options.json) {
|
|
397
|
-
console.log(JSON.stringify(result, null, 2));
|
|
398
|
-
} else if (!options.quiet) {
|
|
399
|
-
console.log(chalk.bold(`Project: `) + `${orgSlug}/${projectSlug}`);
|
|
400
|
-
console.log(chalk.bold(`Auth: `) + (result.auth ?? chalk.dim("none")));
|
|
401
|
-
console.log(chalk.bold(`Access: `) + (result.access ? chalk.green("granted") : chalk.red("denied")));
|
|
402
|
-
if (result.permissions.length)
|
|
403
|
-
console.log(chalk.bold(`Perms: `) + result.permissions.join(", "));
|
|
406
|
+
Ranges2.fromList = fromList;
|
|
407
|
+
function from(x) {
|
|
408
|
+
return [x, x];
|
|
404
409
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
import open from "open";
|
|
417
|
-
import os2 from "os";
|
|
418
|
-
|
|
419
|
-
// src/cli/cmd-auth-logout.ts
|
|
420
|
-
async function cmdAuthLogout() {
|
|
421
|
-
const session = await UserSession.load();
|
|
422
|
-
if (!session)
|
|
423
|
-
return;
|
|
424
|
-
const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
|
|
425
|
-
if (currentSession)
|
|
426
|
-
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
|
|
427
|
-
await UserSession.remove();
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// src/cli/cmd-auth-whoami.ts
|
|
431
|
-
import chalk2 from "chalk";
|
|
432
|
-
async function cmdAuthWhoami() {
|
|
433
|
-
const session = await UserSession.load();
|
|
434
|
-
if (!session || !await printLoggedInUser(session)) {
|
|
435
|
-
console.log('Not logged in. Run "flakiness auth login" first.');
|
|
436
|
-
process.exit(1);
|
|
410
|
+
Ranges2.from = from;
|
|
411
|
+
function fromSortedList(sorted) {
|
|
412
|
+
const ranges = [];
|
|
413
|
+
let rangeStart = 0;
|
|
414
|
+
for (let i = 1; i <= sorted.length; ++i) {
|
|
415
|
+
if (i < sorted.length && sorted[i] - sorted[i - 1] <= 1)
|
|
416
|
+
continue;
|
|
417
|
+
ranges.push(sorted[rangeStart], sorted[i - 1]);
|
|
418
|
+
rangeStart = i;
|
|
419
|
+
}
|
|
420
|
+
return ranges;
|
|
437
421
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const user = await session.api.user.whoami.GET();
|
|
442
|
-
const superUser = user.isSuperUser ? " " + chalk2.red.bold("[SUPERUSER]") : "";
|
|
443
|
-
console.log(`Logged in as ${chalk2.bold(user.userName)} (${user.userLogin})${superUser}`);
|
|
444
|
-
console.log(`Endpoint: ${chalk2.cyan(session.endpoint())}`);
|
|
445
|
-
return true;
|
|
446
|
-
} catch (e) {
|
|
447
|
-
return false;
|
|
422
|
+
Ranges2.fromSortedList = fromSortedList;
|
|
423
|
+
function isInfinite(ranges) {
|
|
424
|
+
return ranges.length > 0 && (Object.is(ranges[0], Infinity) || Object.is(ranges[ranges.length - 1], Infinity));
|
|
448
425
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
462
|
-
let token;
|
|
463
|
-
while (Date.now() < data.deadline) {
|
|
464
|
-
await new Promise((x) => setTimeout(x, 2e3));
|
|
465
|
-
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
|
|
466
|
-
if (!result) {
|
|
467
|
-
console.error(`Authorization request was rejected.`);
|
|
468
|
-
process.exit(1);
|
|
426
|
+
Ranges2.isInfinite = isInfinite;
|
|
427
|
+
function includes(ranges, e2) {
|
|
428
|
+
if (!ranges.length)
|
|
429
|
+
return false;
|
|
430
|
+
if (e2 < ranges[0] || ranges[ranges.length - 1] < e2)
|
|
431
|
+
return false;
|
|
432
|
+
if (ranges.length < 17) {
|
|
433
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
434
|
+
if (ranges[i] <= e2 && e2 <= ranges[i + 1])
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
469
438
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
439
|
+
let lo = 0, hi = ranges.length;
|
|
440
|
+
while (lo < hi) {
|
|
441
|
+
const mid = lo + hi >>> 1;
|
|
442
|
+
if (ranges[mid] === e2)
|
|
443
|
+
return true;
|
|
444
|
+
if (ranges[mid] < e2)
|
|
445
|
+
lo = mid + 1;
|
|
446
|
+
else
|
|
447
|
+
hi = mid;
|
|
448
|
+
}
|
|
449
|
+
return (lo & 1) !== 0;
|
|
473
450
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
451
|
+
Ranges2.includes = includes;
|
|
452
|
+
function cardinality(ranges) {
|
|
453
|
+
if (isInfinite(ranges))
|
|
454
|
+
return Infinity;
|
|
455
|
+
let sum = 0;
|
|
456
|
+
for (let i = 0; i < ranges.length - 1; i += 2)
|
|
457
|
+
sum += ranges[i + 1] - ranges[i] + 1;
|
|
458
|
+
return sum;
|
|
477
459
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
});
|
|
482
|
-
if (await printLoggedInUser(session)) {
|
|
483
|
-
await session.save();
|
|
484
|
-
} else {
|
|
485
|
-
console.error(`x Failed to login:`);
|
|
460
|
+
Ranges2.cardinality = cardinality;
|
|
461
|
+
function offset(ranges, offset2) {
|
|
462
|
+
return ranges.map((x) => x + offset2);
|
|
486
463
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
464
|
+
Ranges2.offset = offset;
|
|
465
|
+
function intersect(ranges1, ranges2) {
|
|
466
|
+
const ranges = [];
|
|
467
|
+
if (!ranges1.length || !ranges2.length)
|
|
468
|
+
return ranges;
|
|
469
|
+
if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
|
|
470
|
+
return ranges;
|
|
471
|
+
let p1 = 0;
|
|
472
|
+
let p2 = 0;
|
|
473
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
474
|
+
if (ranges1[p1 + 1] < ranges2[p2]) {
|
|
475
|
+
p1 += 2;
|
|
476
|
+
let offset2 = 1;
|
|
477
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
|
|
478
|
+
offset2 <<= 1;
|
|
479
|
+
p1 += offset2 >> 1 << 1;
|
|
480
|
+
} else if (ranges2[p2 + 1] < ranges1[p1]) {
|
|
481
|
+
p2 += 2;
|
|
482
|
+
let offset2 = 1;
|
|
483
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
|
|
484
|
+
offset2 <<= 1;
|
|
485
|
+
p2 += offset2 >> 1 << 1;
|
|
486
|
+
} else {
|
|
487
|
+
const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
|
|
488
|
+
const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
|
|
489
|
+
ranges.push(Math.max(a1, b1), Math.min(a2, b2));
|
|
490
|
+
if (a2 < b2) {
|
|
491
|
+
p1 += 2;
|
|
492
|
+
} else if (a2 > b2) {
|
|
493
|
+
p2 += 2;
|
|
494
|
+
} else {
|
|
495
|
+
p1 += 2;
|
|
496
|
+
p2 += 2;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
513
499
|
}
|
|
500
|
+
return ranges;
|
|
514
501
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
|
|
526
|
-
errors.push({
|
|
527
|
-
message,
|
|
528
|
-
stack
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
return errors;
|
|
532
|
-
}
|
|
533
|
-
function extractStdout(testcase, stdio) {
|
|
534
|
-
const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === stdio);
|
|
535
|
-
if (!xmlStdio.length)
|
|
536
|
-
return void 0;
|
|
537
|
-
return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
|
|
538
|
-
text: txtNode.text
|
|
539
|
-
}));
|
|
540
|
-
}
|
|
541
|
-
async function parseAttachment(value) {
|
|
542
|
-
let absolutePath = path2.resolve(process.cwd(), value);
|
|
543
|
-
if (fs2.existsSync(absolutePath))
|
|
544
|
-
return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
|
|
545
|
-
return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
|
|
546
|
-
}
|
|
547
|
-
async function traverseJUnitReport(context, node) {
|
|
548
|
-
const element = node;
|
|
549
|
-
if (!(element instanceof XmlElement))
|
|
550
|
-
return;
|
|
551
|
-
let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
|
|
552
|
-
if (element.attributes["timestamp"])
|
|
553
|
-
currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
|
|
554
|
-
if (element.name === "testsuite") {
|
|
555
|
-
const file = element.attributes["file"];
|
|
556
|
-
const line = parseInt(element.attributes["line"], 10);
|
|
557
|
-
const name = element.attributes["name"];
|
|
558
|
-
const newSuite = {
|
|
559
|
-
title: name ?? file,
|
|
560
|
-
location: file && !isNaN(line) ? {
|
|
561
|
-
file,
|
|
562
|
-
line,
|
|
563
|
-
column: 1
|
|
564
|
-
} : void 0,
|
|
565
|
-
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
566
|
-
suites: [],
|
|
567
|
-
tests: []
|
|
568
|
-
};
|
|
569
|
-
if (currentSuite) {
|
|
570
|
-
currentSuite.suites ??= [];
|
|
571
|
-
currentSuite.suites.push(newSuite);
|
|
572
|
-
} else {
|
|
573
|
-
report.suites ??= [];
|
|
574
|
-
report.suites.push(newSuite);
|
|
575
|
-
}
|
|
576
|
-
currentSuite = newSuite;
|
|
577
|
-
const userSuppliedData = getProperties(element);
|
|
578
|
-
if (userSuppliedData.length) {
|
|
579
|
-
currentEnv = structuredClone(currentEnv);
|
|
580
|
-
currentEnv.userSuppliedData ??= {};
|
|
581
|
-
for (const [key, value] of userSuppliedData)
|
|
582
|
-
currentEnv.userSuppliedData[key] = value;
|
|
583
|
-
currentEnvIndex = report.environments.push(currentEnv) - 1;
|
|
584
|
-
}
|
|
585
|
-
} else if (element.name === "testcase") {
|
|
586
|
-
assert(currentSuite);
|
|
587
|
-
const file = element.attributes["file"];
|
|
588
|
-
const name = element.attributes["name"];
|
|
589
|
-
const line = parseInt(element.attributes["line"], 10);
|
|
590
|
-
const timeMs = parseFloat(element.attributes["time"]) * 1e3;
|
|
591
|
-
const startTimestamp = currentTimeMs;
|
|
592
|
-
const duration = timeMs;
|
|
593
|
-
currentTimeMs += timeMs;
|
|
594
|
-
const annotations = [];
|
|
595
|
-
const attachments2 = [];
|
|
596
|
-
for (const [key, value] of getProperties(element)) {
|
|
597
|
-
if (key.toLowerCase().startsWith("attachment")) {
|
|
598
|
-
if (context.ignoreAttachments)
|
|
599
|
-
continue;
|
|
600
|
-
const attachment = await parseAttachment(value);
|
|
601
|
-
context.attachments.set(attachment.id, attachment);
|
|
602
|
-
attachments2.push({
|
|
603
|
-
id: attachment.id,
|
|
604
|
-
contentType: attachment.contentType,
|
|
605
|
-
//TODO: better default names for attachments?
|
|
606
|
-
name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
|
|
607
|
-
});
|
|
502
|
+
Ranges2.intersect = intersect;
|
|
503
|
+
function capAt(ranges, cap) {
|
|
504
|
+
const result = [];
|
|
505
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
506
|
+
const start = ranges[i];
|
|
507
|
+
const end = ranges[i + 1];
|
|
508
|
+
if (start > cap)
|
|
509
|
+
break;
|
|
510
|
+
if (end <= cap) {
|
|
511
|
+
result.push(start, end);
|
|
608
512
|
} else {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
description: value.length ? value : void 0
|
|
612
|
-
});
|
|
513
|
+
result.push(start, cap);
|
|
514
|
+
break;
|
|
613
515
|
}
|
|
614
516
|
}
|
|
615
|
-
|
|
616
|
-
const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
|
|
617
|
-
if (xmlSkippedAnnotation)
|
|
618
|
-
annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
|
|
619
|
-
const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
|
|
620
|
-
const errors = extractErrors(element);
|
|
621
|
-
const test = {
|
|
622
|
-
title: name,
|
|
623
|
-
location: file && !isNaN(line) ? {
|
|
624
|
-
file,
|
|
625
|
-
line,
|
|
626
|
-
column: 1
|
|
627
|
-
} : void 0,
|
|
628
|
-
attempts: [{
|
|
629
|
-
environmentIdx: currentEnvIndex,
|
|
630
|
-
expectedStatus,
|
|
631
|
-
annotations,
|
|
632
|
-
attachments: attachments2,
|
|
633
|
-
startTimestamp,
|
|
634
|
-
duration,
|
|
635
|
-
status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
|
|
636
|
-
errors,
|
|
637
|
-
stdout: extractStdout(element, "system-out"),
|
|
638
|
-
stderr: extractStdout(element, "system-err")
|
|
639
|
-
}]
|
|
640
|
-
};
|
|
641
|
-
currentSuite.tests ??= [];
|
|
642
|
-
currentSuite.tests.push(test);
|
|
517
|
+
return result;
|
|
643
518
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
};
|
|
668
|
-
for (const xml of xmls) {
|
|
669
|
-
const doc = parseXml(xml);
|
|
670
|
-
for (const element of doc.children)
|
|
671
|
-
await traverseJUnitReport(context, element);
|
|
672
|
-
}
|
|
673
|
-
return {
|
|
674
|
-
report: ReportUtils.normalizeReport(report),
|
|
675
|
-
attachments: Array.from(context.attachments.values())
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// src/cli/cmd-convert.ts
|
|
680
|
-
async function cmdConvert(junitPath, options) {
|
|
681
|
-
const fullPath = path3.resolve(junitPath);
|
|
682
|
-
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
683
|
-
console.error(`Error: path ${fullPath} is not accessible`);
|
|
684
|
-
process.exit(1);
|
|
685
|
-
}
|
|
686
|
-
const stat = await fs3.stat(fullPath);
|
|
687
|
-
let xmlContents = [];
|
|
688
|
-
if (stat.isFile()) {
|
|
689
|
-
const xmlContent = await fs3.readFile(fullPath, "utf-8");
|
|
690
|
-
xmlContents.push(xmlContent);
|
|
691
|
-
} else if (stat.isDirectory()) {
|
|
692
|
-
const xmlFiles = await findXmlFiles(fullPath);
|
|
693
|
-
if (xmlFiles.length === 0) {
|
|
694
|
-
console.error(`Error: No XML files found in directory ${fullPath}`);
|
|
695
|
-
process.exit(1);
|
|
696
|
-
}
|
|
697
|
-
console.log(`Found ${xmlFiles.length} XML files`);
|
|
698
|
-
for (const xmlFile of xmlFiles) {
|
|
699
|
-
const xmlContent = await fs3.readFile(xmlFile, "utf-8");
|
|
700
|
-
xmlContents.push(xmlContent);
|
|
701
|
-
}
|
|
702
|
-
} else {
|
|
703
|
-
console.error(`Error: ${fullPath} is neither a file nor a directory`);
|
|
704
|
-
process.exit(1);
|
|
705
|
-
}
|
|
706
|
-
let commitId;
|
|
707
|
-
if (options.commitId) {
|
|
708
|
-
commitId = options.commitId;
|
|
709
|
-
} else {
|
|
710
|
-
try {
|
|
711
|
-
const worktree = GitWorktree.create(process.cwd());
|
|
712
|
-
commitId = worktree.headCommitId();
|
|
713
|
-
} catch (e) {
|
|
714
|
-
console.error("Failed to get git commit info. Please provide --commit-id option.");
|
|
715
|
-
process.exit(1);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
const { report, attachments } = await parseJUnit(xmlContents, {
|
|
719
|
-
commitId,
|
|
720
|
-
defaultEnv: { name: options.envName },
|
|
721
|
-
runStartTimestamp: Date.now(),
|
|
722
|
-
runDuration: 0
|
|
723
|
-
});
|
|
724
|
-
if (options.flakinessProject)
|
|
725
|
-
report.flakinessProject = options.flakinessProject;
|
|
726
|
-
await writeReport(report, attachments, options.outputDir);
|
|
727
|
-
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
728
|
-
}
|
|
729
|
-
async function findXmlFiles(dir, result = []) {
|
|
730
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
731
|
-
for (const entry of entries) {
|
|
732
|
-
const fullPath = path3.join(dir, entry.name);
|
|
733
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
734
|
-
result.push(fullPath);
|
|
735
|
-
else if (entry.isDirectory())
|
|
736
|
-
await findXmlFiles(fullPath, result);
|
|
737
|
-
}
|
|
738
|
-
return result;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// ../node_modules/.pnpm/vlq@2.0.4/node_modules/vlq/src/index.js
|
|
742
|
-
var char_to_integer = {};
|
|
743
|
-
var integer_to_char = {};
|
|
744
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
|
|
745
|
-
char_to_integer[char] = i;
|
|
746
|
-
integer_to_char[i] = char;
|
|
747
|
-
});
|
|
748
|
-
function decode(string) {
|
|
749
|
-
let result = [];
|
|
750
|
-
let shift = 0;
|
|
751
|
-
let value = 0;
|
|
752
|
-
for (let i = 0; i < string.length; i += 1) {
|
|
753
|
-
let integer = char_to_integer[string[i]];
|
|
754
|
-
if (integer === void 0) {
|
|
755
|
-
throw new Error("Invalid character (" + string[i] + ")");
|
|
756
|
-
}
|
|
757
|
-
const has_continuation_bit = integer & 32;
|
|
758
|
-
integer &= 31;
|
|
759
|
-
value += integer << shift;
|
|
760
|
-
if (has_continuation_bit) {
|
|
761
|
-
shift += 5;
|
|
762
|
-
} else {
|
|
763
|
-
const should_negate = value & 1;
|
|
764
|
-
value >>>= 1;
|
|
765
|
-
if (should_negate) {
|
|
766
|
-
result.push(value === 0 ? -2147483648 : -value);
|
|
519
|
+
Ranges2.capAt = capAt;
|
|
520
|
+
function isIntersecting(ranges1, ranges2) {
|
|
521
|
+
if (!ranges1.length || !ranges2.length)
|
|
522
|
+
return false;
|
|
523
|
+
if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
|
|
524
|
+
return false;
|
|
525
|
+
let p1 = 0;
|
|
526
|
+
let p2 = 0;
|
|
527
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
528
|
+
const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
|
|
529
|
+
const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
|
|
530
|
+
if (a2 < b1) {
|
|
531
|
+
p1 += 2;
|
|
532
|
+
let offset2 = 1;
|
|
533
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
|
|
534
|
+
offset2 <<= 1;
|
|
535
|
+
p1 += offset2 >> 1 << 1;
|
|
536
|
+
} else if (b2 < a1) {
|
|
537
|
+
p2 += 2;
|
|
538
|
+
let offset2 = 1;
|
|
539
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
|
|
540
|
+
offset2 <<= 1;
|
|
541
|
+
p2 += offset2 >> 1 << 1;
|
|
767
542
|
} else {
|
|
768
|
-
|
|
543
|
+
return true;
|
|
769
544
|
}
|
|
770
|
-
value = shift = 0;
|
|
771
545
|
}
|
|
546
|
+
return false;
|
|
772
547
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
function encode_integer(num) {
|
|
786
|
-
let result = "";
|
|
787
|
-
if (num < 0) {
|
|
788
|
-
num = -num << 1 | 1;
|
|
789
|
-
} else {
|
|
790
|
-
num <<= 1;
|
|
791
|
-
}
|
|
792
|
-
do {
|
|
793
|
-
let clamped = num & 31;
|
|
794
|
-
num >>>= 5;
|
|
795
|
-
if (num > 0) {
|
|
796
|
-
clamped |= 32;
|
|
797
|
-
}
|
|
798
|
-
result += integer_to_char[clamped];
|
|
799
|
-
} while (num > 0);
|
|
800
|
-
return result;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// ../server/lib/common/heap.js
|
|
804
|
-
var Heap = class _Heap {
|
|
805
|
-
constructor(_cmp, elements = []) {
|
|
806
|
-
this._cmp = _cmp;
|
|
807
|
-
this._heap = elements.map(([element, score]) => ({ element, score }));
|
|
808
|
-
for (let idx = this._heap.length - 1; idx >= 0; --idx)
|
|
809
|
-
this._down(idx);
|
|
548
|
+
Ranges2.isIntersecting = isIntersecting;
|
|
549
|
+
function complement(r) {
|
|
550
|
+
if (r.length === 0)
|
|
551
|
+
return [-Infinity, Infinity];
|
|
552
|
+
const result = [];
|
|
553
|
+
if (!Object.is(r[0], -Infinity))
|
|
554
|
+
result.push(-Infinity, r[0] - 1);
|
|
555
|
+
for (let i = 1; i < r.length - 2; i += 2)
|
|
556
|
+
result.push(r[i] + 1, r[i + 1] - 1);
|
|
557
|
+
if (!Object.is(r[r.length - 1], Infinity))
|
|
558
|
+
result.push(r[r.length - 1] + 1, Infinity);
|
|
559
|
+
return result;
|
|
810
560
|
}
|
|
811
|
-
|
|
812
|
-
|
|
561
|
+
Ranges2.complement = complement;
|
|
562
|
+
function subtract(ranges1, ranges2) {
|
|
563
|
+
if (!ranges2.length)
|
|
564
|
+
return ranges1;
|
|
565
|
+
return intersect(ranges1, complement(ranges2));
|
|
813
566
|
}
|
|
814
|
-
|
|
815
|
-
|
|
567
|
+
Ranges2.subtract = subtract;
|
|
568
|
+
function singleRange(from2, to) {
|
|
569
|
+
return [from2, to];
|
|
816
570
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
break;
|
|
824
|
-
this._heap[idx] = this._heap[parentIdx];
|
|
825
|
-
idx = parentIdx;
|
|
826
|
-
}
|
|
827
|
-
this._heap[idx] = e;
|
|
571
|
+
Ranges2.singleRange = singleRange;
|
|
572
|
+
function unionAll(ranges) {
|
|
573
|
+
let result = Ranges2.EMPTY;
|
|
574
|
+
for (const r of ranges)
|
|
575
|
+
result = union(result, r);
|
|
576
|
+
return result;
|
|
828
577
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
578
|
+
Ranges2.unionAll = unionAll;
|
|
579
|
+
function unionAll_2(rangesIterable) {
|
|
580
|
+
const ranges = Array.isArray(rangesIterable) ? rangesIterable : Array.from(rangesIterable);
|
|
581
|
+
if (ranges.length === 0)
|
|
582
|
+
return [];
|
|
583
|
+
if (ranges.length === 1)
|
|
584
|
+
return ranges[0];
|
|
585
|
+
if (ranges.length === 2)
|
|
586
|
+
return union(ranges[0], ranges[1]);
|
|
587
|
+
const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
|
|
588
|
+
const result = [];
|
|
589
|
+
let last;
|
|
590
|
+
for (const interval of seq.seek(0)) {
|
|
591
|
+
if (!last || last[1] + 1 < interval[0]) {
|
|
592
|
+
result.push(interval);
|
|
593
|
+
last = interval;
|
|
594
|
+
continue;
|
|
844
595
|
}
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
this._heap[idx] = this._heap[smallestIdx];
|
|
848
|
-
idx = smallestIdx;
|
|
596
|
+
if (last[1] < interval[1])
|
|
597
|
+
last[1] = interval[1];
|
|
849
598
|
}
|
|
850
|
-
|
|
599
|
+
return result.flat();
|
|
851
600
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
|
|
601
|
+
Ranges2.unionAll_2 = unionAll_2;
|
|
602
|
+
function intersectAll(ranges) {
|
|
603
|
+
if (!ranges.length)
|
|
604
|
+
return Ranges2.EMPTY;
|
|
605
|
+
let result = Ranges2.FULL;
|
|
606
|
+
for (const range of ranges)
|
|
607
|
+
result = Ranges2.intersect(result, range);
|
|
608
|
+
return result;
|
|
861
609
|
}
|
|
862
|
-
|
|
863
|
-
|
|
610
|
+
Ranges2.intersectAll = intersectAll;
|
|
611
|
+
function domain(ranges) {
|
|
612
|
+
if (!ranges.length)
|
|
864
613
|
return void 0;
|
|
865
|
-
|
|
866
|
-
const last = this._heap.pop();
|
|
867
|
-
if (!this._heap.length)
|
|
868
|
-
return [entry.element, entry.score];
|
|
869
|
-
this._heap[0] = last;
|
|
870
|
-
this._down(0);
|
|
871
|
-
return [entry.element, entry.score];
|
|
614
|
+
return { min: ranges[0], max: ranges[ranges.length - 1] };
|
|
872
615
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
616
|
+
Ranges2.domain = domain;
|
|
617
|
+
function union(ranges1, ranges2) {
|
|
618
|
+
if (!ranges1.length)
|
|
619
|
+
return ranges2;
|
|
620
|
+
if (!ranges2.length)
|
|
621
|
+
return ranges1;
|
|
622
|
+
if (ranges2[0] < ranges1[0])
|
|
623
|
+
[ranges1, ranges2] = [ranges2, ranges1];
|
|
624
|
+
const r = [ranges1[0], ranges1[1]];
|
|
625
|
+
let p1 = 2, p2 = 0;
|
|
626
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
627
|
+
if (ranges1[p1] <= ranges2[p2]) {
|
|
628
|
+
if (r[r.length - 1] + 1 < ranges1[p1]) {
|
|
629
|
+
r.push(ranges1[p1], ranges1[p1 + 1]);
|
|
630
|
+
p1 += 2;
|
|
631
|
+
} else if (r[r.length - 1] < ranges1[p1 + 1]) {
|
|
632
|
+
r[r.length - 1] = ranges1[p1 + 1];
|
|
633
|
+
p1 += 2;
|
|
634
|
+
} else {
|
|
635
|
+
p1 += 2;
|
|
636
|
+
let offset2 = 1;
|
|
637
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
638
|
+
offset2 <<= 1;
|
|
639
|
+
p1 += offset2 >> 1 << 1;
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
if (r[r.length - 1] + 1 < ranges2[p2]) {
|
|
643
|
+
r.push(ranges2[p2], ranges2[p2 + 1]);
|
|
644
|
+
p2 += 2;
|
|
645
|
+
} else if (r[r.length - 1] < ranges2[p2 + 1]) {
|
|
646
|
+
r[r.length - 1] = ranges2[p2 + 1];
|
|
647
|
+
p2 += 2;
|
|
648
|
+
} else {
|
|
649
|
+
p2 += 2;
|
|
650
|
+
let offset2 = 1;
|
|
651
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
|
|
652
|
+
offset2 <<= 1;
|
|
653
|
+
p2 += offset2 >> 1 << 1;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
while (p1 < ranges1.length - 1) {
|
|
658
|
+
if (r[r.length - 1] + 1 < ranges1[p1]) {
|
|
659
|
+
r.push(ranges1[p1], ranges1[p1 + 1]);
|
|
660
|
+
p1 += 2;
|
|
661
|
+
} else if (r[r.length - 1] < ranges1[p1 + 1]) {
|
|
662
|
+
r[r.length - 1] = ranges1[p1 + 1];
|
|
663
|
+
p1 += 2;
|
|
664
|
+
} else {
|
|
665
|
+
p1 += 2;
|
|
666
|
+
let offset2 = 1;
|
|
667
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
668
|
+
offset2 <<= 1;
|
|
669
|
+
p1 += offset2 >> 1 << 1;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
while (p2 < ranges2.length - 1) {
|
|
673
|
+
if (r[r.length - 1] + 1 < ranges2[p2]) {
|
|
674
|
+
r.push(ranges2[p2], ranges2[p2 + 1]);
|
|
675
|
+
p2 += 2;
|
|
676
|
+
} else if (r[r.length - 1] < ranges2[p2 + 1]) {
|
|
677
|
+
r[r.length - 1] = ranges2[p2 + 1];
|
|
678
|
+
p2 += 2;
|
|
679
|
+
} else {
|
|
680
|
+
p2 += 2;
|
|
681
|
+
let offset2 = 1;
|
|
682
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
|
|
683
|
+
offset2 <<= 1;
|
|
684
|
+
p2 += offset2 >> 1 << 1;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return r;
|
|
880
688
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
);
|
|
689
|
+
Ranges2.union = union;
|
|
690
|
+
function intervalSequence(ranges) {
|
|
691
|
+
return new Sequence(function(idx) {
|
|
692
|
+
return {
|
|
693
|
+
next() {
|
|
694
|
+
if (idx * 2 >= ranges.length)
|
|
695
|
+
return { done: true, value: void 0 };
|
|
696
|
+
const value = [ranges[idx * 2], ranges[idx * 2 + 1]];
|
|
697
|
+
++idx;
|
|
698
|
+
return { done: false, value };
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}, ranges.length >>> 1);
|
|
894
702
|
}
|
|
895
|
-
|
|
896
|
-
|
|
703
|
+
Ranges2.intervalSequence = intervalSequence;
|
|
704
|
+
function sequence(ranges) {
|
|
897
705
|
let length = 0;
|
|
898
|
-
|
|
899
|
-
|
|
706
|
+
const leftsums = [];
|
|
707
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
708
|
+
length += ranges[i + 1] - ranges[i] + 1;
|
|
900
709
|
leftsums.push(length);
|
|
901
710
|
}
|
|
902
|
-
return new
|
|
711
|
+
return new Sequence(
|
|
903
712
|
function(fromIdx) {
|
|
904
713
|
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
let
|
|
714
|
+
const idx = Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
|
|
715
|
+
const intervals = Ranges2.intervalSequence(ranges);
|
|
716
|
+
const it = intervals.seek(idx);
|
|
717
|
+
const firstInterval = it.next();
|
|
718
|
+
if (firstInterval.done)
|
|
719
|
+
return { next: () => firstInterval };
|
|
720
|
+
let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
|
|
721
|
+
let to = firstInterval.value[1];
|
|
912
722
|
return {
|
|
913
723
|
next() {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
724
|
+
if (from2 > to) {
|
|
725
|
+
const interval = it.next();
|
|
726
|
+
if (interval.done)
|
|
727
|
+
return { done: true, value: void 0 };
|
|
728
|
+
from2 = interval.value[0];
|
|
729
|
+
to = interval.value[1];
|
|
918
730
|
}
|
|
919
|
-
return
|
|
731
|
+
return { done: false, value: from2++ };
|
|
920
732
|
}
|
|
921
733
|
};
|
|
922
734
|
},
|
|
923
735
|
length
|
|
924
736
|
);
|
|
925
737
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
const heap = new Heap(cmp, entries);
|
|
941
|
-
return {
|
|
942
|
-
next() {
|
|
943
|
-
if (!heap.size)
|
|
944
|
-
return { done: true, value: void 0 };
|
|
945
|
-
++fromIdx;
|
|
946
|
-
const [it, e] = heap.popEntry();
|
|
947
|
-
const itval = it.next();
|
|
948
|
-
if (!itval.done)
|
|
949
|
-
heap.push(it, itval.value);
|
|
950
|
-
return { done: false, value: e };
|
|
951
|
-
}
|
|
952
|
-
};
|
|
953
|
-
},
|
|
954
|
-
length
|
|
955
|
-
);
|
|
738
|
+
Ranges2.sequence = sequence;
|
|
739
|
+
})(Ranges || (Ranges = {}));
|
|
740
|
+
|
|
741
|
+
// ../server/lib/common/stats/testOutcomes.js
|
|
742
|
+
var TestOutcomes;
|
|
743
|
+
((TestOutcomes2) => {
|
|
744
|
+
TestOutcomes2.EMPTY_TESTS = [];
|
|
745
|
+
function regressifyInplace(outcomes, previouslyUnhealthyTests) {
|
|
746
|
+
const newRegressions = Ranges.subtract(outcomes.unexpected, previouslyUnhealthyTests);
|
|
747
|
+
if (newRegressions.length) {
|
|
748
|
+
outcomes.regressed = Ranges.union(outcomes.regressed, newRegressions);
|
|
749
|
+
outcomes.unexpected = Ranges.intersect(outcomes.unexpected, previouslyUnhealthyTests);
|
|
750
|
+
}
|
|
956
751
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
752
|
+
TestOutcomes2.regressifyInplace = regressifyInplace;
|
|
753
|
+
function regressify(outcomes, previouslyUnhealthyTests) {
|
|
754
|
+
const newRegressions = Ranges.subtract(outcomes.unexpected, previouslyUnhealthyTests);
|
|
755
|
+
return {
|
|
756
|
+
regressed: Ranges.union(outcomes.regressed, newRegressions),
|
|
757
|
+
unexpected: Ranges.intersect(outcomes.unexpected, previouslyUnhealthyTests),
|
|
758
|
+
expected: outcomes.expected,
|
|
759
|
+
flaked: outcomes.flaked,
|
|
760
|
+
skipped: outcomes.skipped
|
|
761
|
+
};
|
|
963
762
|
}
|
|
964
|
-
|
|
965
|
-
|
|
763
|
+
TestOutcomes2.regressify = regressify;
|
|
764
|
+
function includes(outcomes, testIdx) {
|
|
765
|
+
return Ranges.includes(outcomes.regressed, testIdx) || Ranges.includes(outcomes.unexpected, testIdx) || Ranges.includes(outcomes.flaked, testIdx) || Ranges.includes(outcomes.expected, testIdx) || Ranges.includes(outcomes.skipped, testIdx);
|
|
966
766
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
return
|
|
970
|
-
function(idx) {
|
|
971
|
-
const it = originalSeek(idx);
|
|
972
|
-
return {
|
|
973
|
-
next() {
|
|
974
|
-
const next = it.next();
|
|
975
|
-
if (next.done)
|
|
976
|
-
return next;
|
|
977
|
-
return { done: false, value: mapper(next.value) };
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
},
|
|
981
|
-
this.length
|
|
982
|
-
);
|
|
767
|
+
TestOutcomes2.includes = includes;
|
|
768
|
+
function unhealthyTests(outcomes) {
|
|
769
|
+
return Ranges.unionAll([outcomes.regressed, outcomes.flaked, outcomes.unexpected]);
|
|
983
770
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
while (lo < hi) {
|
|
988
|
-
const mid = lo + hi >>> 1;
|
|
989
|
-
if (predicate(this.get(mid)))
|
|
990
|
-
lo = mid + 1;
|
|
991
|
-
else
|
|
992
|
-
hi = mid;
|
|
993
|
-
}
|
|
994
|
-
return lo;
|
|
771
|
+
TestOutcomes2.unhealthyTests = unhealthyTests;
|
|
772
|
+
function unhealthyTestsForAggregatedRun(commitOutcomes) {
|
|
773
|
+
return Ranges.complement(commitOutcomes.regressed);
|
|
995
774
|
}
|
|
996
|
-
|
|
997
|
-
function
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
entries.push([seq, seq.get(offset + x - 1)]);
|
|
1006
|
-
}
|
|
1007
|
-
const heap = new Heap(cmp, entries);
|
|
1008
|
-
while (heap.size && k > 0 && (x === 1 || k >= x * t)) {
|
|
1009
|
-
k -= x;
|
|
1010
|
-
const [seq] = heap.popEntry();
|
|
1011
|
-
const offset = offsets.get(seq) + x;
|
|
1012
|
-
if (offset === seq.length)
|
|
1013
|
-
offsets.delete(seq);
|
|
1014
|
-
else
|
|
1015
|
-
offsets.set(seq, offset);
|
|
1016
|
-
if (offset + x <= seq.length)
|
|
1017
|
-
heap.push(seq, seq.get(offset + x - 1));
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
return sequences.map((seq) => offsets.get(seq) ?? seq.length);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// ../server/lib/common/ranges.js
|
|
1024
|
-
var Ranges;
|
|
1025
|
-
((Ranges2) => {
|
|
1026
|
-
Ranges2.EMPTY = [];
|
|
1027
|
-
Ranges2.FULL = [-Infinity, Infinity];
|
|
1028
|
-
function isFull(ranges) {
|
|
1029
|
-
return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
|
|
775
|
+
TestOutcomes2.unhealthyTestsForAggregatedRun = unhealthyTestsForAggregatedRun;
|
|
776
|
+
function newOutcomeCounts(outcomes) {
|
|
777
|
+
return {
|
|
778
|
+
regressed: outcomes ? Ranges.cardinality(outcomes.regressed) : 0,
|
|
779
|
+
expected: outcomes ? Ranges.cardinality(outcomes.expected) : 0,
|
|
780
|
+
unexpected: outcomes ? Ranges.cardinality(outcomes.unexpected) : 0,
|
|
781
|
+
skipped: outcomes ? Ranges.cardinality(outcomes.skipped) : 0,
|
|
782
|
+
flaked: outcomes ? Ranges.cardinality(outcomes.flaked) : 0
|
|
783
|
+
};
|
|
1030
784
|
}
|
|
1031
|
-
|
|
1032
|
-
function
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
let last = ranges[0] - 1;
|
|
1039
|
-
prepared.push(last);
|
|
1040
|
-
for (let i = 0; i < ranges.length; i += 2) {
|
|
1041
|
-
if (ranges[i] === ranges[i + 1]) {
|
|
1042
|
-
prepared.push(-(ranges[i] - last));
|
|
1043
|
-
} else {
|
|
1044
|
-
prepared.push(ranges[i] - last);
|
|
1045
|
-
prepared.push(ranges[i + 1] - ranges[i]);
|
|
1046
|
-
}
|
|
1047
|
-
last = ranges[i + 1];
|
|
1048
|
-
}
|
|
1049
|
-
return encode(prepared);
|
|
785
|
+
TestOutcomes2.newOutcomeCounts = newOutcomeCounts;
|
|
786
|
+
function accumulateOutcomeCounts(acc, other) {
|
|
787
|
+
acc.regressed += other.regressed;
|
|
788
|
+
acc.expected += other.expected;
|
|
789
|
+
acc.unexpected += other.unexpected;
|
|
790
|
+
acc.skipped += other.skipped;
|
|
791
|
+
acc.flaked += other.flaked;
|
|
1050
792
|
}
|
|
1051
|
-
|
|
1052
|
-
function
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
result.push(-prepared[i] + last);
|
|
1061
|
-
result.push(-prepared[i] + last);
|
|
1062
|
-
last -= prepared[i];
|
|
1063
|
-
} else {
|
|
1064
|
-
result.push(prepared[i] + last);
|
|
1065
|
-
last += prepared[i];
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
return result;
|
|
793
|
+
TestOutcomes2.accumulateOutcomeCounts = accumulateOutcomeCounts;
|
|
794
|
+
function cloneOutcomes(other) {
|
|
795
|
+
return {
|
|
796
|
+
regressed: other.regressed,
|
|
797
|
+
expected: other.expected,
|
|
798
|
+
unexpected: other.unexpected,
|
|
799
|
+
skipped: other.skipped,
|
|
800
|
+
flaked: other.flaked
|
|
801
|
+
};
|
|
1069
802
|
}
|
|
1070
|
-
|
|
1071
|
-
function
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
if (!tokens.length)
|
|
1080
|
-
return `[]`;
|
|
1081
|
-
return `[ ` + tokens.join(", ") + ` ]`;
|
|
803
|
+
TestOutcomes2.cloneOutcomes = cloneOutcomes;
|
|
804
|
+
function newOutcomes() {
|
|
805
|
+
return {
|
|
806
|
+
regressed: Ranges.EMPTY,
|
|
807
|
+
expected: Ranges.EMPTY,
|
|
808
|
+
unexpected: Ranges.EMPTY,
|
|
809
|
+
skipped: Ranges.EMPTY,
|
|
810
|
+
flaked: Ranges.EMPTY
|
|
811
|
+
};
|
|
1082
812
|
}
|
|
1083
|
-
|
|
1084
|
-
function
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (
|
|
1090
|
-
return
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
813
|
+
TestOutcomes2.newOutcomes = newOutcomes;
|
|
814
|
+
function getTestOutcome(outcomes, testIndex) {
|
|
815
|
+
if (Ranges.includes(outcomes.regressed, testIndex))
|
|
816
|
+
return "regressed";
|
|
817
|
+
if (Ranges.includes(outcomes.unexpected, testIndex))
|
|
818
|
+
return "unexpected";
|
|
819
|
+
if (Ranges.includes(outcomes.expected, testIndex))
|
|
820
|
+
return "expected";
|
|
821
|
+
if (Ranges.includes(outcomes.skipped, testIndex))
|
|
822
|
+
return "skipped";
|
|
823
|
+
if (Ranges.includes(outcomes.flaked, testIndex))
|
|
824
|
+
return "flaked";
|
|
825
|
+
}
|
|
826
|
+
TestOutcomes2.getTestOutcome = getTestOutcome;
|
|
827
|
+
function normalizeInplace(outcomes) {
|
|
828
|
+
let union2 = outcomes.regressed;
|
|
829
|
+
outcomes.unexpected = Ranges.subtract(outcomes.unexpected, union2);
|
|
830
|
+
union2 = Ranges.union(union2, outcomes.unexpected);
|
|
831
|
+
outcomes.flaked = Ranges.subtract(outcomes.flaked, union2);
|
|
832
|
+
union2 = Ranges.union(union2, outcomes.flaked);
|
|
833
|
+
outcomes.expected = Ranges.subtract(outcomes.expected, union2);
|
|
834
|
+
union2 = Ranges.union(union2, outcomes.expected);
|
|
835
|
+
outcomes.skipped = Ranges.subtract(outcomes.skipped, union2);
|
|
836
|
+
}
|
|
837
|
+
TestOutcomes2.normalizeInplace = normalizeInplace;
|
|
838
|
+
function addOverlapAsFlakyInplace(outcomes) {
|
|
839
|
+
const allExpected = Ranges.unionAll([outcomes.flaked, outcomes.expected, outcomes.skipped]);
|
|
840
|
+
const allUnexpected = Ranges.union(outcomes.unexpected, outcomes.regressed);
|
|
841
|
+
const unexpectedToFlaked = Ranges.intersect(allExpected, allUnexpected);
|
|
842
|
+
outcomes.regressed = Ranges.subtract(outcomes.regressed, unexpectedToFlaked);
|
|
843
|
+
outcomes.unexpected = Ranges.subtract(outcomes.unexpected, unexpectedToFlaked);
|
|
844
|
+
outcomes.expected = Ranges.subtract(outcomes.expected, unexpectedToFlaked);
|
|
845
|
+
outcomes.skipped = Ranges.subtract(outcomes.skipped, unexpectedToFlaked);
|
|
846
|
+
outcomes.flaked = Ranges.union(outcomes.flaked, unexpectedToFlaked);
|
|
847
|
+
}
|
|
848
|
+
TestOutcomes2.addOverlapAsFlakyInplace = addOverlapAsFlakyInplace;
|
|
849
|
+
function unionInplace(acc, other) {
|
|
850
|
+
acc.regressed = Ranges.union(acc.regressed, other.regressed);
|
|
851
|
+
acc.expected = Ranges.union(acc.expected, other.expected);
|
|
852
|
+
acc.unexpected = Ranges.union(acc.unexpected, other.unexpected);
|
|
853
|
+
acc.flaked = Ranges.union(acc.flaked, other.flaked);
|
|
854
|
+
acc.skipped = Ranges.union(acc.skipped, other.skipped);
|
|
855
|
+
return acc;
|
|
856
|
+
}
|
|
857
|
+
TestOutcomes2.unionInplace = unionInplace;
|
|
858
|
+
function union(one, another) {
|
|
859
|
+
return {
|
|
860
|
+
regressed: Ranges.union(one.regressed, another.regressed),
|
|
861
|
+
expected: Ranges.union(one.expected, another.expected),
|
|
862
|
+
unexpected: Ranges.union(one.unexpected, another.unexpected),
|
|
863
|
+
flaked: Ranges.union(one.flaked, another.flaked),
|
|
864
|
+
skipped: Ranges.union(one.skipped, another.skipped)
|
|
865
|
+
};
|
|
1098
866
|
}
|
|
1099
|
-
|
|
1100
|
-
function
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
yield j;
|
|
1106
|
-
}
|
|
867
|
+
TestOutcomes2.union = union;
|
|
868
|
+
function unionAll(outcomes) {
|
|
869
|
+
const acc = newOutcomes();
|
|
870
|
+
for (const outcome of outcomes)
|
|
871
|
+
unionInplace(acc, outcome);
|
|
872
|
+
return acc;
|
|
1107
873
|
}
|
|
1108
|
-
|
|
1109
|
-
function
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
return list;
|
|
874
|
+
TestOutcomes2.unionAll = unionAll;
|
|
875
|
+
function intersectRanges(outcomes, ranges) {
|
|
876
|
+
return {
|
|
877
|
+
regressed: Ranges.intersect(outcomes.regressed, ranges),
|
|
878
|
+
unexpected: Ranges.intersect(outcomes.unexpected, ranges),
|
|
879
|
+
expected: Ranges.intersect(outcomes.expected, ranges),
|
|
880
|
+
skipped: Ranges.intersect(outcomes.skipped, ranges),
|
|
881
|
+
flaked: Ranges.intersect(outcomes.flaked, ranges)
|
|
882
|
+
};
|
|
1118
883
|
}
|
|
1119
|
-
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
return result;
|
|
884
|
+
TestOutcomes2.intersectRanges = intersectRanges;
|
|
885
|
+
function capAt(outcomes, cutoffTestIndex) {
|
|
886
|
+
return {
|
|
887
|
+
regressed: Ranges.capAt(outcomes.regressed, cutoffTestIndex),
|
|
888
|
+
unexpected: Ranges.capAt(outcomes.unexpected, cutoffTestIndex),
|
|
889
|
+
expected: Ranges.capAt(outcomes.expected, cutoffTestIndex),
|
|
890
|
+
skipped: Ranges.capAt(outcomes.skipped, cutoffTestIndex),
|
|
891
|
+
flaked: Ranges.capAt(outcomes.flaked, cutoffTestIndex)
|
|
892
|
+
};
|
|
1130
893
|
}
|
|
1131
|
-
|
|
1132
|
-
function
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
894
|
+
TestOutcomes2.capAt = capAt;
|
|
895
|
+
function intersectRangesInplace(outcomes, ranges) {
|
|
896
|
+
outcomes.regressed = Ranges.intersect(outcomes.regressed, ranges);
|
|
897
|
+
outcomes.unexpected = Ranges.intersect(outcomes.unexpected, ranges);
|
|
898
|
+
outcomes.expected = Ranges.intersect(outcomes.expected, ranges);
|
|
899
|
+
outcomes.skipped = Ranges.intersect(outcomes.skipped, ranges);
|
|
900
|
+
outcomes.flaked = Ranges.intersect(outcomes.flaked, ranges);
|
|
901
|
+
}
|
|
902
|
+
TestOutcomes2.intersectRangesInplace = intersectRangesInplace;
|
|
903
|
+
function subtractRangesInplace(outcomes, ranges) {
|
|
904
|
+
outcomes.regressed = Ranges.subtract(outcomes.regressed, ranges);
|
|
905
|
+
outcomes.unexpected = Ranges.subtract(outcomes.unexpected, ranges);
|
|
906
|
+
outcomes.expected = Ranges.subtract(outcomes.expected, ranges);
|
|
907
|
+
outcomes.skipped = Ranges.subtract(outcomes.skipped, ranges);
|
|
908
|
+
outcomes.flaked = Ranges.subtract(outcomes.flaked, ranges);
|
|
909
|
+
}
|
|
910
|
+
TestOutcomes2.subtractRangesInplace = subtractRangesInplace;
|
|
911
|
+
function subtractRanges(outcomes, ranges) {
|
|
912
|
+
return {
|
|
913
|
+
regressed: Ranges.subtract(outcomes.regressed, ranges),
|
|
914
|
+
unexpected: Ranges.subtract(outcomes.unexpected, ranges),
|
|
915
|
+
expected: Ranges.subtract(outcomes.expected, ranges),
|
|
916
|
+
skipped: Ranges.subtract(outcomes.skipped, ranges),
|
|
917
|
+
flaked: Ranges.subtract(outcomes.flaked, ranges)
|
|
918
|
+
};
|
|
1140
919
|
}
|
|
1141
|
-
|
|
1142
|
-
function
|
|
1143
|
-
return
|
|
920
|
+
TestOutcomes2.subtractRanges = subtractRanges;
|
|
921
|
+
function intersectWithOutcomes(a, b) {
|
|
922
|
+
return {
|
|
923
|
+
regressed: Ranges.intersect(a.regressed, b.regressed),
|
|
924
|
+
unexpected: Ranges.intersect(a.unexpected, b.unexpected),
|
|
925
|
+
expected: Ranges.intersect(a.expected, b.expected),
|
|
926
|
+
skipped: Ranges.intersect(a.skipped, b.skipped),
|
|
927
|
+
flaked: Ranges.intersect(a.flaked, b.flaked)
|
|
928
|
+
};
|
|
1144
929
|
}
|
|
1145
|
-
|
|
1146
|
-
function
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
return ranges;
|
|
930
|
+
TestOutcomes2.intersectWithOutcomes = intersectWithOutcomes;
|
|
931
|
+
function subtractOutcomes(a, b) {
|
|
932
|
+
return {
|
|
933
|
+
regressed: Ranges.subtract(a.regressed, b.regressed),
|
|
934
|
+
unexpected: Ranges.subtract(a.unexpected, b.unexpected),
|
|
935
|
+
expected: Ranges.subtract(a.expected, b.expected),
|
|
936
|
+
skipped: Ranges.subtract(a.skipped, b.skipped),
|
|
937
|
+
flaked: Ranges.subtract(a.flaked, b.flaked)
|
|
938
|
+
};
|
|
1156
939
|
}
|
|
1157
|
-
|
|
1158
|
-
function
|
|
1159
|
-
return
|
|
940
|
+
TestOutcomes2.subtractOutcomes = subtractOutcomes;
|
|
941
|
+
function flatten(outcomes) {
|
|
942
|
+
return Ranges.unionAll([outcomes.expected, outcomes.flaked, outcomes.skipped, outcomes.unexpected, outcomes.regressed]);
|
|
1160
943
|
}
|
|
1161
|
-
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
return false;
|
|
1165
|
-
if (e < ranges[0] || ranges[ranges.length - 1] < e)
|
|
1166
|
-
return false;
|
|
1167
|
-
if (ranges.length < 17) {
|
|
1168
|
-
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
1169
|
-
if (ranges[i] <= e && e <= ranges[i + 1])
|
|
1170
|
-
return true;
|
|
1171
|
-
}
|
|
1172
|
-
return false;
|
|
1173
|
-
}
|
|
1174
|
-
let lo = 0, hi = ranges.length;
|
|
1175
|
-
while (lo < hi) {
|
|
1176
|
-
const mid = lo + hi >>> 1;
|
|
1177
|
-
if (ranges[mid] === e)
|
|
1178
|
-
return true;
|
|
1179
|
-
if (ranges[mid] < e)
|
|
1180
|
-
lo = mid + 1;
|
|
1181
|
-
else
|
|
1182
|
-
hi = mid;
|
|
1183
|
-
}
|
|
1184
|
-
return (lo & 1) !== 0;
|
|
944
|
+
TestOutcomes2.flatten = flatten;
|
|
945
|
+
function isEmptyOutcomes(x) {
|
|
946
|
+
return x.regressed.length === 0 && x.expected.length === 0 && x.flaked.length === 0 && x.skipped.length === 0 && x.unexpected.length === 0;
|
|
1185
947
|
}
|
|
1186
|
-
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
return Infinity;
|
|
1190
|
-
let sum = 0;
|
|
1191
|
-
for (let i = 0; i < ranges.length - 1; i += 2)
|
|
1192
|
-
sum += ranges[i + 1] - ranges[i] + 1;
|
|
1193
|
-
return sum;
|
|
948
|
+
TestOutcomes2.isEmptyOutcomes = isEmptyOutcomes;
|
|
949
|
+
function isEmptyOutcomeCounts(x) {
|
|
950
|
+
return x.regressed === 0 && x.expected === 0 && x.flaked === 0 && x.skipped === 0 && x.unexpected === 0;
|
|
1194
951
|
}
|
|
1195
|
-
|
|
1196
|
-
function
|
|
1197
|
-
return
|
|
952
|
+
TestOutcomes2.isEmptyOutcomeCounts = isEmptyOutcomeCounts;
|
|
953
|
+
function sumAllCounts(x) {
|
|
954
|
+
return x.regressed + x.expected + x.flaked + x.skipped + x.unexpected;
|
|
1198
955
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
956
|
+
TestOutcomes2.sumAllCounts = sumAllCounts;
|
|
957
|
+
})(TestOutcomes || (TestOutcomes = {}));
|
|
958
|
+
|
|
959
|
+
// ../server/lib/common/wireTypes.js
|
|
960
|
+
var WireTypes;
|
|
961
|
+
((WireTypes2) => {
|
|
962
|
+
WireTypes2.SORT_AXES = {
|
|
963
|
+
tests: [
|
|
964
|
+
"name",
|
|
965
|
+
"timeline_name",
|
|
966
|
+
"duration",
|
|
967
|
+
"outcome",
|
|
968
|
+
"duration_trend",
|
|
969
|
+
"flip_rate"
|
|
970
|
+
],
|
|
971
|
+
timelines: [
|
|
972
|
+
"name",
|
|
973
|
+
"outcome",
|
|
974
|
+
"total_time",
|
|
975
|
+
"unexpected",
|
|
976
|
+
"expected",
|
|
977
|
+
"skipped",
|
|
978
|
+
"flaked",
|
|
979
|
+
"regressed"
|
|
980
|
+
],
|
|
981
|
+
errors: [
|
|
982
|
+
"name",
|
|
983
|
+
"timelines",
|
|
984
|
+
"tests"
|
|
985
|
+
],
|
|
986
|
+
annotations: [
|
|
987
|
+
"name",
|
|
988
|
+
"timelines",
|
|
989
|
+
"tests"
|
|
990
|
+
],
|
|
991
|
+
tags: [
|
|
992
|
+
"name",
|
|
993
|
+
"timelines",
|
|
994
|
+
"tests"
|
|
995
|
+
],
|
|
996
|
+
commits: [
|
|
997
|
+
"chrono",
|
|
998
|
+
"outcome",
|
|
999
|
+
"unexpected",
|
|
1000
|
+
"expected",
|
|
1001
|
+
"skipped",
|
|
1002
|
+
"flaked",
|
|
1003
|
+
"regressed"
|
|
1004
|
+
],
|
|
1005
|
+
runs: [
|
|
1006
|
+
"id",
|
|
1007
|
+
"outcome",
|
|
1008
|
+
"chrono",
|
|
1009
|
+
"unexpected",
|
|
1010
|
+
"expected",
|
|
1011
|
+
"skipped",
|
|
1012
|
+
"flaked",
|
|
1013
|
+
"factual_duration",
|
|
1014
|
+
"regressed"
|
|
1015
|
+
],
|
|
1016
|
+
pullRequests: [
|
|
1017
|
+
"created"
|
|
1018
|
+
]
|
|
1019
|
+
};
|
|
1020
|
+
WireTypes2.EMPTY_OUTCOMES = TestOutcomes.newOutcomeCounts();
|
|
1021
|
+
})(WireTypes || (WireTypes = {}));
|
|
1022
|
+
|
|
1023
|
+
// src/cli/cli.ts
|
|
1024
|
+
import { Command, Option } from "commander";
|
|
1025
|
+
import debug2 from "debug";
|
|
1026
|
+
import path7 from "path";
|
|
1027
|
+
|
|
1028
|
+
// ../package.json
|
|
1029
|
+
var package_default = {
|
|
1030
|
+
name: "flakiness",
|
|
1031
|
+
version: "0.214.0",
|
|
1032
|
+
type: "module",
|
|
1033
|
+
private: true,
|
|
1034
|
+
scripts: {
|
|
1035
|
+
minor: "./version.mjs minor",
|
|
1036
|
+
patch: "./version.mjs patch",
|
|
1037
|
+
dev: "pnpm kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./github_webhooks.mts",
|
|
1038
|
+
"dev+billing": "pnpm kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe_webhooks.mts ./github_webhooks.mts",
|
|
1039
|
+
prod: "pnpm kubik --env-file=.env.prodlocal -w ./cli/build.mts ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts ./github_webhooks.mts",
|
|
1040
|
+
build: "pnpm kubik $(find . -name build.mts)",
|
|
1041
|
+
perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
|
|
1042
|
+
},
|
|
1043
|
+
engines: {
|
|
1044
|
+
node: ">=24"
|
|
1045
|
+
},
|
|
1046
|
+
author: "Degu Labs, Inc",
|
|
1047
|
+
license: "Fair Source 100",
|
|
1048
|
+
devDependencies: {
|
|
1049
|
+
"@flakiness/playwright": "catalog:",
|
|
1050
|
+
"@playwright/test": "catalog:",
|
|
1051
|
+
"@types/node": "^22.19.3",
|
|
1052
|
+
esbuild: "^0.27.2",
|
|
1053
|
+
glob: "catalog:",
|
|
1054
|
+
kubik: "^0.24.0",
|
|
1055
|
+
"smee-client": "^5.0.0",
|
|
1056
|
+
tsx: "^4.21.0",
|
|
1057
|
+
typescript: "^5.9.3"
|
|
1236
1058
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/userSession.ts
|
|
1062
|
+
import fs from "fs/promises";
|
|
1063
|
+
import os from "os";
|
|
1064
|
+
import path from "path";
|
|
1065
|
+
|
|
1066
|
+
// ../shared/lib/common/typedHttp.js
|
|
1067
|
+
var TypedHTTP;
|
|
1068
|
+
((TypedHTTP2) => {
|
|
1069
|
+
TypedHTTP2.StatusCodes = {
|
|
1070
|
+
Informational: {
|
|
1071
|
+
CONTINUE: 100,
|
|
1072
|
+
SWITCHING_PROTOCOLS: 101,
|
|
1073
|
+
PROCESSING: 102,
|
|
1074
|
+
EARLY_HINTS: 103
|
|
1075
|
+
},
|
|
1076
|
+
Success: {
|
|
1077
|
+
OK: 200,
|
|
1078
|
+
CREATED: 201,
|
|
1079
|
+
ACCEPTED: 202,
|
|
1080
|
+
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
1081
|
+
NO_CONTENT: 204,
|
|
1082
|
+
RESET_CONTENT: 205,
|
|
1083
|
+
PARTIAL_CONTENT: 206,
|
|
1084
|
+
MULTI_STATUS: 207
|
|
1085
|
+
},
|
|
1086
|
+
Redirection: {
|
|
1087
|
+
MULTIPLE_CHOICES: 300,
|
|
1088
|
+
MOVED_PERMANENTLY: 301,
|
|
1089
|
+
MOVED_TEMPORARILY: 302,
|
|
1090
|
+
SEE_OTHER: 303,
|
|
1091
|
+
NOT_MODIFIED: 304,
|
|
1092
|
+
USE_PROXY: 305,
|
|
1093
|
+
TEMPORARY_REDIRECT: 307,
|
|
1094
|
+
PERMANENT_REDIRECT: 308
|
|
1095
|
+
},
|
|
1096
|
+
ClientErrors: {
|
|
1097
|
+
BAD_REQUEST: 400,
|
|
1098
|
+
UNAUTHORIZED: 401,
|
|
1099
|
+
PAYMENT_REQUIRED: 402,
|
|
1100
|
+
FORBIDDEN: 403,
|
|
1101
|
+
NOT_FOUND: 404,
|
|
1102
|
+
METHOD_NOT_ALLOWED: 405,
|
|
1103
|
+
NOT_ACCEPTABLE: 406,
|
|
1104
|
+
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
1105
|
+
REQUEST_TIMEOUT: 408,
|
|
1106
|
+
CONFLICT: 409,
|
|
1107
|
+
GONE: 410,
|
|
1108
|
+
LENGTH_REQUIRED: 411,
|
|
1109
|
+
PRECONDITION_FAILED: 412,
|
|
1110
|
+
REQUEST_TOO_LONG: 413,
|
|
1111
|
+
REQUEST_URI_TOO_LONG: 414,
|
|
1112
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
1113
|
+
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
1114
|
+
EXPECTATION_FAILED: 417,
|
|
1115
|
+
IM_A_TEAPOT: 418,
|
|
1116
|
+
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
1117
|
+
METHOD_FAILURE: 420,
|
|
1118
|
+
MISDIRECTED_REQUEST: 421,
|
|
1119
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
1120
|
+
LOCKED: 423,
|
|
1121
|
+
FAILED_DEPENDENCY: 424,
|
|
1122
|
+
UPGRADE_REQUIRED: 426,
|
|
1123
|
+
PRECONDITION_REQUIRED: 428,
|
|
1124
|
+
TOO_MANY_REQUESTS: 429,
|
|
1125
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
1126
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
1127
|
+
},
|
|
1128
|
+
ServerErrors: {
|
|
1129
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
1130
|
+
NOT_IMPLEMENTED: 501,
|
|
1131
|
+
BAD_GATEWAY: 502,
|
|
1132
|
+
SERVICE_UNAVAILABLE: 503,
|
|
1133
|
+
GATEWAY_TIMEOUT: 504,
|
|
1134
|
+
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
1135
|
+
INSUFFICIENT_STORAGE: 507,
|
|
1136
|
+
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
1251
1137
|
}
|
|
1252
|
-
|
|
1138
|
+
};
|
|
1139
|
+
const AllErrorCodes = {
|
|
1140
|
+
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
1141
|
+
...TypedHTTP2.StatusCodes.ServerErrors
|
|
1142
|
+
};
|
|
1143
|
+
function assert2(value, code, message) {
|
|
1144
|
+
if (!value)
|
|
1145
|
+
throw HttpError.withCode(code, message);
|
|
1253
1146
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
|
|
1265
|
-
if (a2 < b1) {
|
|
1266
|
-
p1 += 2;
|
|
1267
|
-
let offset2 = 1;
|
|
1268
|
-
while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
|
|
1269
|
-
offset2 <<= 1;
|
|
1270
|
-
p1 += offset2 >> 1 << 1;
|
|
1271
|
-
} else if (b2 < a1) {
|
|
1272
|
-
p2 += 2;
|
|
1273
|
-
let offset2 = 1;
|
|
1274
|
-
while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
|
|
1275
|
-
offset2 <<= 1;
|
|
1276
|
-
p2 += offset2 >> 1 << 1;
|
|
1277
|
-
} else {
|
|
1278
|
-
return true;
|
|
1279
|
-
}
|
|
1147
|
+
TypedHTTP2.assert = assert2;
|
|
1148
|
+
class HttpError extends Error {
|
|
1149
|
+
constructor(status, message) {
|
|
1150
|
+
super(message);
|
|
1151
|
+
this.status = status;
|
|
1152
|
+
}
|
|
1153
|
+
static withCode(code, message) {
|
|
1154
|
+
const statusCode = AllErrorCodes[code];
|
|
1155
|
+
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
1156
|
+
return new HttpError(statusCode, message ?? defaultMessage);
|
|
1280
1157
|
}
|
|
1281
|
-
return false;
|
|
1282
1158
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const result = [];
|
|
1288
|
-
if (!Object.is(r[0], -Infinity))
|
|
1289
|
-
result.push(-Infinity, r[0] - 1);
|
|
1290
|
-
for (let i = 1; i < r.length - 2; i += 2)
|
|
1291
|
-
result.push(r[i] + 1, r[i + 1] - 1);
|
|
1292
|
-
if (!Object.is(r[r.length - 1], Infinity))
|
|
1293
|
-
result.push(r[r.length - 1] + 1, Infinity);
|
|
1294
|
-
return result;
|
|
1159
|
+
TypedHTTP2.HttpError = HttpError;
|
|
1160
|
+
const allHttpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
|
|
1161
|
+
function isInformationalResponse(response) {
|
|
1162
|
+
return response.status >= 100 && response.status < 200;
|
|
1295
1163
|
}
|
|
1296
|
-
|
|
1297
|
-
function
|
|
1298
|
-
|
|
1299
|
-
return ranges1;
|
|
1300
|
-
return intersect(ranges1, complement(ranges2));
|
|
1164
|
+
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
1165
|
+
function isSuccessResponse(response) {
|
|
1166
|
+
return response.status >= 200 && response.status < 300;
|
|
1301
1167
|
}
|
|
1302
|
-
|
|
1303
|
-
function
|
|
1304
|
-
return
|
|
1168
|
+
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
1169
|
+
function isRedirectResponse(response) {
|
|
1170
|
+
return response.status >= 300 && response.status < 400;
|
|
1305
1171
|
}
|
|
1306
|
-
|
|
1307
|
-
function
|
|
1308
|
-
|
|
1309
|
-
for (const r of ranges)
|
|
1310
|
-
result = union(result, r);
|
|
1311
|
-
return result;
|
|
1172
|
+
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
1173
|
+
function isErrorResponse(response) {
|
|
1174
|
+
return response.status >= 400 && response.status < 600;
|
|
1312
1175
|
}
|
|
1313
|
-
|
|
1314
|
-
function
|
|
1315
|
-
|
|
1316
|
-
if (ranges.length === 0)
|
|
1317
|
-
return [];
|
|
1318
|
-
if (ranges.length === 1)
|
|
1319
|
-
return ranges[0];
|
|
1320
|
-
if (ranges.length === 2)
|
|
1321
|
-
return union(ranges[0], ranges[1]);
|
|
1322
|
-
const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
|
|
1323
|
-
const result = [];
|
|
1324
|
-
let last;
|
|
1325
|
-
for (const interval of seq.seek(0)) {
|
|
1326
|
-
if (!last || last[1] + 1 < interval[0]) {
|
|
1327
|
-
result.push(interval);
|
|
1328
|
-
last = interval;
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
if (last[1] < interval[1])
|
|
1332
|
-
last[1] = interval[1];
|
|
1333
|
-
}
|
|
1334
|
-
return result.flat();
|
|
1176
|
+
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
1177
|
+
function info(status) {
|
|
1178
|
+
return { status };
|
|
1335
1179
|
}
|
|
1336
|
-
|
|
1337
|
-
function
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
return result;
|
|
1180
|
+
TypedHTTP2.info = info;
|
|
1181
|
+
function ok(data, contentType, status) {
|
|
1182
|
+
return {
|
|
1183
|
+
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
1184
|
+
contentType,
|
|
1185
|
+
data
|
|
1186
|
+
};
|
|
1344
1187
|
}
|
|
1345
|
-
|
|
1346
|
-
function
|
|
1347
|
-
|
|
1348
|
-
return void 0;
|
|
1349
|
-
return { min: ranges[0], max: ranges[ranges.length - 1] };
|
|
1188
|
+
TypedHTTP2.ok = ok;
|
|
1189
|
+
function redirect(url, status = 302) {
|
|
1190
|
+
return { status, url };
|
|
1350
1191
|
}
|
|
1351
|
-
|
|
1352
|
-
function
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
let offset2 = 1;
|
|
1372
|
-
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
1373
|
-
offset2 <<= 1;
|
|
1374
|
-
p1 += offset2 >> 1 << 1;
|
|
1375
|
-
}
|
|
1376
|
-
} else {
|
|
1377
|
-
if (r[r.length - 1] + 1 < ranges2[p2]) {
|
|
1378
|
-
r.push(ranges2[p2], ranges2[p2 + 1]);
|
|
1379
|
-
p2 += 2;
|
|
1380
|
-
} else if (r[r.length - 1] < ranges2[p2 + 1]) {
|
|
1381
|
-
r[r.length - 1] = ranges2[p2 + 1];
|
|
1382
|
-
p2 += 2;
|
|
1383
|
-
} else {
|
|
1384
|
-
p2 += 2;
|
|
1385
|
-
let offset2 = 1;
|
|
1386
|
-
while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
|
|
1387
|
-
offset2 <<= 1;
|
|
1388
|
-
p2 += offset2 >> 1 << 1;
|
|
1192
|
+
TypedHTTP2.redirect = redirect;
|
|
1193
|
+
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
1194
|
+
return { status, message };
|
|
1195
|
+
}
|
|
1196
|
+
TypedHTTP2.error = error;
|
|
1197
|
+
class Router {
|
|
1198
|
+
constructor(_resolveContext) {
|
|
1199
|
+
this._resolveContext = _resolveContext;
|
|
1200
|
+
}
|
|
1201
|
+
static create() {
|
|
1202
|
+
return new Router(async (e2) => e2.ctx);
|
|
1203
|
+
}
|
|
1204
|
+
rawMethod(method, route) {
|
|
1205
|
+
return {
|
|
1206
|
+
[method]: {
|
|
1207
|
+
method,
|
|
1208
|
+
input: route.input,
|
|
1209
|
+
etag: route.etag,
|
|
1210
|
+
resolveContext: this._resolveContext,
|
|
1211
|
+
handler: route.handler
|
|
1389
1212
|
}
|
|
1390
|
-
}
|
|
1213
|
+
};
|
|
1391
1214
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
r[r.length - 1] = ranges1[p1 + 1];
|
|
1398
|
-
p1 += 2;
|
|
1399
|
-
} else {
|
|
1400
|
-
p1 += 2;
|
|
1401
|
-
let offset2 = 1;
|
|
1402
|
-
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
1403
|
-
offset2 <<= 1;
|
|
1404
|
-
p1 += offset2 >> 1 << 1;
|
|
1405
|
-
}
|
|
1215
|
+
get(route) {
|
|
1216
|
+
return this.rawMethod("GET", {
|
|
1217
|
+
...route,
|
|
1218
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
|
|
1219
|
+
});
|
|
1406
1220
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1221
|
+
post(route) {
|
|
1222
|
+
return this.rawMethod("POST", {
|
|
1223
|
+
...route,
|
|
1224
|
+
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
use(resolveContext) {
|
|
1228
|
+
return new Router(async (options) => {
|
|
1229
|
+
const m = await this._resolveContext(options);
|
|
1230
|
+
return resolveContext({ ...options, ctx: m });
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
TypedHTTP2.Router = Router;
|
|
1235
|
+
function createClient(base, fetchCallback) {
|
|
1236
|
+
function buildUrl(method, path8, input, options) {
|
|
1237
|
+
const url = new URL(path8.join("/"), base);
|
|
1238
|
+
const signal = options?.signal;
|
|
1239
|
+
let body = void 0;
|
|
1240
|
+
if (method === "GET" && input)
|
|
1241
|
+
url.searchParams.set("input", JSON.stringify(input));
|
|
1242
|
+
else if (method !== "GET" && input)
|
|
1243
|
+
body = JSON.stringify(input);
|
|
1244
|
+
return {
|
|
1245
|
+
url,
|
|
1246
|
+
method,
|
|
1247
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
1248
|
+
body,
|
|
1249
|
+
signal
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function fetcher(method, path8, input, methodOptions) {
|
|
1253
|
+
const options = buildUrl(method, path8, input, methodOptions);
|
|
1254
|
+
return fetchCallback(options.url, {
|
|
1255
|
+
method: options.method,
|
|
1256
|
+
body: options.body,
|
|
1257
|
+
headers: options.headers,
|
|
1258
|
+
signal: options.signal
|
|
1259
|
+
}).then(async (response) => {
|
|
1260
|
+
if (response.status >= 200 && response.status < 300) {
|
|
1261
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
1262
|
+
const text = await response.text();
|
|
1263
|
+
return text.length ? JSON.parse(text) : void 0;
|
|
1264
|
+
}
|
|
1265
|
+
return await response.arrayBuffer();
|
|
1266
|
+
}
|
|
1267
|
+
if (response.status >= 400 && response.status < 600) {
|
|
1268
|
+
const text = await response.text();
|
|
1269
|
+
if (text)
|
|
1270
|
+
throw new HttpError(response.status, `HTTP request failed with status ${response.status}: ${text}`);
|
|
1271
|
+
else
|
|
1272
|
+
throw new HttpError(response.status, `HTTP request failed with status ${response.status}`);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function createProxy(path8 = []) {
|
|
1277
|
+
return new Proxy({}, {
|
|
1278
|
+
get(target, prop) {
|
|
1279
|
+
if (typeof prop === "symbol")
|
|
1280
|
+
return void 0;
|
|
1281
|
+
if (allHttpMethods.includes(prop)) {
|
|
1282
|
+
const f = fetcher.bind(null, prop, path8);
|
|
1283
|
+
f.prepare = buildUrl.bind(null, prop, path8);
|
|
1284
|
+
return f;
|
|
1285
|
+
}
|
|
1286
|
+
const newPath = [...path8, prop];
|
|
1287
|
+
return createProxy(newPath);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return createProxy();
|
|
1292
|
+
}
|
|
1293
|
+
TypedHTTP2.createClient = createClient;
|
|
1294
|
+
})(TypedHTTP || (TypedHTTP = {}));
|
|
1295
|
+
|
|
1296
|
+
// src/serverapi.ts
|
|
1297
|
+
import debug from "debug";
|
|
1298
|
+
var log = debug("fk:server_api");
|
|
1299
|
+
function createServerAPI(endpoint, options) {
|
|
1300
|
+
endpoint += "/api/";
|
|
1301
|
+
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
1302
|
+
...init,
|
|
1303
|
+
headers: {
|
|
1304
|
+
...init.headers,
|
|
1305
|
+
"Authorization": `Bearer ${options.auth}`
|
|
1306
|
+
}
|
|
1307
|
+
}) : fetch;
|
|
1308
|
+
if (options?.retries)
|
|
1309
|
+
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
1310
|
+
return TypedHTTP.createClient(endpoint, fetcher);
|
|
1311
|
+
}
|
|
1312
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
1313
|
+
for (const timeout of backoff) {
|
|
1314
|
+
try {
|
|
1315
|
+
return await job();
|
|
1316
|
+
} catch (e2) {
|
|
1317
|
+
if (e2 instanceof AggregateError)
|
|
1318
|
+
console.error(`[flakiness.io err]`, log(e2.errors[0]));
|
|
1319
|
+
else if (e2 instanceof Error)
|
|
1320
|
+
console.error(`[flakiness.io err]`, log(e2));
|
|
1321
|
+
else
|
|
1322
|
+
console.error(`[flakiness.io err]`, e2);
|
|
1323
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return await job();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/userSession.ts
|
|
1330
|
+
var CONFIG_DIR = (() => {
|
|
1331
|
+
const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
|
|
1332
|
+
return configDir;
|
|
1333
|
+
})();
|
|
1334
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
1335
|
+
var UserSession = class _UserSession {
|
|
1336
|
+
constructor(_config) {
|
|
1337
|
+
this._config = _config;
|
|
1338
|
+
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
1339
|
+
}
|
|
1340
|
+
static async load() {
|
|
1341
|
+
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e2) => void 0);
|
|
1342
|
+
if (!data)
|
|
1343
|
+
return void 0;
|
|
1344
|
+
const json = JSON.parse(data);
|
|
1345
|
+
return new _UserSession(json);
|
|
1346
|
+
}
|
|
1347
|
+
static async remove() {
|
|
1348
|
+
await fs.unlink(CONFIG_PATH).catch((e2) => void 0);
|
|
1349
|
+
}
|
|
1350
|
+
api;
|
|
1351
|
+
endpoint() {
|
|
1352
|
+
return this._config.endpoint;
|
|
1353
|
+
}
|
|
1354
|
+
path() {
|
|
1355
|
+
return CONFIG_PATH;
|
|
1356
|
+
}
|
|
1357
|
+
sessionToken() {
|
|
1358
|
+
return this._config.token;
|
|
1359
|
+
}
|
|
1360
|
+
async save() {
|
|
1361
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
1362
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/cli/cmd-access.ts
|
|
1367
|
+
import chalk from "chalk";
|
|
1368
|
+
|
|
1369
|
+
// src/authenticate.ts
|
|
1370
|
+
import { GithubOIDC } from "@flakiness/sdk";
|
|
1371
|
+
async function authenticate(options) {
|
|
1372
|
+
if (options.accessToken) {
|
|
1373
|
+
return {
|
|
1374
|
+
api: createServerAPI(options.endpoint, { auth: options.accessToken }),
|
|
1375
|
+
method: "access token",
|
|
1376
|
+
accessToken: options.accessToken,
|
|
1377
|
+
endpoint: options.endpoint
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
const session = await UserSession.load();
|
|
1381
|
+
if (session && session.endpoint() === options.endpoint) {
|
|
1382
|
+
return {
|
|
1383
|
+
api: session.api,
|
|
1384
|
+
method: "device OAuth",
|
|
1385
|
+
accessToken: session.sessionToken(),
|
|
1386
|
+
endpoint: session.endpoint()
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const githubOIDC = GithubOIDC.initializeFromEnv();
|
|
1390
|
+
if (githubOIDC) {
|
|
1391
|
+
if (!options.flakinessProject)
|
|
1392
|
+
throw new Error("GitHub OIDC requires --project to be specified");
|
|
1393
|
+
const token = await githubOIDC.createFlakinessAccessToken(options.flakinessProject);
|
|
1394
|
+
return {
|
|
1395
|
+
api: createServerAPI(options.endpoint, { auth: token }),
|
|
1396
|
+
method: "GitHub OIDC",
|
|
1397
|
+
accessToken: token,
|
|
1398
|
+
endpoint: options.endpoint
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
throw new Error('No authentication found. Use --access-token, run "flakiness auth login", or run in GitHub Actions with OIDC enabled.');
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/cli/cmd-access.ts
|
|
1405
|
+
async function cmdAccess(options) {
|
|
1406
|
+
const [orgSlug, projectSlug] = options.flakinessProject.split("/");
|
|
1407
|
+
if (!orgSlug || !projectSlug)
|
|
1408
|
+
throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
|
|
1409
|
+
const auth2 = await authenticate({
|
|
1410
|
+
accessToken: options.accessToken,
|
|
1411
|
+
endpoint: options.endpoint,
|
|
1412
|
+
flakinessProject: options.flakinessProject
|
|
1413
|
+
});
|
|
1414
|
+
const result = await auth2.api.project.checkAccess.GET({ orgSlug, projectSlug });
|
|
1415
|
+
if (options.json) {
|
|
1416
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1417
|
+
} else if (!options.quiet) {
|
|
1418
|
+
console.log(chalk.bold(`Project: `) + `${orgSlug}/${projectSlug}`);
|
|
1419
|
+
console.log(chalk.bold(`Auth: `) + (result.auth ?? chalk.dim("none")));
|
|
1420
|
+
console.log(chalk.bold(`Access: `) + (result.access ? chalk.green("granted") : chalk.red("denied")));
|
|
1421
|
+
if (result.permissions.length)
|
|
1422
|
+
console.log(chalk.bold(`Perms: `) + result.permissions.join(", "));
|
|
1423
|
+
}
|
|
1424
|
+
if (!result.access)
|
|
1425
|
+
process.exitCode = 1;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ../server/lib/common/knownClientIds.js
|
|
1429
|
+
var KNOWN_CLIENT_IDS = {
|
|
1430
|
+
OFFICIAL_WEB: "flakiness-io-official-cli",
|
|
1431
|
+
OFFICIAL_CLI: "flakiness-io-official-website"
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// src/cli/cmd-auth-login.ts
|
|
1435
|
+
import open from "open";
|
|
1436
|
+
import os2 from "os";
|
|
1437
|
+
|
|
1438
|
+
// src/cli/cmd-auth-logout.ts
|
|
1439
|
+
async function cmdAuthLogout() {
|
|
1440
|
+
const session = await UserSession.load();
|
|
1441
|
+
if (!session)
|
|
1442
|
+
return;
|
|
1443
|
+
const currentSession = await session.api.user.currentSession.GET().catch((e2) => void 0);
|
|
1444
|
+
if (currentSession)
|
|
1445
|
+
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e2) => void 0);
|
|
1446
|
+
await UserSession.remove();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/cli/cmd-auth-whoami.ts
|
|
1450
|
+
import chalk2 from "chalk";
|
|
1451
|
+
async function cmdAuthWhoami() {
|
|
1452
|
+
const session = await UserSession.load();
|
|
1453
|
+
if (!session || !await printLoggedInUser(session)) {
|
|
1454
|
+
console.log('Not logged in. Run "flakiness auth login" first.');
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async function printLoggedInUser(session) {
|
|
1459
|
+
try {
|
|
1460
|
+
const user = await session.api.user.whoami.GET();
|
|
1461
|
+
const superUser = user.isSuperUser ? " " + chalk2.red.bold("[SUPERUSER]") : "";
|
|
1462
|
+
console.log(`Logged in as ${chalk2.bold(user.userName)} (${user.userLogin})${superUser}`);
|
|
1463
|
+
console.log(`Endpoint: ${chalk2.cyan(session.endpoint())}`);
|
|
1464
|
+
return true;
|
|
1465
|
+
} catch (e2) {
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/cli/cmd-auth-login.ts
|
|
1471
|
+
var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
|
|
1472
|
+
async function cmdAuthLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
1473
|
+
await cmdAuthLogout();
|
|
1474
|
+
const api = createServerAPI(endpoint);
|
|
1475
|
+
const data = await api.deviceauth.createRequest.POST({
|
|
1476
|
+
clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
|
|
1477
|
+
name: os2.hostname()
|
|
1478
|
+
});
|
|
1479
|
+
await open(new URL(data.verificationUrl, endpoint).href);
|
|
1480
|
+
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
1481
|
+
let token;
|
|
1482
|
+
while (Date.now() < data.deadline) {
|
|
1483
|
+
await new Promise((x) => setTimeout(x, 2e3));
|
|
1484
|
+
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e2) => void 0);
|
|
1485
|
+
if (!result) {
|
|
1486
|
+
console.error(`Authorization request was rejected.`);
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
}
|
|
1489
|
+
token = result.token;
|
|
1490
|
+
if (token)
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
if (!token) {
|
|
1494
|
+
console.log(`Failed to login.`);
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
const session = new UserSession({
|
|
1498
|
+
endpoint,
|
|
1499
|
+
token
|
|
1500
|
+
});
|
|
1501
|
+
if (await printLoggedInUser(session)) {
|
|
1502
|
+
await session.save();
|
|
1503
|
+
} else {
|
|
1504
|
+
console.error(`x Failed to login:`);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/cli/cmd-convert.ts
|
|
1509
|
+
import { GitWorktree, writeReport } from "@flakiness/sdk";
|
|
1510
|
+
import fs3 from "fs/promises";
|
|
1511
|
+
import path3 from "path";
|
|
1512
|
+
|
|
1513
|
+
// src/junit.ts
|
|
1514
|
+
import { ReportUtils } from "@flakiness/sdk";
|
|
1515
|
+
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
1516
|
+
import assert from "assert";
|
|
1517
|
+
import fs2 from "fs";
|
|
1518
|
+
import mime from "mime";
|
|
1519
|
+
import path2 from "path";
|
|
1520
|
+
function getProperties(element) {
|
|
1521
|
+
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
1522
|
+
if (!propertiesNodes.length)
|
|
1523
|
+
return [];
|
|
1524
|
+
const result = [];
|
|
1525
|
+
for (const propertiesNode of propertiesNodes) {
|
|
1526
|
+
const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
|
|
1527
|
+
for (const property of properties) {
|
|
1528
|
+
const name = property.attributes["name"];
|
|
1529
|
+
const innerText = property.children.find((node) => node instanceof XmlText);
|
|
1530
|
+
const value = property.attributes["value"] ?? innerText?.text ?? "";
|
|
1531
|
+
result.push([name, value]);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return result;
|
|
1535
|
+
}
|
|
1536
|
+
function extractErrors(testcase) {
|
|
1537
|
+
const xmlErrors = testcase.children.filter((e2) => e2 instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
|
|
1538
|
+
if (!xmlErrors.length)
|
|
1539
|
+
return void 0;
|
|
1540
|
+
const errors = [];
|
|
1541
|
+
for (const xmlErr of xmlErrors) {
|
|
1542
|
+
const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
|
|
1543
|
+
const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
|
|
1544
|
+
const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
|
|
1545
|
+
errors.push({
|
|
1546
|
+
message,
|
|
1547
|
+
stack
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return errors;
|
|
1551
|
+
}
|
|
1552
|
+
function extractStdout(testcase, stdio) {
|
|
1553
|
+
const xmlStdio = testcase.children.filter((e2) => e2 instanceof XmlElement).filter((element) => element.name === stdio);
|
|
1554
|
+
if (!xmlStdio.length)
|
|
1555
|
+
return void 0;
|
|
1556
|
+
return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
|
|
1557
|
+
text: txtNode.text
|
|
1558
|
+
}));
|
|
1559
|
+
}
|
|
1560
|
+
async function parseAttachment(value) {
|
|
1561
|
+
let absolutePath = path2.resolve(process.cwd(), value);
|
|
1562
|
+
if (fs2.existsSync(absolutePath))
|
|
1563
|
+
return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
|
|
1564
|
+
return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
|
|
1565
|
+
}
|
|
1566
|
+
async function traverseJUnitReport(context, node) {
|
|
1567
|
+
const element = node;
|
|
1568
|
+
if (!(element instanceof XmlElement))
|
|
1569
|
+
return;
|
|
1570
|
+
let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
|
|
1571
|
+
if (element.attributes["timestamp"])
|
|
1572
|
+
currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
|
|
1573
|
+
if (element.name === "testsuite") {
|
|
1574
|
+
const file = element.attributes["file"];
|
|
1575
|
+
const line = parseInt(element.attributes["line"], 10);
|
|
1576
|
+
const name = element.attributes["name"];
|
|
1577
|
+
const newSuite = {
|
|
1578
|
+
title: name ?? file,
|
|
1579
|
+
location: file && !isNaN(line) ? {
|
|
1580
|
+
file,
|
|
1581
|
+
line,
|
|
1582
|
+
column: 1
|
|
1583
|
+
} : void 0,
|
|
1584
|
+
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
1585
|
+
suites: [],
|
|
1586
|
+
tests: []
|
|
1587
|
+
};
|
|
1588
|
+
if (currentSuite) {
|
|
1589
|
+
currentSuite.suites ??= [];
|
|
1590
|
+
currentSuite.suites.push(newSuite);
|
|
1591
|
+
} else {
|
|
1592
|
+
report.suites ??= [];
|
|
1593
|
+
report.suites.push(newSuite);
|
|
1594
|
+
}
|
|
1595
|
+
currentSuite = newSuite;
|
|
1596
|
+
const userSuppliedData = getProperties(element);
|
|
1597
|
+
if (userSuppliedData.length) {
|
|
1598
|
+
currentEnv = structuredClone(currentEnv);
|
|
1599
|
+
currentEnv.userSuppliedData ??= {};
|
|
1600
|
+
for (const [key, value] of userSuppliedData)
|
|
1601
|
+
currentEnv.userSuppliedData[key] = value;
|
|
1602
|
+
currentEnvIndex = report.environments.push(currentEnv) - 1;
|
|
1603
|
+
}
|
|
1604
|
+
} else if (element.name === "testcase") {
|
|
1605
|
+
assert(currentSuite);
|
|
1606
|
+
const file = element.attributes["file"];
|
|
1607
|
+
const name = element.attributes["name"];
|
|
1608
|
+
const line = parseInt(element.attributes["line"], 10);
|
|
1609
|
+
const timeMs = parseFloat(element.attributes["time"]) * 1e3;
|
|
1610
|
+
const startTimestamp = currentTimeMs;
|
|
1611
|
+
const duration = timeMs;
|
|
1612
|
+
currentTimeMs += timeMs;
|
|
1613
|
+
const annotations = [];
|
|
1614
|
+
const attachments2 = [];
|
|
1615
|
+
for (const [key, value] of getProperties(element)) {
|
|
1616
|
+
if (key.toLowerCase().startsWith("attachment")) {
|
|
1617
|
+
if (context.ignoreAttachments)
|
|
1618
|
+
continue;
|
|
1619
|
+
const attachment = await parseAttachment(value);
|
|
1620
|
+
context.attachments.set(attachment.id, attachment);
|
|
1621
|
+
attachments2.push({
|
|
1622
|
+
id: attachment.id,
|
|
1623
|
+
contentType: attachment.contentType,
|
|
1624
|
+
//TODO: better default names for attachments?
|
|
1625
|
+
name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
|
|
1626
|
+
});
|
|
1414
1627
|
} else {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
p2 += offset2 >> 1 << 1;
|
|
1628
|
+
annotations.push({
|
|
1629
|
+
type: key,
|
|
1630
|
+
description: value.length ? value : void 0
|
|
1631
|
+
});
|
|
1420
1632
|
}
|
|
1421
1633
|
}
|
|
1422
|
-
|
|
1634
|
+
const childElements = element.children.filter((child) => child instanceof XmlElement);
|
|
1635
|
+
const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
|
|
1636
|
+
if (xmlSkippedAnnotation)
|
|
1637
|
+
annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
|
|
1638
|
+
const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
|
|
1639
|
+
const errors = extractErrors(element);
|
|
1640
|
+
const test = {
|
|
1641
|
+
title: name,
|
|
1642
|
+
location: file && !isNaN(line) ? {
|
|
1643
|
+
file,
|
|
1644
|
+
line,
|
|
1645
|
+
column: 1
|
|
1646
|
+
} : void 0,
|
|
1647
|
+
attempts: [{
|
|
1648
|
+
environmentIdx: currentEnvIndex,
|
|
1649
|
+
expectedStatus,
|
|
1650
|
+
annotations,
|
|
1651
|
+
attachments: attachments2,
|
|
1652
|
+
startTimestamp,
|
|
1653
|
+
duration,
|
|
1654
|
+
status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
|
|
1655
|
+
errors,
|
|
1656
|
+
stdout: extractStdout(element, "system-out"),
|
|
1657
|
+
stderr: extractStdout(element, "system-err")
|
|
1658
|
+
}]
|
|
1659
|
+
};
|
|
1660
|
+
currentSuite.tests ??= [];
|
|
1661
|
+
currentSuite.tests.push(test);
|
|
1423
1662
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1663
|
+
context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
|
|
1664
|
+
for (const child of element.children)
|
|
1665
|
+
await traverseJUnitReport(context, child);
|
|
1666
|
+
}
|
|
1667
|
+
async function parseJUnit(xmls, options) {
|
|
1668
|
+
const report = {
|
|
1669
|
+
category: "junit",
|
|
1670
|
+
commitId: options.commitId,
|
|
1671
|
+
duration: options.runDuration,
|
|
1672
|
+
startTimestamp: options.runStartTimestamp,
|
|
1673
|
+
url: options.runUrl,
|
|
1674
|
+
environments: [options.defaultEnv],
|
|
1675
|
+
suites: [],
|
|
1676
|
+
unattributedErrors: []
|
|
1677
|
+
};
|
|
1678
|
+
const context = {
|
|
1679
|
+
currentEnv: options.defaultEnv,
|
|
1680
|
+
currentEnvIndex: 0,
|
|
1681
|
+
currentTimeMs: 0,
|
|
1682
|
+
report,
|
|
1683
|
+
currentSuite: void 0,
|
|
1684
|
+
attachments: /* @__PURE__ */ new Map(),
|
|
1685
|
+
ignoreAttachments: !!options.ignoreAttachments
|
|
1686
|
+
};
|
|
1687
|
+
for (const xml of xmls) {
|
|
1688
|
+
const doc = parseXml(xml);
|
|
1689
|
+
for (const element of doc.children)
|
|
1690
|
+
await traverseJUnitReport(context, element);
|
|
1437
1691
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1692
|
+
return {
|
|
1693
|
+
report: ReportUtils.normalizeReport(report),
|
|
1694
|
+
attachments: Array.from(context.attachments.values())
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/cli/cmd-convert.ts
|
|
1699
|
+
async function cmdConvert(junitPath, options) {
|
|
1700
|
+
const fullPath = path3.resolve(junitPath);
|
|
1701
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1702
|
+
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
}
|
|
1705
|
+
const stat = await fs3.stat(fullPath);
|
|
1706
|
+
let xmlContents = [];
|
|
1707
|
+
if (stat.isFile()) {
|
|
1708
|
+
const xmlContent = await fs3.readFile(fullPath, "utf-8");
|
|
1709
|
+
xmlContents.push(xmlContent);
|
|
1710
|
+
} else if (stat.isDirectory()) {
|
|
1711
|
+
const xmlFiles = await findXmlFiles(fullPath);
|
|
1712
|
+
if (xmlFiles.length === 0) {
|
|
1713
|
+
console.error(`Error: No XML files found in directory ${fullPath}`);
|
|
1714
|
+
process.exit(1);
|
|
1445
1715
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
return { next: () => firstInterval };
|
|
1455
|
-
let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
|
|
1456
|
-
let to = firstInterval.value[1];
|
|
1457
|
-
return {
|
|
1458
|
-
next() {
|
|
1459
|
-
if (from2 > to) {
|
|
1460
|
-
const interval = it.next();
|
|
1461
|
-
if (interval.done)
|
|
1462
|
-
return { done: true, value: void 0 };
|
|
1463
|
-
from2 = interval.value[0];
|
|
1464
|
-
to = interval.value[1];
|
|
1465
|
-
}
|
|
1466
|
-
return { done: false, value: from2++ };
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
},
|
|
1470
|
-
length
|
|
1471
|
-
);
|
|
1716
|
+
console.log(`Found ${xmlFiles.length} XML files`);
|
|
1717
|
+
for (const xmlFile of xmlFiles) {
|
|
1718
|
+
const xmlContent = await fs3.readFile(xmlFile, "utf-8");
|
|
1719
|
+
xmlContents.push(xmlContent);
|
|
1720
|
+
}
|
|
1721
|
+
} else {
|
|
1722
|
+
console.error(`Error: ${fullPath} is neither a file nor a directory`);
|
|
1723
|
+
process.exit(1);
|
|
1472
1724
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1725
|
+
let commitId;
|
|
1726
|
+
if (options.commitId) {
|
|
1727
|
+
commitId = options.commitId;
|
|
1728
|
+
} else {
|
|
1729
|
+
try {
|
|
1730
|
+
const worktree = GitWorktree.create(process.cwd());
|
|
1731
|
+
commitId = worktree.headCommitId();
|
|
1732
|
+
} catch (e2) {
|
|
1733
|
+
console.error("Failed to get git commit info. Please provide --commit-id option.");
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const { report, attachments } = await parseJUnit(xmlContents, {
|
|
1738
|
+
commitId,
|
|
1739
|
+
defaultEnv: { name: options.envName },
|
|
1740
|
+
runStartTimestamp: Date.now(),
|
|
1741
|
+
runDuration: 0
|
|
1742
|
+
});
|
|
1743
|
+
if (options.flakinessProject)
|
|
1744
|
+
report.flakinessProject = options.flakinessProject;
|
|
1745
|
+
await writeReport(report, attachments, options.outputDir);
|
|
1746
|
+
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1747
|
+
}
|
|
1748
|
+
async function findXmlFiles(dir, result = []) {
|
|
1749
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1750
|
+
for (const entry of entries) {
|
|
1751
|
+
const fullPath = path3.join(dir, entry.name);
|
|
1752
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
1753
|
+
result.push(fullPath);
|
|
1754
|
+
else if (entry.isDirectory())
|
|
1755
|
+
await findXmlFiles(fullPath, result);
|
|
1756
|
+
}
|
|
1757
|
+
return result;
|
|
1758
|
+
}
|
|
1475
1759
|
|
|
1476
1760
|
// src/cli/cmd-download.ts
|
|
1477
1761
|
import fs4 from "fs";
|
|
@@ -1565,14 +1849,268 @@ async function downloadRun(api, project, runId) {
|
|
|
1565
1849
|
await Promise.all(workerPromises);
|
|
1566
1850
|
}
|
|
1567
1851
|
|
|
1852
|
+
// ../node_modules/.pnpm/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js
|
|
1853
|
+
var t = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 48, 8, 96, 3, 127, 127, 127, 1, 127, 96, 3, 127, 127, 127, 0, 96, 2, 127, 127, 0, 96, 1, 127, 1, 127, 96, 3, 127, 127, 126, 1, 126, 96, 3, 126, 127, 127, 1, 126, 96, 2, 127, 126, 0, 96, 1, 127, 1, 126, 3, 11, 10, 0, 0, 2, 1, 3, 4, 5, 6, 1, 7, 5, 3, 1, 0, 1, 7, 85, 9, 3, 109, 101, 109, 2, 0, 5, 120, 120, 104, 51, 50, 0, 0, 6, 105, 110, 105, 116, 51, 50, 0, 2, 8, 117, 112, 100, 97, 116, 101, 51, 50, 0, 3, 8, 100, 105, 103, 101, 115, 116, 51, 50, 0, 4, 5, 120, 120, 104, 54, 52, 0, 5, 6, 105, 110, 105, 116, 54, 52, 0, 7, 8, 117, 112, 100, 97, 116, 101, 54, 52, 0, 8, 8, 100, 105, 103, 101, 115, 116, 54, 52, 0, 9, 10, 251, 22, 10, 242, 1, 1, 4, 127, 32, 0, 32, 1, 106, 33, 3, 32, 1, 65, 16, 79, 4, 127, 32, 3, 65, 16, 107, 33, 6, 32, 2, 65, 168, 136, 141, 161, 2, 106, 33, 3, 32, 2, 65, 137, 235, 208, 208, 7, 107, 33, 4, 32, 2, 65, 207, 140, 162, 142, 6, 106, 33, 5, 3, 64, 32, 3, 32, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 4, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 4, 32, 2, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 5, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 6, 32, 0, 65, 4, 106, 34, 0, 79, 13, 0, 11, 32, 2, 65, 12, 119, 32, 5, 65, 18, 119, 106, 32, 4, 65, 7, 119, 106, 32, 3, 65, 1, 119, 106, 5, 32, 2, 65, 177, 207, 217, 178, 1, 106, 11, 32, 1, 106, 32, 0, 32, 1, 65, 15, 113, 16, 1, 11, 146, 1, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 1, 65, 4, 106, 32, 2, 75, 69, 4, 64, 32, 0, 32, 1, 40, 2, 0, 65, 189, 220, 202, 149, 124, 108, 106, 65, 17, 119, 65, 175, 214, 211, 190, 2, 108, 33, 0, 32, 1, 65, 4, 106, 33, 1, 12, 1, 11, 11, 3, 64, 32, 1, 32, 2, 79, 69, 4, 64, 32, 0, 32, 1, 45, 0, 0, 65, 177, 207, 217, 178, 1, 108, 106, 65, 11, 119, 65, 177, 243, 221, 241, 121, 108, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 65, 15, 118, 115, 65, 247, 148, 175, 175, 120, 108, 34, 0, 65, 13, 118, 32, 0, 115, 65, 189, 220, 202, 149, 124, 108, 34, 0, 65, 16, 118, 32, 0, 115, 11, 63, 0, 32, 0, 65, 8, 106, 32, 1, 65, 168, 136, 141, 161, 2, 106, 54, 2, 0, 32, 0, 65, 12, 106, 32, 1, 65, 137, 235, 208, 208, 7, 107, 54, 2, 0, 32, 0, 65, 16, 106, 32, 1, 54, 2, 0, 32, 0, 65, 20, 106, 32, 1, 65, 207, 140, 162, 142, 6, 106, 54, 2, 0, 11, 195, 4, 1, 6, 127, 32, 1, 32, 2, 106, 33, 6, 32, 0, 65, 24, 106, 33, 4, 32, 0, 65, 40, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 40, 2, 0, 32, 2, 106, 54, 2, 0, 32, 0, 65, 4, 106, 34, 5, 32, 5, 40, 2, 0, 32, 2, 65, 16, 79, 32, 0, 40, 2, 0, 65, 16, 79, 114, 114, 54, 2, 0, 32, 2, 32, 3, 106, 65, 16, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 16, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 12, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 4, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 8, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 20, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 12, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 40, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 32, 6, 65, 16, 107, 77, 4, 64, 32, 6, 65, 16, 107, 33, 8, 32, 0, 65, 8, 106, 40, 2, 0, 33, 2, 32, 0, 65, 12, 106, 40, 2, 0, 33, 3, 32, 0, 65, 16, 106, 40, 2, 0, 33, 5, 32, 0, 65, 20, 106, 40, 2, 0, 33, 7, 3, 64, 32, 2, 32, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 3, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 5, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 7, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 7, 32, 8, 32, 1, 65, 4, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 2, 54, 2, 0, 32, 0, 65, 12, 106, 32, 3, 54, 2, 0, 32, 0, 65, 16, 106, 32, 5, 54, 2, 0, 32, 0, 65, 20, 106, 32, 7, 54, 2, 0, 11, 32, 1, 32, 6, 73, 4, 64, 32, 4, 32, 1, 32, 6, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 1, 54, 2, 0, 11, 11, 97, 1, 1, 127, 32, 0, 65, 16, 106, 40, 2, 0, 33, 1, 32, 0, 65, 4, 106, 40, 2, 0, 4, 127, 32, 1, 65, 12, 119, 32, 0, 65, 20, 106, 40, 2, 0, 65, 18, 119, 106, 32, 0, 65, 12, 106, 40, 2, 0, 65, 7, 119, 106, 32, 0, 65, 8, 106, 40, 2, 0, 65, 1, 119, 106, 5, 32, 1, 65, 177, 207, 217, 178, 1, 106, 11, 32, 0, 40, 2, 0, 106, 32, 0, 65, 24, 106, 32, 0, 65, 40, 106, 40, 2, 0, 16, 1, 11, 255, 3, 2, 3, 126, 1, 127, 32, 0, 32, 1, 106, 33, 6, 32, 1, 65, 32, 79, 4, 126, 32, 6, 65, 32, 107, 33, 6, 32, 2, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 33, 3, 32, 2, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 33, 4, 32, 2, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 33, 5, 3, 64, 32, 3, 32, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 3, 32, 4, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 4, 32, 2, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 2, 32, 5, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 5, 32, 6, 32, 0, 65, 8, 106, 34, 0, 79, 13, 0, 11, 32, 2, 66, 12, 137, 32, 5, 66, 18, 137, 124, 32, 4, 66, 7, 137, 124, 32, 3, 66, 1, 137, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 2, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 2, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 1, 173, 124, 32, 0, 32, 1, 65, 31, 113, 16, 6, 11, 134, 2, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 2, 32, 1, 65, 8, 106, 79, 4, 64, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 32, 0, 133, 66, 27, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 33, 0, 32, 1, 65, 8, 106, 33, 1, 12, 1, 11, 11, 32, 1, 65, 4, 106, 32, 2, 77, 4, 64, 32, 0, 32, 1, 53, 2, 0, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 23, 137, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 124, 33, 0, 32, 1, 65, 4, 106, 33, 1, 11, 3, 64, 32, 1, 32, 2, 73, 4, 64, 32, 0, 32, 1, 49, 0, 0, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 126, 133, 66, 11, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 66, 33, 136, 133, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 34, 0, 32, 0, 66, 29, 136, 133, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 126, 34, 0, 32, 0, 66, 32, 136, 133, 11, 77, 0, 32, 0, 65, 8, 106, 32, 1, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 55, 3, 0, 32, 0, 65, 16, 106, 32, 1, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 55, 3, 0, 32, 0, 65, 24, 106, 32, 1, 55, 3, 0, 32, 0, 65, 32, 106, 32, 1, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 55, 3, 0, 11, 244, 4, 2, 3, 127, 4, 126, 32, 1, 32, 2, 106, 33, 5, 32, 0, 65, 40, 106, 33, 4, 32, 0, 65, 200, 0, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 41, 3, 0, 32, 2, 173, 124, 55, 3, 0, 32, 2, 32, 3, 106, 65, 32, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 32, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 8, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 24, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 16, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 32, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 24, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 200, 0, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 65, 32, 106, 32, 5, 77, 4, 64, 32, 5, 65, 32, 107, 33, 2, 32, 0, 65, 8, 106, 41, 3, 0, 33, 6, 32, 0, 65, 16, 106, 41, 3, 0, 33, 7, 32, 0, 65, 24, 106, 41, 3, 0, 33, 8, 32, 0, 65, 32, 106, 41, 3, 0, 33, 9, 3, 64, 32, 6, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 6, 32, 7, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 7, 32, 8, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 8, 32, 9, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 9, 32, 2, 32, 1, 65, 8, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 6, 55, 3, 0, 32, 0, 65, 16, 106, 32, 7, 55, 3, 0, 32, 0, 65, 24, 106, 32, 8, 55, 3, 0, 32, 0, 65, 32, 106, 32, 9, 55, 3, 0, 11, 32, 1, 32, 5, 73, 4, 64, 32, 4, 32, 1, 32, 5, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 1, 54, 2, 0, 11, 11, 188, 2, 1, 5, 126, 32, 0, 65, 24, 106, 41, 3, 0, 33, 1, 32, 0, 41, 3, 0, 34, 2, 66, 32, 90, 4, 126, 32, 0, 65, 8, 106, 41, 3, 0, 34, 3, 66, 1, 137, 32, 0, 65, 16, 106, 41, 3, 0, 34, 4, 66, 7, 137, 124, 32, 1, 66, 12, 137, 32, 0, 65, 32, 106, 41, 3, 0, 34, 5, 66, 18, 137, 124, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 1, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 1, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 2, 124, 32, 0, 65, 40, 106, 32, 2, 66, 31, 131, 167, 16, 6, 11]);
|
|
1854
|
+
async function e() {
|
|
1855
|
+
return (function(t2) {
|
|
1856
|
+
const { exports: { mem: e2, xxh32: n, xxh64: r, init32: i, update32: a, digest32: o, init64: s, update64: u, digest64: c } } = t2;
|
|
1857
|
+
let h = new Uint8Array(e2.buffer);
|
|
1858
|
+
function g(t3, n2) {
|
|
1859
|
+
if (e2.buffer.byteLength < t3 + n2) {
|
|
1860
|
+
const r2 = Math.ceil((t3 + n2 - e2.buffer.byteLength) / 65536);
|
|
1861
|
+
e2.grow(r2), h = new Uint8Array(e2.buffer);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function f(t3, e3, n2, r2, i2, a2) {
|
|
1865
|
+
g(t3);
|
|
1866
|
+
const o2 = new Uint8Array(t3);
|
|
1867
|
+
return h.set(o2), n2(0, e3), o2.set(h.subarray(0, t3)), { update(e4) {
|
|
1868
|
+
let n3;
|
|
1869
|
+
return h.set(o2), "string" == typeof e4 ? (g(3 * e4.length, t3), n3 = w.encodeInto(e4, h.subarray(t3)).written) : (g(e4.byteLength, t3), h.set(e4, t3), n3 = e4.byteLength), r2(0, t3, n3), o2.set(h.subarray(0, t3)), this;
|
|
1870
|
+
}, digest: () => (h.set(o2), a2(i2(0))) };
|
|
1871
|
+
}
|
|
1872
|
+
function y(t3) {
|
|
1873
|
+
return t3 >>> 0;
|
|
1874
|
+
}
|
|
1875
|
+
const b = 2n ** 64n - 1n;
|
|
1876
|
+
function d(t3) {
|
|
1877
|
+
return t3 & b;
|
|
1878
|
+
}
|
|
1879
|
+
const w = new TextEncoder(), l = 0, p = 0n;
|
|
1880
|
+
function x(t3, e3 = l) {
|
|
1881
|
+
return g(3 * t3.length, 0), y(n(0, w.encodeInto(t3, h).written, e3));
|
|
1882
|
+
}
|
|
1883
|
+
function L(t3, e3 = p) {
|
|
1884
|
+
return g(3 * t3.length, 0), d(r(0, w.encodeInto(t3, h).written, e3));
|
|
1885
|
+
}
|
|
1886
|
+
return { h32: x, h32ToString: (t3, e3 = l) => x(t3, e3).toString(16).padStart(8, "0"), h32Raw: (t3, e3 = l) => (g(t3.byteLength, 0), h.set(t3), y(n(0, t3.byteLength, e3))), create32: (t3 = l) => f(48, t3, i, a, o, y), h64: L, h64ToString: (t3, e3 = p) => L(t3, e3).toString(16).padStart(16, "0"), h64Raw: (t3, e3 = p) => (g(t3.byteLength, 0), h.set(t3), d(r(0, t3.byteLength, e3))), create64: (t3 = p) => f(88, t3, s, u, c, d) };
|
|
1887
|
+
})((await WebAssembly.instantiate(t)).instance);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// ../shared/lib/common/utils.js
|
|
1891
|
+
var xxHasher = await e();
|
|
1892
|
+
function humanReadableMs(ms) {
|
|
1893
|
+
if (+ms < 1e-5)
|
|
1894
|
+
return `0 ms`;
|
|
1895
|
+
if (+ms < 1)
|
|
1896
|
+
return `${Math.round(+ms * 1e3)} \xB5s`;
|
|
1897
|
+
ms = Math.round(+ms);
|
|
1898
|
+
let seconds = +ms / 1e3;
|
|
1899
|
+
if (seconds < 1)
|
|
1900
|
+
return `${ms} ms`;
|
|
1901
|
+
if (seconds < 60)
|
|
1902
|
+
return `${seconds.toFixed(1)} sec`;
|
|
1903
|
+
seconds = Math.round(seconds);
|
|
1904
|
+
let minutes = seconds / 60 | 0;
|
|
1905
|
+
seconds = seconds % 60;
|
|
1906
|
+
if (minutes < 1)
|
|
1907
|
+
return `${seconds} sec`;
|
|
1908
|
+
let hours = minutes / 60 | 0;
|
|
1909
|
+
minutes = minutes % 60;
|
|
1910
|
+
if (hours < 1)
|
|
1911
|
+
return seconds !== 0 ? `${minutes} min ${seconds} sec` : `${minutes} min`;
|
|
1912
|
+
let days = hours / 24 | 0;
|
|
1913
|
+
if (days < 1)
|
|
1914
|
+
return `${hours} h ${minutes} min`;
|
|
1915
|
+
hours = hours % 24;
|
|
1916
|
+
let weeks = days / 7 | 0;
|
|
1917
|
+
if (weeks < 1)
|
|
1918
|
+
return `${days} days ${hours} h ${minutes} min`;
|
|
1919
|
+
days %= 7;
|
|
1920
|
+
return `${weeks} weeks ${days} days ${hours} h`;
|
|
1921
|
+
}
|
|
1922
|
+
function durationTrendScale(currentMs, baseMs) {
|
|
1923
|
+
const trend = baseMs > 0 ? currentMs / baseMs : 1;
|
|
1924
|
+
if (trend > 1e3)
|
|
1925
|
+
return ">x1000";
|
|
1926
|
+
if (trend > 2)
|
|
1927
|
+
return `x${trend.toFixed(2)}`;
|
|
1928
|
+
if (trend > 1.01)
|
|
1929
|
+
return `+${(trend - 1) * 100 | 0}%`;
|
|
1930
|
+
if (trend > 0.99)
|
|
1931
|
+
return "\xB10%";
|
|
1932
|
+
return `-${(1 - trend) * 100 | 0}%`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/cli/cmd-list-tests.ts
|
|
1936
|
+
async function cmdListTests(options) {
|
|
1937
|
+
const [orgSlug, projectSlug] = options.flakinessProject.split("/");
|
|
1938
|
+
if (!orgSlug || !projectSlug)
|
|
1939
|
+
throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
|
|
1940
|
+
const { api } = await authenticate({
|
|
1941
|
+
endpoint: options.endpoint,
|
|
1942
|
+
accessToken: options.accessToken,
|
|
1943
|
+
flakinessProject: options.flakinessProject
|
|
1944
|
+
});
|
|
1945
|
+
let preset;
|
|
1946
|
+
if (options.pr !== void 0)
|
|
1947
|
+
preset = { type: "pr", number: options.pr };
|
|
1948
|
+
else if (options.branch !== void 0)
|
|
1949
|
+
preset = { type: "branch", name: options.branch };
|
|
1950
|
+
const result = await api.report.testStats.POST({
|
|
1951
|
+
orgSlug,
|
|
1952
|
+
projectSlug,
|
|
1953
|
+
preset,
|
|
1954
|
+
pageOptions: {
|
|
1955
|
+
// CLI pages are 1-based while backend pages are 0-based.
|
|
1956
|
+
number: options.page - 1,
|
|
1957
|
+
size: options.pageSize
|
|
1958
|
+
},
|
|
1959
|
+
sortOptions: {
|
|
1960
|
+
axis: options.sort,
|
|
1961
|
+
direction: options.sortDir
|
|
1962
|
+
},
|
|
1963
|
+
historyBuckets: 10,
|
|
1964
|
+
fql: options.fql
|
|
1965
|
+
});
|
|
1966
|
+
let scope = "default branch";
|
|
1967
|
+
if (options.pr !== void 0)
|
|
1968
|
+
scope = `PR #${options.pr}`;
|
|
1969
|
+
else if (options.branch !== void 0)
|
|
1970
|
+
scope = `branch \`${options.branch}\``;
|
|
1971
|
+
const lines = [
|
|
1972
|
+
`# Tests for \`${orgSlug}/${projectSlug}\` (${scope})`,
|
|
1973
|
+
"",
|
|
1974
|
+
`Total tests: **${result.totalElements}**`,
|
|
1975
|
+
`Showing page: **${result.pageNumber + 1}/${Math.max(result.totalPages, 1)}** (page size ${result.pageSize})`,
|
|
1976
|
+
""
|
|
1977
|
+
];
|
|
1978
|
+
if (!result.elements.length) {
|
|
1979
|
+
lines.push("_No tests found on this page._");
|
|
1980
|
+
console.log(lines.join("\n"));
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
for (const [index, testStats] of result.elements.entries()) {
|
|
1984
|
+
const fullName = testStats.test.titles.join(" > ") || testStats.test.testId;
|
|
1985
|
+
const location = `${testStats.test.filePath}:${testStats.lineNumber}`;
|
|
1986
|
+
const error = extractError(testStats);
|
|
1987
|
+
const status = outcomeToStatus(testStats.outcome);
|
|
1988
|
+
const duration = humanReadableMs(testStats.durationMs);
|
|
1989
|
+
const trend = formatDurationTrend(testStats.durationChangeMs, testStats.durationMs);
|
|
1990
|
+
const flipRate = formatFlipRate(testStats.flipRate);
|
|
1991
|
+
const env = formatEnv(testStats.env);
|
|
1992
|
+
lines.push(`### ${index + 1}. ${fullName}`);
|
|
1993
|
+
lines.push(`- Location: ${asInlineCode(location)}`);
|
|
1994
|
+
lines.push(`- Env: ${asInlineCode(env)}`);
|
|
1995
|
+
lines.push(`- Status: ${asInlineCode(status)}`);
|
|
1996
|
+
lines.push(`- Duration: ${asInlineCode(duration)} (${asInlineCode(trend)})`);
|
|
1997
|
+
lines.push(`- Flip Rate: ${asInlineCode(flipRate)}`);
|
|
1998
|
+
lines.push(`- Error: ${error ? asInlineCode(error) : "None"}`);
|
|
1999
|
+
lines.push("");
|
|
2000
|
+
}
|
|
2001
|
+
console.log(lines.join("\n").trimEnd());
|
|
2002
|
+
}
|
|
2003
|
+
function extractError(testStats) {
|
|
2004
|
+
const texts = (testStats.errors ?? []).map((error) => error.message?.trim() || error.value?.trim()).filter(Boolean);
|
|
2005
|
+
if (!texts.length)
|
|
2006
|
+
return void 0;
|
|
2007
|
+
return texts.join(" | ");
|
|
2008
|
+
}
|
|
2009
|
+
function outcomeToStatus(outcome) {
|
|
2010
|
+
switch (outcome) {
|
|
2011
|
+
case "expected":
|
|
2012
|
+
return "passed";
|
|
2013
|
+
case "unexpected":
|
|
2014
|
+
return "failed";
|
|
2015
|
+
case "regressed":
|
|
2016
|
+
return "regressed";
|
|
2017
|
+
case "skipped":
|
|
2018
|
+
return "skipped";
|
|
2019
|
+
case "flaked":
|
|
2020
|
+
return "flaked";
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function formatEnv(env) {
|
|
2024
|
+
const parts = [];
|
|
2025
|
+
const { systemData } = env;
|
|
2026
|
+
if (systemData.osName)
|
|
2027
|
+
parts.push(`os=${systemData.osName}`);
|
|
2028
|
+
if (systemData.osVersion)
|
|
2029
|
+
parts.push(`osVersion=${systemData.osVersion}`);
|
|
2030
|
+
if (systemData.osArch)
|
|
2031
|
+
parts.push(`arch=${systemData.osArch}`);
|
|
2032
|
+
if (env.userSuppliedData) {
|
|
2033
|
+
for (const [key, value] of Object.entries(env.userSuppliedData))
|
|
2034
|
+
parts.push(`${key}=${value}`);
|
|
2035
|
+
}
|
|
2036
|
+
return parts.join(", ") || env.name || "unknown";
|
|
2037
|
+
}
|
|
2038
|
+
function formatDurationTrend(changeMs, currentMs) {
|
|
2039
|
+
const baseMs = currentMs - changeMs;
|
|
2040
|
+
const scale = durationTrendScale(currentMs, baseMs);
|
|
2041
|
+
const sign = changeMs >= 0 ? "+" : "-";
|
|
2042
|
+
return `${sign}${humanReadableMs(Math.abs(changeMs))} (${scale})`;
|
|
2043
|
+
}
|
|
2044
|
+
function formatFlipRate(flipRate) {
|
|
2045
|
+
if (flipRate === void 0 || !Number.isFinite(flipRate))
|
|
2046
|
+
return "n/a";
|
|
2047
|
+
return `${(flipRate * 100).toFixed(2)}%`;
|
|
2048
|
+
}
|
|
2049
|
+
function asInlineCode(text) {
|
|
2050
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
2051
|
+
const ticks = normalized.includes("`") ? "``" : "`";
|
|
2052
|
+
return `${ticks}${normalized}${ticks}`;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// src/cli/cmd-skills-install.ts
|
|
2056
|
+
import { execSync } from "child_process";
|
|
2057
|
+
import chalk3 from "chalk";
|
|
2058
|
+
import fs5 from "fs";
|
|
2059
|
+
import path5 from "path";
|
|
2060
|
+
|
|
2061
|
+
// src/generated/bundledSkillsData.ts
|
|
2062
|
+
var BUNDLED_SKILLS = [
|
|
2063
|
+
{
|
|
2064
|
+
"name": "flakiness-investigation",
|
|
2065
|
+
"description": "Use when querying Flakiness.io test data, writing FQL filters, finding flaky or failed tests, investigating regressions, analyzing test health, or fixing PR test failures with the flakiness CLI.",
|
|
2066
|
+
"files": [
|
|
2067
|
+
{
|
|
2068
|
+
"path": "SKILL.md",
|
|
2069
|
+
"content": "---\nname: flakiness-investigation\ndescription: Use when querying Flakiness.io test data, writing FQL filters, finding flaky or failed tests, investigating regressions, analyzing test health, or fixing PR test failures with the flakiness CLI.\n---\n\n# Flakiness Investigation\n\nUse the `flakiness` CLI to query and analyze test data from Flakiness.io.\n\n## Prerequisites\n\n- The `flakiness` CLI must be available in PATH (installed via `npm install -g flakiness` or `npx flakiness`).\n- A project must be specified: `--project <org/project>` or set `FLAKINESS_PROJECT` env var.\n- Authentication: run `flakiness auth login` first, or set `FLAKINESS_ACCESS_TOKEN`.\n\n## Core command\n\n```bash\nflakiness list tests --project <org/project> [--pr <number>] [--branch <name>] [--fql <query>] [--sort <axis>] [--sort-dir asc|desc] [--page <n>] [--page-size <n>]\n```\n\n### Scope options\n\n| Flag | Effect |\n|------|--------|\n| *(none)* | Default branch \u2014 tests from the day of its head commit (in the project's timezone) |\n| `--pr <number>` | Pull request \u2014 tests from the merge-commit of the PR branch into the target branch |\n| `--branch <name>` | Named branch \u2014 tests from the day of its head commit (in the project's timezone) |\n\n`--pr` and `--branch` are mutually exclusive.\n\n### Status semantics with `--pr`\n\nWhen querying a PR, the status values have specific meaning:\n\n| Status | Meaning |\n|--------|---------|\n| `regressed` | Test was passing on the target branch but fails in this PR \u2014 **caused by the PR** |\n| `failed` | Test also fails on the target branch \u2014 **pre-existing failure, not caused by the PR** |\n| `flaked` | Test failed but passed on retry \u2014 flaky, not a real failure |\n| `passed` | Test passes |\n| `skipped` | Test was skipped |\n\n### Sort axes\n\n- `outcome` \u2014 sort by test outcome severity (default)\n- `flip_rate` \u2014 sort by flip rate (how often a test flips between pass/fail)\n- `duration` \u2014 sort by test duration\n- `duration_trend` \u2014 sort by duration trend\n- `name` \u2014 sort alphabetically\n\n### Common queries\n\n| Goal | FQL + flags |\n|------|-------------|\n| Flaky tests | `--fql 'flip>0%' --sort flip_rate --sort-dir desc` |\n| Very flaky (>50%) | `--fql 'flip>50%' --sort flip_rate --sort-dir desc` |\n| Failed tests | `--fql 's:failed' --sort outcome --sort-dir desc` |\n| Regressions | `--fql 's:regressed'` |\n| All broken tests | `--fql 'status:(failed, regressed)'` |\n| Slow tests | `--fql 'd>5s' --sort duration --sort-dir desc` |\n| Tests matching error | `--fql '$timeout'` |\n| Tests in file | `--fql 'f:login.spec.ts'` |\n\n## FQL (Filter Query Language)\n\nFull reference: [references/fql.md](references/fql.md)\n\nKey rules:\n- Multiple tokens combine with AND: `s:failed f:e2e` means \"failed AND in e2e files\"\n- Prefix with `-` to exclude: `-#smoke` excludes smoke-tagged tests\n- Same filter type uses OR: `status:(failed, regressed)` means \"failed OR regressed\"\n- Quote values with spaces: `f:'tests/e2e checkout'`\n\n### Filter types\n\n| Filter | Syntax | Example |\n|--------|--------|---------|\n| Text search | `<text>` | `login` |\n| Status | `s:<status>` | `s:failed`, `status:(failed, regressed)` |\n| File | `f:<path>` | `f:login.spec.ts` |\n| Error | `$<text>` | `$timeout` |\n| Tag | `#<tag>` | `#smoke` |\n| Duration | `d><time>` | `d>2s`, `d<=500ms` |\n| Flip rate | `flip><pct>` | `flip>0%`, `fr>50%` |\n| Annotation | `@<type>` | `@skip` |\n\n### Status values\n\n`passed`, `failed`, `flaked`, `skipped`, `regressed`\n\n## Workflow: Fix My PR Tests\n\nWhen a user asks to fix failing tests in a PR, follow these steps:\n\n1. **Fetch regressions from the PR:**\n ```bash\n flakiness list tests --project <org/project> --pr <number> --fql 's:regressed' --page-size 50\n ```\n Tests with status `regressed` were passing on the target branch but fail in this PR \u2014 these are **caused by the PR** and must be fixed.\n\n2. **Analyze the output:** Look at the error messages, file paths, and test names to understand what the PR broke.\n\n3. **Fix the regressions** by reading the reported file paths and error messages, then making targeted code changes.\n\n4. **Optionally check pre-existing failures:** Tests with status `failed` also fail on the target branch and are not caused by the PR. You can list them with:\n ```bash\n flakiness list tests --project <org/project> --pr <number> --fql 's:failed' --page-size 50\n ```\n These are informational \u2014 fixing them is a bonus, not a requirement.\n\n5. **Ignore flakes:** Tests with status `flaked` failed but passed on retry \u2014 they are flaky and not actionable in the context of a PR fix.\n\n## Workflow tips\n\n1. Start broad: `flakiness list tests --project <org/project> --page-size 20`\n2. Filter down with FQL based on what you're investigating\n3. Use `--page-size 50` or higher to see more results at once\n4. Combine filters: `--fql 's:failed $timeout f:e2e -#smoke'`\n\nMore recipes: [references/recipes.md](references/recipes.md)\n"
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
"path": "references/fql.md",
|
|
2073
|
+
"content": "# FQL Reference\n\nUse `--fql <query>` with `flakiness list tests` to filter tests before sorting and pagination.\n\n## Core rules\n\n- Multiple tokens are combined with `AND` by default.\n- Prefix a token with `-` to exclude matches.\n- Use quotes for spaces or special characters.\n- Repeating the same filter type behaves like `OR`, for example `status:(failed, regressed)`.\n\n## Filter types\n\n| Filter | Examples | Meaning |\n| --- | --- | --- |\n| Test text | `login` | Matches test titles, suite titles, and file path |\n| Status | `s:failed`, `status:(failed, regressed)` | Matches test status |\n| File | `f:login.spec.ts`, `file:e2e` | Matches file path |\n| Line | `:120`, `>200`, `<=50` | Matches line number |\n| Error | `$timeout`, `$'network error'` | Matches error text |\n| Annotation | `@skip`, `@'fails in ci'` | Matches annotation type or description |\n| Tag | `#smoke` | Matches test tags |\n| Duration | `d>2s`, `duration<=500ms` | Matches duration |\n| Flip rate | `flip>0%`, `fr<=50%` | Matches flip rate |\n\n## Status values\n\n- `passed`\n- `failed`\n- `flaked`\n- `skipped`\n- `regressed`\n\n## Examples\n\n```fql\ns:flaked\nstatus:(failed, regressed)\nf:e2e d>5s\nflip>0%\nfr>50%\n$timeout -#smoke\n@skip f:'tests/e2e checkout'\n```\n\n## Quoting\n\n- Single and double quotes both work.\n- Escape quotes and backslashes with JSON-style escaping.\n\n```fql\nf:'some path'\n$\"say \\\"hello\\\"\"\n'can\\'t touch this'\n```\n\n## Flip rate\n\nValues are percentages (0\u2013100). The `%` suffix is optional.\n\n```fql\nflip>0%\nflip>=50\nfr<=25%\n```\n\n## Duration units\n\n- `ns`\n- `ms` or no suffix\n- `s`\n- `m`\n- `h`\n\n```fql\nd>100\nd>1.5s\nduration<=2m\n```\n"
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
"path": "references/recipes.md",
|
|
2077
|
+
"content": "# Investigation Recipes\n\nReplace `myorg/myproject` with the target project slug, or set `FLAKINESS_PROJECT`.\n\n## Query a pull request\n\nShows tests from the merge-commit of the PR branch into the target branch.\n\n```bash\nflakiness list tests --project myorg/myproject --pr 42\n```\n\n## Find PR regressions (caused by the PR)\n\nTests marked `regressed` were passing on the target branch but fail in this PR.\n\n```bash\nflakiness list tests --project myorg/myproject --pr 42 --fql 's:regressed'\n```\n\n## Find pre-existing failures in a PR\n\nTests marked `failed` also fail on the target branch \u2014 not caused by the PR.\n\n```bash\nflakiness list tests --project myorg/myproject --pr 42 --fql 's:failed'\n```\n\n## Find all broken tests in a PR\n\nIncludes both PR-caused regressions and pre-existing failures.\n\n```bash\nflakiness list tests --project myorg/myproject --pr 42 --fql 'status:(failed, regressed)' --page-size 50\n```\n\n## Query a specific branch\n\nShows tests from the day of the branch's head commit, in the project's timezone.\n\n```bash\nflakiness list tests --project myorg/myproject --branch feature/login\n```\n\n## Find all flaky tests\n\n```bash\nflakiness list tests --project myorg/myproject --fql 'flip>0%' --sort flip_rate --sort-dir desc\n```\n\n## Find the most flaky tests (>50% flip rate)\n\n```bash\nflakiness list tests --project myorg/myproject --fql 'flip>50%' --sort flip_rate --sort-dir desc\n```\n\n## Show failed tests\n\n```bash\nflakiness list tests --project myorg/myproject --fql 's:failed' --sort outcome --sort-dir desc\n```\n\n## Show regressions\n\n```bash\nflakiness list tests --project myorg/myproject --fql 's:regressed'\n```\n\n## Show all currently broken tests\n\n```bash\nflakiness list tests --project myorg/myproject --fql 'status:(failed, regressed)' --sort outcome --sort-dir desc\n```\n\n## Show flaked tests (passed on retry)\n\n```bash\nflakiness list tests --project myorg/myproject --fql 's:flaked' --sort flip_rate --sort-dir desc\n```\n\n## Find slow tests\n\n```bash\nflakiness list tests --project myorg/myproject --fql 'd>5s' --sort duration --sort-dir desc\n```\n\n## Find tests by error text\n\n```bash\nflakiness list tests --project myorg/myproject --fql '$timeout'\nflakiness list tests --project myorg/myproject --fql '$\"network error\"'\n```\n\n## Combine filters\n\nFailed tests in e2e files, excluding smoke-tagged tests:\n\n```bash\nflakiness list tests --project myorg/myproject --fql 's:failed f:e2e -#smoke'\n```\n\n## Narrow to a specific file\n\n```bash\nflakiness list tests --project myorg/myproject --fql 'f:login.spec.ts'\n```\n\n## Find tests with a specific annotation\n\n```bash\nflakiness list tests --project myorg/myproject --fql '@skip'\nflakiness list tests --project myorg/myproject --fql '@fixme'\n```\n"
|
|
2078
|
+
}
|
|
2079
|
+
]
|
|
2080
|
+
}
|
|
2081
|
+
];
|
|
2082
|
+
|
|
2083
|
+
// src/cli/cmd-skills-install.ts
|
|
2084
|
+
var AGENTS = ["claude", "codex", "cursor"];
|
|
2085
|
+
function projectRoot() {
|
|
2086
|
+
try {
|
|
2087
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
2088
|
+
} catch {
|
|
2089
|
+
return process.cwd();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async function cmdSkillsInstall(options) {
|
|
2093
|
+
for (const skill of BUNDLED_SKILLS) {
|
|
2094
|
+
const dest = path5.join(projectRoot(), `.${options.agent}`, "skills", skill.name);
|
|
2095
|
+
await fs5.promises.rm(dest, { recursive: true, force: true });
|
|
2096
|
+
for (const file of skill.files) {
|
|
2097
|
+
const filePath = path5.join(dest, file.path);
|
|
2098
|
+
await fs5.promises.mkdir(path5.dirname(filePath), { recursive: true });
|
|
2099
|
+
await fs5.promises.writeFile(filePath, file.content);
|
|
2100
|
+
}
|
|
2101
|
+
console.log(`${chalk3.green("\u2713")} Installed ${chalk3.bold(skill.name)} \u2192 ${chalk3.dim(dest)}`);
|
|
2102
|
+
}
|
|
2103
|
+
console.log("\nRestart your agent to pick up new skills.");
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1568
2106
|
// src/cli/cmd-upload.ts
|
|
1569
2107
|
import { readReport, uploadReport } from "@flakiness/sdk";
|
|
1570
|
-
import
|
|
1571
|
-
import
|
|
2108
|
+
import chalk4 from "chalk";
|
|
2109
|
+
import fs6 from "fs/promises";
|
|
1572
2110
|
import ora2 from "ora";
|
|
1573
|
-
import
|
|
1574
|
-
var warn = (txt) => console.warn(
|
|
1575
|
-
var err = (txt) => console.error(
|
|
2111
|
+
import path6 from "path";
|
|
2112
|
+
var warn = (txt) => console.warn(chalk4.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
2113
|
+
var err = (txt) => console.error(chalk4.red(`[flakiness.io] Error: ${txt}`));
|
|
1576
2114
|
async function cmdUpload(relativePaths, options) {
|
|
1577
2115
|
const total = relativePaths.length;
|
|
1578
2116
|
const spinner = total > 1 ? ora2("Uploading reports:").start() : void 0;
|
|
@@ -1589,14 +2127,14 @@ async function cmdUpload(relativePaths, options) {
|
|
|
1589
2127
|
return sharedAuth;
|
|
1590
2128
|
}
|
|
1591
2129
|
for (const relativePath of relativePaths) {
|
|
1592
|
-
const fullPath =
|
|
1593
|
-
const stat = await
|
|
2130
|
+
const fullPath = path6.resolve(relativePath);
|
|
2131
|
+
const stat = await fs6.stat(fullPath).catch(() => void 0);
|
|
1594
2132
|
if (!stat) {
|
|
1595
2133
|
spinner?.stop();
|
|
1596
2134
|
err(`Path ${fullPath} is not accessible!`);
|
|
1597
2135
|
process.exit(1);
|
|
1598
2136
|
}
|
|
1599
|
-
const reportDir = stat.isDirectory() ? fullPath :
|
|
2137
|
+
const reportDir = stat.isDirectory() ? fullPath : path6.dirname(fullPath);
|
|
1600
2138
|
const { report, attachments, missingAttachments } = await readReport(reportDir);
|
|
1601
2139
|
if (missingAttachments.length)
|
|
1602
2140
|
warn(`Missing ${missingAttachments.length} attachments`);
|
|
@@ -1629,20 +2167,57 @@ var log2 = debug2("fk:cli");
|
|
|
1629
2167
|
var loggedInEndpoint = await UserSession.load().then((session) => session?.endpoint());
|
|
1630
2168
|
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
1631
2169
|
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(loggedInEndpoint ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
|
|
1632
|
-
var mustFlakinessProject = new Option("--project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT").makeOptionMandatory();
|
|
1633
|
-
var optFlakinessProject = new Option("--project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT");
|
|
2170
|
+
var mustFlakinessProject = new Option("-p, --project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT").makeOptionMandatory();
|
|
2171
|
+
var optFlakinessProject = new Option("-p, --project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT");
|
|
1634
2172
|
async function runCommand(callback) {
|
|
1635
2173
|
try {
|
|
1636
2174
|
await callback();
|
|
1637
|
-
} catch (
|
|
1638
|
-
if (!(
|
|
1639
|
-
throw
|
|
1640
|
-
log2(
|
|
1641
|
-
console.error(
|
|
2175
|
+
} catch (e2) {
|
|
2176
|
+
if (!(e2 instanceof Error))
|
|
2177
|
+
throw e2;
|
|
2178
|
+
log2(e2);
|
|
2179
|
+
console.error(e2.message);
|
|
1642
2180
|
process.exit(1);
|
|
1643
2181
|
}
|
|
1644
2182
|
}
|
|
1645
2183
|
var program = new Command().name("flakiness").description("Flakiness CLI tool").version(package_default.version);
|
|
2184
|
+
var list = program.command("list").description("Query test data");
|
|
2185
|
+
var optTestsSort = new Option("--sort <sort>", `A sort axis for tests`).choices([...WireTypes.SORT_AXES.tests]).default("outcome");
|
|
2186
|
+
var optTestsSortDir = new Option("--sort-dir <sort-dir>", "Sort direction for tests").choices(["asc", "desc"]).default("desc");
|
|
2187
|
+
var optTestsPage = new Option("--page <page>", "1-based page number").argParser((value) => {
|
|
2188
|
+
const parsed = parseInt(value, 10);
|
|
2189
|
+
if (isNaN(parsed) || parsed < 1)
|
|
2190
|
+
throw new Error("page must be a number >= 1");
|
|
2191
|
+
return parsed;
|
|
2192
|
+
}).default(1);
|
|
2193
|
+
var optTestsPageSize = new Option("--page-size <page-size>", "Number of tests per page").argParser((value) => {
|
|
2194
|
+
const parsed = parseInt(value, 10);
|
|
2195
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 300)
|
|
2196
|
+
throw new Error("page-size must be a number in range [1, 300]");
|
|
2197
|
+
return parsed;
|
|
2198
|
+
}).default(20);
|
|
2199
|
+
var optTestsFQL = new Option("--fql <query>", "Filter Query Language (FQL) expression for tests. Learn about FQL: https://flakiness.io/docs/concepts/fql/");
|
|
2200
|
+
var optTestsPR = new Option("--pr <number>", "Show tests from a specific pull request").argParser((value) => {
|
|
2201
|
+
const parsed = parseInt(value, 10);
|
|
2202
|
+
if (isNaN(parsed) || parsed < 1)
|
|
2203
|
+
throw new Error("PR number must be a positive integer");
|
|
2204
|
+
return parsed;
|
|
2205
|
+
}).conflicts("branch");
|
|
2206
|
+
var optTestsBranch = new Option("--branch <name>", "Show tests from a specific branch").conflicts("pr");
|
|
2207
|
+
list.command("tests").description("Query tests data. Defaults to the default branch (day of head commit in project timezone). Use --pr for PR merge-commit tests, or --branch for a named branch.").addOption(optTestsPage).addOption(optTestsPageSize).addOption(optTestsSort).addOption(optTestsSortDir).addOption(optTestsFQL).addOption(optTestsPR).addOption(optTestsBranch).addOption(mustFlakinessProject).addOption(optEndpoint).addOption(optAccessToken).action(async (options) => runCommand(async () => {
|
|
2208
|
+
await cmdListTests({
|
|
2209
|
+
page: options.page,
|
|
2210
|
+
pageSize: options.pageSize,
|
|
2211
|
+
sort: options.sort,
|
|
2212
|
+
sortDir: options.sortDir,
|
|
2213
|
+
fql: options.fql,
|
|
2214
|
+
pr: options.pr,
|
|
2215
|
+
branch: options.branch,
|
|
2216
|
+
endpoint: options.endpoint,
|
|
2217
|
+
accessToken: options.accessToken,
|
|
2218
|
+
flakinessProject: options.project
|
|
2219
|
+
});
|
|
2220
|
+
}));
|
|
1646
2221
|
var auth = program.command("auth").description("Manage Device OAuth");
|
|
1647
2222
|
auth.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
|
|
1648
2223
|
await cmdAuthLogin(options.endpoint);
|
|
@@ -1653,6 +2228,11 @@ auth.command("logout").description("Logout from current session").action(async (
|
|
|
1653
2228
|
auth.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
|
|
1654
2229
|
await cmdAuthWhoami();
|
|
1655
2230
|
}));
|
|
2231
|
+
var skills = program.command("skills").description("Manage agent skills");
|
|
2232
|
+
var optAgent = new Option("--agent <agent>", "Target agent").choices(AGENTS).makeOptionMandatory();
|
|
2233
|
+
skills.command("install").description("Install bundled skills into the project").addOption(optAgent).action(async (options) => runCommand(async () => {
|
|
2234
|
+
await cmdSkillsInstall({ agent: options.agent });
|
|
2235
|
+
}));
|
|
1656
2236
|
program.command("access").description("Check access to a Flakiness.io project").addOption(mustFlakinessProject).addOption(optAccessToken).addOption(optEndpoint).option("--json", "Output result as JSON").option("-q, --quiet", "Suppress output, only set exit code").action(async (options) => runCommand(async () => {
|
|
1657
2237
|
await cmdAccess({
|
|
1658
2238
|
flakinessProject: options.project,
|
|
@@ -1704,7 +2284,7 @@ program.command("upload").description("Upload Flakiness report to the flakiness.
|
|
|
1704
2284
|
});
|
|
1705
2285
|
var optViewerUrl = new Option("-e, --viewer-url <url>", "A URL where report viewer is deployed").default("https://report.flakiness.io").env("FLAKINESS_ENDPOINT");
|
|
1706
2286
|
program.command("show").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`. (default: flakiness-report)").addOption(optViewerUrl).action(async (arg, options) => runCommand(async () => {
|
|
1707
|
-
const dir =
|
|
2287
|
+
const dir = path7.resolve(arg ?? "flakiness-report");
|
|
1708
2288
|
await showReport(dir, {
|
|
1709
2289
|
reportViewerUrl: options.viewerUrl
|
|
1710
2290
|
});
|