@flakiness/sdk 0.95.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/cli.js +1199 -380
- package/lib/cli/cmd-convert.js +8 -9
- package/lib/cli/cmd-download.js +9 -459
- package/lib/cli/cmd-link.js +83 -268
- package/lib/cli/cmd-login.js +18 -230
- package/lib/cli/cmd-logout.js +22 -230
- package/lib/cli/cmd-serve.js +479 -1
- package/lib/cli/cmd-status.js +83 -268
- package/lib/cli/cmd-unlink.js +67 -32
- package/lib/cli/cmd-upload-playwright-json.js +49 -253
- package/lib/cli/cmd-upload.js +90 -287
- package/lib/cli/cmd-whoami.js +16 -230
- package/lib/{flakinessLink.js → flakinessConfig.js} +64 -33
- package/lib/flakinessSession.js +16 -230
- package/lib/junit.js +8 -9
- package/lib/localGit.js +43 -0
- package/lib/localReportApi.js +40 -0
- package/lib/localReportServer.js +300 -0
- package/lib/playwright-test.js +470 -309
- package/lib/playwrightJSONReport.js +11 -12
- package/lib/reportUploader.js +45 -247
- package/lib/serverapi.js +10 -230
- package/lib/utils.js +45 -15
- package/package.json +15 -5
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cli.js
CHANGED
|
@@ -1,238 +1,751 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
CONTINUE: 100,
|
|
19
|
-
SWITCHING_PROTOCOLS: 101,
|
|
20
|
-
PROCESSING: 102,
|
|
21
|
-
EARLY_HINTS: 103
|
|
22
|
-
},
|
|
23
|
-
Success: {
|
|
24
|
-
OK: 200,
|
|
25
|
-
CREATED: 201,
|
|
26
|
-
ACCEPTED: 202,
|
|
27
|
-
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
28
|
-
NO_CONTENT: 204,
|
|
29
|
-
RESET_CONTENT: 205,
|
|
30
|
-
PARTIAL_CONTENT: 206,
|
|
31
|
-
MULTI_STATUS: 207
|
|
32
|
-
},
|
|
33
|
-
Redirection: {
|
|
34
|
-
MULTIPLE_CHOICES: 300,
|
|
35
|
-
MOVED_PERMANENTLY: 301,
|
|
36
|
-
MOVED_TEMPORARILY: 302,
|
|
37
|
-
SEE_OTHER: 303,
|
|
38
|
-
NOT_MODIFIED: 304,
|
|
39
|
-
USE_PROXY: 305,
|
|
40
|
-
TEMPORARY_REDIRECT: 307,
|
|
41
|
-
PERMANENT_REDIRECT: 308
|
|
42
|
-
},
|
|
43
|
-
ClientErrors: {
|
|
44
|
-
BAD_REQUEST: 400,
|
|
45
|
-
UNAUTHORIZED: 401,
|
|
46
|
-
PAYMENT_REQUIRED: 402,
|
|
47
|
-
FORBIDDEN: 403,
|
|
48
|
-
NOT_FOUND: 404,
|
|
49
|
-
METHOD_NOT_ALLOWED: 405,
|
|
50
|
-
NOT_ACCEPTABLE: 406,
|
|
51
|
-
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
52
|
-
REQUEST_TIMEOUT: 408,
|
|
53
|
-
CONFLICT: 409,
|
|
54
|
-
GONE: 410,
|
|
55
|
-
LENGTH_REQUIRED: 411,
|
|
56
|
-
PRECONDITION_FAILED: 412,
|
|
57
|
-
REQUEST_TOO_LONG: 413,
|
|
58
|
-
REQUEST_URI_TOO_LONG: 414,
|
|
59
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
60
|
-
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
61
|
-
EXPECTATION_FAILED: 417,
|
|
62
|
-
IM_A_TEAPOT: 418,
|
|
63
|
-
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
64
|
-
METHOD_FAILURE: 420,
|
|
65
|
-
MISDIRECTED_REQUEST: 421,
|
|
66
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
67
|
-
LOCKED: 423,
|
|
68
|
-
FAILED_DEPENDENCY: 424,
|
|
69
|
-
UPGRADE_REQUIRED: 426,
|
|
70
|
-
PRECONDITION_REQUIRED: 428,
|
|
71
|
-
TOO_MANY_REQUESTS: 429,
|
|
72
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
73
|
-
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
74
|
-
},
|
|
75
|
-
ServerErrors: {
|
|
76
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
77
|
-
NOT_IMPLEMENTED: 501,
|
|
78
|
-
BAD_GATEWAY: 502,
|
|
79
|
-
SERVICE_UNAVAILABLE: 503,
|
|
80
|
-
GATEWAY_TIMEOUT: 504,
|
|
81
|
-
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
82
|
-
INSUFFICIENT_STORAGE: 507,
|
|
83
|
-
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const AllErrorCodes = {
|
|
87
|
-
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
88
|
-
...TypedHTTP2.StatusCodes.ServerErrors
|
|
89
|
-
};
|
|
90
|
-
class HttpError extends Error {
|
|
91
|
-
constructor(status, message) {
|
|
92
|
-
super(message);
|
|
93
|
-
this.status = status;
|
|
3
|
+
// ../node_modules/vlq/src/index.js
|
|
4
|
+
var char_to_integer = {};
|
|
5
|
+
var integer_to_char = {};
|
|
6
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
|
|
7
|
+
char_to_integer[char] = i;
|
|
8
|
+
integer_to_char[i] = char;
|
|
9
|
+
});
|
|
10
|
+
function decode(string) {
|
|
11
|
+
let result = [];
|
|
12
|
+
let shift = 0;
|
|
13
|
+
let value = 0;
|
|
14
|
+
for (let i = 0; i < string.length; i += 1) {
|
|
15
|
+
let integer = char_to_integer[string[i]];
|
|
16
|
+
if (integer === void 0) {
|
|
17
|
+
throw new Error("Invalid character (" + string[i] + ")");
|
|
94
18
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
19
|
+
const has_continuation_bit = integer & 32;
|
|
20
|
+
integer &= 31;
|
|
21
|
+
value += integer << shift;
|
|
22
|
+
if (has_continuation_bit) {
|
|
23
|
+
shift += 5;
|
|
24
|
+
} else {
|
|
25
|
+
const should_negate = value & 1;
|
|
26
|
+
value >>>= 1;
|
|
27
|
+
if (should_negate) {
|
|
28
|
+
result.push(value === 0 ? -2147483648 : -value);
|
|
29
|
+
} else {
|
|
30
|
+
result.push(value);
|
|
31
|
+
}
|
|
32
|
+
value = shift = 0;
|
|
99
33
|
}
|
|
100
34
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function encode(value) {
|
|
38
|
+
if (typeof value === "number") {
|
|
39
|
+
return encode_integer(value);
|
|
40
|
+
}
|
|
41
|
+
let result = "";
|
|
42
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
43
|
+
result += encode_integer(value[i]);
|
|
104
44
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function encode_integer(num) {
|
|
48
|
+
let result = "";
|
|
49
|
+
if (num < 0) {
|
|
50
|
+
num = -num << 1 | 1;
|
|
51
|
+
} else {
|
|
52
|
+
num <<= 1;
|
|
53
|
+
}
|
|
54
|
+
do {
|
|
55
|
+
let clamped = num & 31;
|
|
56
|
+
num >>>= 5;
|
|
57
|
+
if (num > 0) {
|
|
58
|
+
clamped |= 32;
|
|
59
|
+
}
|
|
60
|
+
result += integer_to_char[clamped];
|
|
61
|
+
} while (num > 0);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ../server/lib/common/heap.js
|
|
66
|
+
var Heap = class _Heap {
|
|
67
|
+
constructor(_cmp, elements = []) {
|
|
68
|
+
this._cmp = _cmp;
|
|
69
|
+
this._heap = elements.map(([element, score]) => ({ element, score }));
|
|
70
|
+
for (let idx = this._heap.length - 1; idx >= 0; --idx)
|
|
71
|
+
this._down(idx);
|
|
72
|
+
}
|
|
73
|
+
static createMin(elements = []) {
|
|
74
|
+
return new _Heap((a, b) => a - b, elements);
|
|
75
|
+
}
|
|
76
|
+
static createMax(elements = []) {
|
|
77
|
+
return new _Heap((a, b) => b - a, elements);
|
|
78
|
+
}
|
|
79
|
+
_heap = [];
|
|
80
|
+
_up(idx) {
|
|
81
|
+
const e = this._heap[idx];
|
|
82
|
+
while (idx > 0) {
|
|
83
|
+
const parentIdx = idx - 1 >>> 1;
|
|
84
|
+
if (this._cmp(this._heap[parentIdx].score, e.score) <= 0)
|
|
85
|
+
break;
|
|
86
|
+
this._heap[idx] = this._heap[parentIdx];
|
|
87
|
+
idx = parentIdx;
|
|
88
|
+
}
|
|
89
|
+
this._heap[idx] = e;
|
|
90
|
+
}
|
|
91
|
+
_down(idx) {
|
|
92
|
+
const N = this._heap.length;
|
|
93
|
+
const e = this._heap[idx];
|
|
94
|
+
while (true) {
|
|
95
|
+
const leftIdx = idx * 2 + 1;
|
|
96
|
+
const rightIdx = idx * 2 + 2;
|
|
97
|
+
let smallestIdx;
|
|
98
|
+
if (leftIdx < N && rightIdx < N) {
|
|
99
|
+
smallestIdx = this._cmp(this._heap[leftIdx].score, this._heap[rightIdx].score) < 0 ? leftIdx : rightIdx;
|
|
100
|
+
} else if (leftIdx < N) {
|
|
101
|
+
smallestIdx = leftIdx;
|
|
102
|
+
} else if (rightIdx < N) {
|
|
103
|
+
smallestIdx = rightIdx;
|
|
104
|
+
} else {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (this._cmp(e.score, this._heap[smallestIdx].score) < 0)
|
|
108
|
+
break;
|
|
109
|
+
this._heap[idx] = this._heap[smallestIdx];
|
|
110
|
+
idx = smallestIdx;
|
|
111
|
+
}
|
|
112
|
+
this._heap[idx] = e;
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
push(element, score) {
|
|
115
|
+
this._heap.push({ element, score });
|
|
116
|
+
this._up(this._heap.length - 1);
|
|
112
117
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return response.status >= 400 && response.status < 600;
|
|
118
|
+
get size() {
|
|
119
|
+
return this._heap.length;
|
|
116
120
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return { status };
|
|
121
|
+
peekEntry() {
|
|
122
|
+
return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
|
|
120
123
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
popEntry() {
|
|
125
|
+
if (!this._heap.length)
|
|
126
|
+
return void 0;
|
|
127
|
+
const entry = this._heap[0];
|
|
128
|
+
const last = this._heap.pop();
|
|
129
|
+
if (!this._heap.length)
|
|
130
|
+
return [entry.element, entry.score];
|
|
131
|
+
this._heap[0] = last;
|
|
132
|
+
this._down(0);
|
|
133
|
+
return [entry.element, entry.score];
|
|
127
134
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ../server/lib/common/sequence.js
|
|
138
|
+
var Sequence = class _Sequence {
|
|
139
|
+
constructor(_seek, length) {
|
|
140
|
+
this._seek = _seek;
|
|
141
|
+
this.length = length;
|
|
142
|
+
}
|
|
143
|
+
static fromList(a) {
|
|
144
|
+
return new _Sequence(
|
|
145
|
+
function(pos) {
|
|
146
|
+
return {
|
|
147
|
+
next() {
|
|
148
|
+
if (pos >= a.length)
|
|
149
|
+
return { done: true, value: void 0 };
|
|
150
|
+
return { done: false, value: a[pos++] };
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
a.length
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
static chain(seqs) {
|
|
158
|
+
const leftsums = [];
|
|
159
|
+
let length = 0;
|
|
160
|
+
for (let i = 0; i < seqs.length; ++i) {
|
|
161
|
+
length += seqs[i].length;
|
|
162
|
+
leftsums.push(length);
|
|
163
|
+
}
|
|
164
|
+
return new _Sequence(
|
|
165
|
+
function(fromIdx) {
|
|
166
|
+
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
167
|
+
let idx = _Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
|
|
168
|
+
if (idx >= seqs.length) {
|
|
169
|
+
return {
|
|
170
|
+
next: () => ({ done: true, value: void 0 })
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
let it = seqs[idx].seek(idx > 0 ? fromIdx - leftsums[idx - 1] : fromIdx);
|
|
174
|
+
return {
|
|
175
|
+
next() {
|
|
176
|
+
let result = it.next();
|
|
177
|
+
while (result.done && ++idx < seqs.length) {
|
|
178
|
+
it = seqs[idx].seek(0);
|
|
179
|
+
result = it.next();
|
|
180
|
+
}
|
|
181
|
+
return result.done ? result : { done: false, value: [result.value, idx] };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
length
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
static merge(sequences, cmp) {
|
|
189
|
+
const length = sequences.reduce((acc, seq) => acc + seq.length, 0);
|
|
190
|
+
return new _Sequence(
|
|
191
|
+
function(fromIdx) {
|
|
192
|
+
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
193
|
+
const offsets = quickAdvance(sequences, cmp, fromIdx);
|
|
194
|
+
const entries = [];
|
|
195
|
+
for (let i = 0; i < sequences.length; ++i) {
|
|
196
|
+
const seq = sequences[i];
|
|
197
|
+
const it = seq.seek(offsets[i]);
|
|
198
|
+
const itval = it.next();
|
|
199
|
+
if (!itval.done)
|
|
200
|
+
entries.push([it, itval.value]);
|
|
201
|
+
}
|
|
202
|
+
const heap = new Heap(cmp, entries);
|
|
203
|
+
return {
|
|
204
|
+
next() {
|
|
205
|
+
if (!heap.size)
|
|
206
|
+
return { done: true, value: void 0 };
|
|
207
|
+
++fromIdx;
|
|
208
|
+
const [it, e] = heap.popEntry();
|
|
209
|
+
const itval = it.next();
|
|
210
|
+
if (!itval.done)
|
|
211
|
+
heap.push(it, itval.value);
|
|
212
|
+
return { done: false, value: e };
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
length
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
static EMPTY = new _Sequence(function* () {
|
|
220
|
+
}, 0);
|
|
221
|
+
seek(idx) {
|
|
222
|
+
const it = this._seek(idx);
|
|
223
|
+
it[Symbol.iterator] = () => it;
|
|
224
|
+
return it;
|
|
225
|
+
}
|
|
226
|
+
get(idx) {
|
|
227
|
+
return this.seek(idx).next().value;
|
|
228
|
+
}
|
|
229
|
+
map(mapper) {
|
|
230
|
+
const originalSeek = this._seek;
|
|
231
|
+
return new _Sequence(
|
|
232
|
+
function(idx) {
|
|
233
|
+
const it = originalSeek(idx);
|
|
234
|
+
return {
|
|
235
|
+
next() {
|
|
236
|
+
const next = it.next();
|
|
237
|
+
if (next.done)
|
|
238
|
+
return next;
|
|
239
|
+
return { done: false, value: mapper(next.value) };
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
this.length
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
/** Number of elements in sequence that are <= comparator. Only works on sorted sequences. */
|
|
247
|
+
partitionPoint(predicate) {
|
|
248
|
+
let lo = 0, hi = this.length;
|
|
249
|
+
while (lo < hi) {
|
|
250
|
+
const mid = lo + hi >>> 1;
|
|
251
|
+
if (predicate(this.get(mid)))
|
|
252
|
+
lo = mid + 1;
|
|
253
|
+
else
|
|
254
|
+
hi = mid;
|
|
255
|
+
}
|
|
256
|
+
return lo;
|
|
131
257
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
258
|
+
};
|
|
259
|
+
function quickAdvance(sequences, cmp, k) {
|
|
260
|
+
const offsets = new Map(sequences.map((s) => [s, 0]));
|
|
261
|
+
while (offsets.size && k > 0) {
|
|
262
|
+
const t2 = offsets.size;
|
|
263
|
+
const x = Math.max(Math.floor(k / t2 / 2), 1);
|
|
264
|
+
const entries = [];
|
|
265
|
+
for (const [seq, offset] of offsets) {
|
|
266
|
+
if (offset + x <= seq.length)
|
|
267
|
+
entries.push([seq, seq.get(offset + x - 1)]);
|
|
268
|
+
}
|
|
269
|
+
const heap = new Heap(cmp, entries);
|
|
270
|
+
while (heap.size && k > 0 && (x === 1 || k >= x * t2)) {
|
|
271
|
+
k -= x;
|
|
272
|
+
const [seq] = heap.popEntry();
|
|
273
|
+
const offset = offsets.get(seq) + x;
|
|
274
|
+
if (offset === seq.length)
|
|
275
|
+
offsets.delete(seq);
|
|
276
|
+
else
|
|
277
|
+
offsets.set(seq, offset);
|
|
278
|
+
if (offset + x <= seq.length)
|
|
279
|
+
heap.push(seq, seq.get(offset + x - 1));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return sequences.map((seq) => offsets.get(seq) ?? seq.length);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ../server/lib/common/ranges.js
|
|
286
|
+
var Ranges;
|
|
287
|
+
((Ranges2) => {
|
|
288
|
+
Ranges2.EMPTY = [];
|
|
289
|
+
Ranges2.FULL = [-Infinity, Infinity];
|
|
290
|
+
function isFull(ranges) {
|
|
291
|
+
return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
|
|
292
|
+
}
|
|
293
|
+
Ranges2.isFull = isFull;
|
|
294
|
+
function compress(ranges) {
|
|
295
|
+
if (!ranges.length)
|
|
296
|
+
return "";
|
|
297
|
+
if (isInfinite(ranges))
|
|
298
|
+
throw new Error("Compression of infinite ranges is not supported");
|
|
299
|
+
const prepared = [];
|
|
300
|
+
let last = ranges[0] - 1;
|
|
301
|
+
prepared.push(last);
|
|
302
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
303
|
+
if (ranges[i] === ranges[i + 1]) {
|
|
304
|
+
prepared.push(-(ranges[i] - last));
|
|
305
|
+
} else {
|
|
306
|
+
prepared.push(ranges[i] - last);
|
|
307
|
+
prepared.push(ranges[i + 1] - ranges[i]);
|
|
308
|
+
}
|
|
309
|
+
last = ranges[i + 1];
|
|
310
|
+
}
|
|
311
|
+
return encode(prepared);
|
|
312
|
+
}
|
|
313
|
+
Ranges2.compress = compress;
|
|
314
|
+
function decompress(compressed) {
|
|
315
|
+
if (!compressed.length)
|
|
316
|
+
return [];
|
|
317
|
+
const prepared = decode(compressed);
|
|
318
|
+
const result = [];
|
|
319
|
+
let last = prepared[0];
|
|
320
|
+
for (let i = 1; i < prepared.length; ++i) {
|
|
321
|
+
if (prepared[i] < 0) {
|
|
322
|
+
result.push(-prepared[i] + last);
|
|
323
|
+
result.push(-prepared[i] + last);
|
|
324
|
+
last -= prepared[i];
|
|
325
|
+
} else {
|
|
326
|
+
result.push(prepared[i] + last);
|
|
327
|
+
last += prepared[i];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
Ranges2.decompress = decompress;
|
|
333
|
+
function toString(ranges) {
|
|
334
|
+
const tokens = [];
|
|
335
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
336
|
+
if (ranges[i] === ranges[i + 1])
|
|
337
|
+
tokens.push(ranges[i]);
|
|
338
|
+
else
|
|
339
|
+
tokens.push(`${ranges[i]}-${ranges[i + 1]}`);
|
|
340
|
+
}
|
|
341
|
+
if (!tokens.length)
|
|
342
|
+
return `[]`;
|
|
343
|
+
return `[ ` + tokens.join(", ") + ` ]`;
|
|
344
|
+
}
|
|
345
|
+
Ranges2.toString = toString;
|
|
346
|
+
function popInplace(ranges) {
|
|
347
|
+
if (isInfinite(ranges))
|
|
348
|
+
throw new Error("cannot pop from infinite ranges!");
|
|
349
|
+
const last = ranges.at(-1);
|
|
350
|
+
const prelast = ranges.at(-2);
|
|
351
|
+
if (last === void 0 || prelast === void 0)
|
|
352
|
+
return void 0;
|
|
353
|
+
if (last === prelast) {
|
|
354
|
+
ranges.pop();
|
|
355
|
+
ranges.pop();
|
|
356
|
+
} else {
|
|
357
|
+
ranges[ranges.length - 1] = last - 1;
|
|
358
|
+
}
|
|
359
|
+
return last;
|
|
360
|
+
}
|
|
361
|
+
Ranges2.popInplace = popInplace;
|
|
362
|
+
function* iterate(ranges) {
|
|
363
|
+
if (isInfinite(ranges))
|
|
364
|
+
throw new Error("cannot iterate infinite ranges!");
|
|
365
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
366
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
367
|
+
yield j;
|
|
368
|
+
}
|
|
135
369
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
370
|
+
Ranges2.iterate = iterate;
|
|
371
|
+
function toSortedList(ranges) {
|
|
372
|
+
if (isInfinite(ranges))
|
|
373
|
+
throw new Error("cannot convert infinite ranges!");
|
|
374
|
+
const list = [];
|
|
375
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
376
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
377
|
+
list.push(j);
|
|
140
378
|
}
|
|
141
|
-
|
|
142
|
-
|
|
379
|
+
return list;
|
|
380
|
+
}
|
|
381
|
+
Ranges2.toSortedList = toSortedList;
|
|
382
|
+
function toInt32Array(ranges) {
|
|
383
|
+
if (isInfinite(ranges))
|
|
384
|
+
throw new Error("cannot convert infinite ranges!");
|
|
385
|
+
const result = new Int32Array(cardinality(ranges));
|
|
386
|
+
let idx = 0;
|
|
387
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
388
|
+
for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
|
|
389
|
+
result[idx++] = j;
|
|
143
390
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
Ranges2.toInt32Array = toInt32Array;
|
|
394
|
+
function fromList(x) {
|
|
395
|
+
for (let i = 0; i < x.length - 1; ++i) {
|
|
396
|
+
if (x[i] > x[i + 1]) {
|
|
397
|
+
x = x.toSorted((a, b) => a - b);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return fromSortedList(x);
|
|
402
|
+
}
|
|
403
|
+
Ranges2.fromList = fromList;
|
|
404
|
+
function from(x) {
|
|
405
|
+
return [x, x];
|
|
406
|
+
}
|
|
407
|
+
Ranges2.from = from;
|
|
408
|
+
function fromSortedList(sorted) {
|
|
409
|
+
const ranges = [];
|
|
410
|
+
let rangeStart = 0;
|
|
411
|
+
for (let i = 1; i <= sorted.length; ++i) {
|
|
412
|
+
if (i < sorted.length && sorted[i] - sorted[i - 1] <= 1)
|
|
413
|
+
continue;
|
|
414
|
+
ranges.push(sorted[rangeStart], sorted[i - 1]);
|
|
415
|
+
rangeStart = i;
|
|
416
|
+
}
|
|
417
|
+
return ranges;
|
|
418
|
+
}
|
|
419
|
+
Ranges2.fromSortedList = fromSortedList;
|
|
420
|
+
function isInfinite(ranges) {
|
|
421
|
+
return ranges.length > 0 && (Object.is(ranges[0], Infinity) || Object.is(ranges[ranges.length - 1], Infinity));
|
|
422
|
+
}
|
|
423
|
+
Ranges2.isInfinite = isInfinite;
|
|
424
|
+
function includes(ranges, e) {
|
|
425
|
+
if (!ranges.length)
|
|
426
|
+
return false;
|
|
427
|
+
if (e < ranges[0] || ranges[ranges.length - 1] < e)
|
|
428
|
+
return false;
|
|
429
|
+
if (ranges.length < 17) {
|
|
430
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
431
|
+
if (ranges[i] <= e && e <= ranges[i + 1])
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
let lo = 0, hi = ranges.length;
|
|
437
|
+
while (lo < hi) {
|
|
438
|
+
const mid = lo + hi >>> 1;
|
|
439
|
+
if (ranges[mid] === e)
|
|
440
|
+
return true;
|
|
441
|
+
if (ranges[mid] < e)
|
|
442
|
+
lo = mid + 1;
|
|
443
|
+
else
|
|
444
|
+
hi = mid;
|
|
445
|
+
}
|
|
446
|
+
return (lo & 1) !== 0;
|
|
447
|
+
}
|
|
448
|
+
Ranges2.includes = includes;
|
|
449
|
+
function cardinality(ranges) {
|
|
450
|
+
if (isInfinite(ranges))
|
|
451
|
+
return Infinity;
|
|
452
|
+
let sum = 0;
|
|
453
|
+
for (let i = 0; i < ranges.length - 1; i += 2)
|
|
454
|
+
sum += ranges[i + 1] - ranges[i] + 1;
|
|
455
|
+
return sum;
|
|
456
|
+
}
|
|
457
|
+
Ranges2.cardinality = cardinality;
|
|
458
|
+
function offset(ranges, offset2) {
|
|
459
|
+
return ranges.map((x) => x + offset2);
|
|
460
|
+
}
|
|
461
|
+
Ranges2.offset = offset;
|
|
462
|
+
function intersect(ranges1, ranges2) {
|
|
463
|
+
const ranges = [];
|
|
464
|
+
if (!ranges1.length || !ranges2.length)
|
|
465
|
+
return ranges;
|
|
466
|
+
if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
|
|
467
|
+
return ranges;
|
|
468
|
+
let p1 = 0;
|
|
469
|
+
let p2 = 0;
|
|
470
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
471
|
+
if (ranges1[p1 + 1] < ranges2[p2]) {
|
|
472
|
+
p1 += 2;
|
|
473
|
+
let offset2 = 1;
|
|
474
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
|
|
475
|
+
offset2 <<= 1;
|
|
476
|
+
p1 += offset2 >> 1 << 1;
|
|
477
|
+
} else if (ranges2[p2 + 1] < ranges1[p1]) {
|
|
478
|
+
p2 += 2;
|
|
479
|
+
let offset2 = 1;
|
|
480
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
|
|
481
|
+
offset2 <<= 1;
|
|
482
|
+
p2 += offset2 >> 1 << 1;
|
|
483
|
+
} else {
|
|
484
|
+
const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
|
|
485
|
+
const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
|
|
486
|
+
ranges.push(Math.max(a1, b1), Math.min(a2, b2));
|
|
487
|
+
if (a2 < b2) {
|
|
488
|
+
p1 += 2;
|
|
489
|
+
} else if (a2 > b2) {
|
|
490
|
+
p2 += 2;
|
|
491
|
+
} else {
|
|
492
|
+
p1 += 2;
|
|
493
|
+
p2 += 2;
|
|
152
494
|
}
|
|
153
|
-
}
|
|
495
|
+
}
|
|
154
496
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
497
|
+
return ranges;
|
|
498
|
+
}
|
|
499
|
+
Ranges2.intersect = intersect;
|
|
500
|
+
function capAt(ranges, cap) {
|
|
501
|
+
const result = [];
|
|
502
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
503
|
+
const start = ranges[i];
|
|
504
|
+
const end = ranges[i + 1];
|
|
505
|
+
if (start > cap)
|
|
506
|
+
break;
|
|
507
|
+
if (end <= cap) {
|
|
508
|
+
result.push(start, end);
|
|
509
|
+
} else {
|
|
510
|
+
result.push(start, cap);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
160
513
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
Ranges2.capAt = capAt;
|
|
517
|
+
function isIntersecting(ranges1, ranges2) {
|
|
518
|
+
if (!ranges1.length || !ranges2.length)
|
|
519
|
+
return false;
|
|
520
|
+
if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
|
|
521
|
+
return false;
|
|
522
|
+
let p1 = 0;
|
|
523
|
+
let p2 = 0;
|
|
524
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
525
|
+
const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
|
|
526
|
+
const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
|
|
527
|
+
if (a2 < b1) {
|
|
528
|
+
p1 += 2;
|
|
529
|
+
let offset2 = 1;
|
|
530
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
|
|
531
|
+
offset2 <<= 1;
|
|
532
|
+
p1 += offset2 >> 1 << 1;
|
|
533
|
+
} else if (b2 < a1) {
|
|
534
|
+
p2 += 2;
|
|
535
|
+
let offset2 = 1;
|
|
536
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
|
|
537
|
+
offset2 <<= 1;
|
|
538
|
+
p2 += offset2 >> 1 << 1;
|
|
539
|
+
} else {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
166
542
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
Ranges2.isIntersecting = isIntersecting;
|
|
546
|
+
function complement(r) {
|
|
547
|
+
if (r.length === 0)
|
|
548
|
+
return [-Infinity, Infinity];
|
|
549
|
+
const result = [];
|
|
550
|
+
if (!Object.is(r[0], -Infinity))
|
|
551
|
+
result.push(-Infinity, r[0] - 1);
|
|
552
|
+
for (let i = 1; i < r.length - 2; i += 2)
|
|
553
|
+
result.push(r[i] + 1, r[i + 1] - 1);
|
|
554
|
+
if (!Object.is(r[r.length - 1], Infinity))
|
|
555
|
+
result.push(r[r.length - 1] + 1, Infinity);
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
Ranges2.complement = complement;
|
|
559
|
+
function subtract(ranges1, ranges2) {
|
|
560
|
+
return intersect(ranges1, complement(ranges2));
|
|
561
|
+
}
|
|
562
|
+
Ranges2.subtract = subtract;
|
|
563
|
+
function singleRange(from2, to) {
|
|
564
|
+
return [from2, to];
|
|
565
|
+
}
|
|
566
|
+
Ranges2.singleRange = singleRange;
|
|
567
|
+
function unionAll(ranges) {
|
|
568
|
+
let result = Ranges2.EMPTY;
|
|
569
|
+
for (const r of ranges)
|
|
570
|
+
result = union(result, r);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
Ranges2.unionAll = unionAll;
|
|
574
|
+
function unionAll_2(rangesIterable) {
|
|
575
|
+
const ranges = Array.isArray(rangesIterable) ? rangesIterable : Array.from(rangesIterable);
|
|
576
|
+
if (ranges.length === 0)
|
|
577
|
+
return [];
|
|
578
|
+
if (ranges.length === 1)
|
|
579
|
+
return ranges[0];
|
|
580
|
+
if (ranges.length === 2)
|
|
581
|
+
return union(ranges[0], ranges[1]);
|
|
582
|
+
const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
|
|
583
|
+
const result = [];
|
|
584
|
+
let last;
|
|
585
|
+
for (const interval of seq.seek(0)) {
|
|
586
|
+
if (!last || last[1] + 1 < interval[0]) {
|
|
587
|
+
result.push(interval);
|
|
588
|
+
last = interval;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (last[1] < interval[1])
|
|
592
|
+
last[1] = interval[1];
|
|
593
|
+
}
|
|
594
|
+
return result.flat();
|
|
595
|
+
}
|
|
596
|
+
Ranges2.unionAll_2 = unionAll_2;
|
|
597
|
+
function intersectAll(ranges) {
|
|
598
|
+
if (!ranges.length)
|
|
599
|
+
return Ranges2.EMPTY;
|
|
600
|
+
let result = Ranges2.FULL;
|
|
601
|
+
for (const range of ranges)
|
|
602
|
+
result = Ranges2.intersect(result, range);
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
Ranges2.intersectAll = intersectAll;
|
|
606
|
+
function domain(ranges) {
|
|
607
|
+
if (!ranges.length)
|
|
608
|
+
return void 0;
|
|
609
|
+
return { min: ranges[0], max: ranges[ranges.length - 1] };
|
|
610
|
+
}
|
|
611
|
+
Ranges2.domain = domain;
|
|
612
|
+
function union(ranges1, ranges2) {
|
|
613
|
+
if (!ranges1.length)
|
|
614
|
+
return ranges2;
|
|
615
|
+
if (!ranges2.length)
|
|
616
|
+
return ranges1;
|
|
617
|
+
if (ranges2[0] < ranges1[0])
|
|
618
|
+
[ranges1, ranges2] = [ranges2, ranges1];
|
|
619
|
+
const r = [ranges1[0], ranges1[1]];
|
|
620
|
+
let p1 = 2, p2 = 0;
|
|
621
|
+
while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
|
|
622
|
+
if (ranges1[p1] <= ranges2[p2]) {
|
|
623
|
+
if (r[r.length - 1] + 1 < ranges1[p1]) {
|
|
624
|
+
r.push(ranges1[p1], ranges1[p1 + 1]);
|
|
625
|
+
p1 += 2;
|
|
626
|
+
} else if (r[r.length - 1] < ranges1[p1 + 1]) {
|
|
627
|
+
r[r.length - 1] = ranges1[p1 + 1];
|
|
628
|
+
p1 += 2;
|
|
629
|
+
} else {
|
|
630
|
+
p1 += 2;
|
|
631
|
+
let offset2 = 1;
|
|
632
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
633
|
+
offset2 <<= 1;
|
|
634
|
+
p1 += offset2 >> 1 << 1;
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
if (r[r.length - 1] + 1 < ranges2[p2]) {
|
|
638
|
+
r.push(ranges2[p2], ranges2[p2 + 1]);
|
|
639
|
+
p2 += 2;
|
|
640
|
+
} else if (r[r.length - 1] < ranges2[p2 + 1]) {
|
|
641
|
+
r[r.length - 1] = ranges2[p2 + 1];
|
|
642
|
+
p2 += 2;
|
|
643
|
+
} else {
|
|
644
|
+
p2 += 2;
|
|
645
|
+
let offset2 = 1;
|
|
646
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
|
|
647
|
+
offset2 <<= 1;
|
|
648
|
+
p2 += offset2 >> 1 << 1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
while (p1 < ranges1.length - 1) {
|
|
653
|
+
if (r[r.length - 1] + 1 < ranges1[p1]) {
|
|
654
|
+
r.push(ranges1[p1], ranges1[p1 + 1]);
|
|
655
|
+
p1 += 2;
|
|
656
|
+
} else if (r[r.length - 1] < ranges1[p1 + 1]) {
|
|
657
|
+
r[r.length - 1] = ranges1[p1 + 1];
|
|
658
|
+
p1 += 2;
|
|
659
|
+
} else {
|
|
660
|
+
p1 += 2;
|
|
661
|
+
let offset2 = 1;
|
|
662
|
+
while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
|
|
663
|
+
offset2 <<= 1;
|
|
664
|
+
p1 += offset2 >> 1 << 1;
|
|
665
|
+
}
|
|
172
666
|
}
|
|
667
|
+
while (p2 < ranges2.length - 1) {
|
|
668
|
+
if (r[r.length - 1] + 1 < ranges2[p2]) {
|
|
669
|
+
r.push(ranges2[p2], ranges2[p2 + 1]);
|
|
670
|
+
p2 += 2;
|
|
671
|
+
} else if (r[r.length - 1] < ranges2[p2 + 1]) {
|
|
672
|
+
r[r.length - 1] = ranges2[p2 + 1];
|
|
673
|
+
p2 += 2;
|
|
674
|
+
} else {
|
|
675
|
+
p2 += 2;
|
|
676
|
+
let offset2 = 1;
|
|
677
|
+
while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
|
|
678
|
+
offset2 <<= 1;
|
|
679
|
+
p2 += offset2 >> 1 << 1;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return r;
|
|
173
683
|
}
|
|
174
|
-
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
const method = path9.at(-1);
|
|
178
|
-
const url = new URL(path9.slice(0, path9.length - 1).join("/"), base);
|
|
179
|
-
const signal = options?.signal;
|
|
180
|
-
let body = void 0;
|
|
181
|
-
if (method === "GET" && input)
|
|
182
|
-
url.searchParams.set("input", JSON.stringify(input));
|
|
183
|
-
else if (method !== "GET" && input)
|
|
184
|
-
body = JSON.stringify(input);
|
|
684
|
+
Ranges2.union = union;
|
|
685
|
+
function intervalSequence(ranges) {
|
|
686
|
+
return new Sequence(function(idx) {
|
|
185
687
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
688
|
+
next() {
|
|
689
|
+
if (idx * 2 >= ranges.length)
|
|
690
|
+
return { done: true, value: void 0 };
|
|
691
|
+
const value = [ranges[idx * 2], ranges[idx * 2 + 1]];
|
|
692
|
+
++idx;
|
|
693
|
+
return { done: false, value };
|
|
694
|
+
}
|
|
191
695
|
};
|
|
696
|
+
}, ranges.length >>> 1);
|
|
697
|
+
}
|
|
698
|
+
Ranges2.intervalSequence = intervalSequence;
|
|
699
|
+
function sequence(ranges) {
|
|
700
|
+
let length = 0;
|
|
701
|
+
const leftsums = [];
|
|
702
|
+
for (let i = 0; i < ranges.length - 1; i += 2) {
|
|
703
|
+
length += ranges[i + 1] - ranges[i] + 1;
|
|
704
|
+
leftsums.push(length);
|
|
192
705
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (response.status >= 200 && response.status < 300) {
|
|
213
|
-
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
214
|
-
const text = await response.text();
|
|
215
|
-
return text.length ? JSON.parse(text) : void 0;
|
|
216
|
-
}
|
|
217
|
-
return await response.blob();
|
|
706
|
+
return new Sequence(
|
|
707
|
+
function(fromIdx) {
|
|
708
|
+
fromIdx = Math.max(0, Math.min(length, fromIdx));
|
|
709
|
+
const idx = Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
|
|
710
|
+
const intervals = Ranges2.intervalSequence(ranges);
|
|
711
|
+
const it = intervals.seek(idx);
|
|
712
|
+
const firstInterval = it.next();
|
|
713
|
+
if (firstInterval.done)
|
|
714
|
+
return { next: () => firstInterval };
|
|
715
|
+
let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
|
|
716
|
+
let to = firstInterval.value[1];
|
|
717
|
+
return {
|
|
718
|
+
next() {
|
|
719
|
+
if (from2 > to) {
|
|
720
|
+
const interval = it.next();
|
|
721
|
+
if (interval.done)
|
|
722
|
+
return { done: true, value: void 0 };
|
|
723
|
+
from2 = interval.value[0];
|
|
724
|
+
to = interval.value[1];
|
|
218
725
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
return createProxy();
|
|
726
|
+
return { done: false, value: from2++ };
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
},
|
|
730
|
+
length
|
|
731
|
+
);
|
|
231
732
|
}
|
|
232
|
-
|
|
233
|
-
})(
|
|
733
|
+
Ranges2.sequence = sequence;
|
|
734
|
+
})(Ranges || (Ranges = {}));
|
|
735
|
+
|
|
736
|
+
// src/cli/cli.ts
|
|
737
|
+
import { TypedHTTP as TypedHTTP4 } from "@flakiness/shared/common/typedHttp.js";
|
|
738
|
+
import assert4 from "assert";
|
|
739
|
+
import { Command, Option } from "commander";
|
|
740
|
+
import fs12 from "fs";
|
|
741
|
+
import path10 from "path";
|
|
742
|
+
|
|
743
|
+
// src/flakinessConfig.ts
|
|
744
|
+
import fs2 from "fs";
|
|
745
|
+
import path2 from "path";
|
|
234
746
|
|
|
235
747
|
// src/utils.ts
|
|
748
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
236
749
|
import assert from "assert";
|
|
237
750
|
import { spawnSync } from "child_process";
|
|
238
751
|
import crypto from "crypto";
|
|
@@ -240,14 +753,7 @@ import fs from "fs";
|
|
|
240
753
|
import http from "http";
|
|
241
754
|
import https from "https";
|
|
242
755
|
import os from "os";
|
|
243
|
-
import { posix as posixPath, win32 as win32Path } from "path";
|
|
244
|
-
import util from "util";
|
|
245
|
-
import zlib from "zlib";
|
|
246
|
-
var gzipAsync = util.promisify(zlib.gzip);
|
|
247
|
-
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
248
|
-
var gunzipSync = zlib.gunzipSync;
|
|
249
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
250
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
756
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
251
757
|
async function existsAsync(aPath) {
|
|
252
758
|
return fs.promises.stat(aPath).then(() => true).catch((e) => false);
|
|
253
759
|
}
|
|
@@ -272,6 +778,10 @@ function sha1File(filePath) {
|
|
|
272
778
|
});
|
|
273
779
|
});
|
|
274
780
|
}
|
|
781
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
782
|
+
function errorText(error) {
|
|
783
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
784
|
+
}
|
|
275
785
|
function sha1Buffer(data) {
|
|
276
786
|
const hash = crypto.createHash("sha1");
|
|
277
787
|
hash.update(data);
|
|
@@ -283,9 +793,9 @@ async function retryWithBackoff(job, backoff = []) {
|
|
|
283
793
|
return await job();
|
|
284
794
|
} catch (e) {
|
|
285
795
|
if (e instanceof AggregateError)
|
|
286
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
796
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
287
797
|
else if (e instanceof Error)
|
|
288
|
-
console.error(`[flakiness.io err]`, e
|
|
798
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
289
799
|
else
|
|
290
800
|
console.error(`[flakiness.io err]`, e);
|
|
291
801
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -303,6 +813,7 @@ var httpUtils;
|
|
|
303
813
|
reject = b;
|
|
304
814
|
});
|
|
305
815
|
const protocol = url.startsWith("https") ? https : http;
|
|
816
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
306
817
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
307
818
|
const chunks = [];
|
|
308
819
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -424,6 +935,40 @@ function gitCommitInfo(gitRepo) {
|
|
|
424
935
|
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
425
936
|
return sha.trim();
|
|
426
937
|
}
|
|
938
|
+
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
939
|
+
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
940
|
+
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
941
|
+
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
942
|
+
const missingAttachments = /* @__PURE__ */ new Set();
|
|
943
|
+
FlakinessReport.visitTests(report, (test) => {
|
|
944
|
+
for (const attempt of test.attempts) {
|
|
945
|
+
for (const attachment of attempt.attachments ?? []) {
|
|
946
|
+
const attachmentPath = filenameToPath.get(attachment.id);
|
|
947
|
+
if (!attachmentPath) {
|
|
948
|
+
missingAttachments.add(attachment.id);
|
|
949
|
+
} else {
|
|
950
|
+
attachmentIdToPath.set(attachment.id, {
|
|
951
|
+
contentType: attachment.contentType,
|
|
952
|
+
id: attachment.id,
|
|
953
|
+
path: attachmentPath
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
960
|
+
}
|
|
961
|
+
async function listFilesRecursively(dir, result = []) {
|
|
962
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
963
|
+
for (const entry of entries) {
|
|
964
|
+
const fullPath = path.join(dir, entry.name);
|
|
965
|
+
if (entry.isDirectory())
|
|
966
|
+
await listFilesRecursively(fullPath, result);
|
|
967
|
+
else
|
|
968
|
+
result.push(fullPath);
|
|
969
|
+
}
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
427
972
|
function computeGitRoot(somePathInsideGitRepo) {
|
|
428
973
|
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
429
974
|
cwd: somePathInsideGitRepo,
|
|
@@ -484,7 +1029,75 @@ function createEnvironments(projects) {
|
|
|
484
1029
|
return result;
|
|
485
1030
|
}
|
|
486
1031
|
|
|
1032
|
+
// src/flakinessConfig.ts
|
|
1033
|
+
function createConfigPath(dir) {
|
|
1034
|
+
return path2.join(dir, ".flakiness", "config.json");
|
|
1035
|
+
}
|
|
1036
|
+
var gConfigPath;
|
|
1037
|
+
function ensureConfigPath() {
|
|
1038
|
+
if (!gConfigPath)
|
|
1039
|
+
gConfigPath = computeConfigPath();
|
|
1040
|
+
return gConfigPath;
|
|
1041
|
+
}
|
|
1042
|
+
function computeConfigPath() {
|
|
1043
|
+
for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
|
|
1044
|
+
const configPath = createConfigPath(p);
|
|
1045
|
+
if (fs2.existsSync(configPath))
|
|
1046
|
+
return configPath;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
const gitRoot = computeGitRoot(process.cwd());
|
|
1050
|
+
return createConfigPath(gitRoot);
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
return createConfigPath(process.cwd());
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
var FlakinessConfig = class _FlakinessConfig {
|
|
1056
|
+
constructor(_configPath, _config) {
|
|
1057
|
+
this._configPath = _configPath;
|
|
1058
|
+
this._config = _config;
|
|
1059
|
+
}
|
|
1060
|
+
static async load() {
|
|
1061
|
+
const configPath = ensureConfigPath();
|
|
1062
|
+
const data = await fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
1063
|
+
const json = data ? JSON.parse(data) : {};
|
|
1064
|
+
return new _FlakinessConfig(configPath, json);
|
|
1065
|
+
}
|
|
1066
|
+
static async projectOrDie(session2) {
|
|
1067
|
+
const config = await _FlakinessConfig.load();
|
|
1068
|
+
const projectPublicId = config.projectPublicId();
|
|
1069
|
+
if (!projectPublicId)
|
|
1070
|
+
throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
|
|
1071
|
+
const project = await session2.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
|
|
1072
|
+
if (!project)
|
|
1073
|
+
throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
|
|
1074
|
+
return project;
|
|
1075
|
+
}
|
|
1076
|
+
static createEmpty() {
|
|
1077
|
+
return new _FlakinessConfig(ensureConfigPath(), {});
|
|
1078
|
+
}
|
|
1079
|
+
path() {
|
|
1080
|
+
return this._configPath;
|
|
1081
|
+
}
|
|
1082
|
+
projectPublicId() {
|
|
1083
|
+
return this._config.projectPublicId;
|
|
1084
|
+
}
|
|
1085
|
+
setProjectPublicId(projectId) {
|
|
1086
|
+
this._config.projectPublicId = projectId;
|
|
1087
|
+
}
|
|
1088
|
+
async save() {
|
|
1089
|
+
await fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
|
|
1090
|
+
await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// src/flakinessSession.ts
|
|
1095
|
+
import fs3 from "fs/promises";
|
|
1096
|
+
import os2 from "os";
|
|
1097
|
+
import path3 from "path";
|
|
1098
|
+
|
|
487
1099
|
// src/serverapi.ts
|
|
1100
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
488
1101
|
function createServerAPI(endpoint, options) {
|
|
489
1102
|
endpoint += "/api/";
|
|
490
1103
|
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
@@ -501,24 +1114,30 @@ function createServerAPI(endpoint, options) {
|
|
|
501
1114
|
|
|
502
1115
|
// src/flakinessSession.ts
|
|
503
1116
|
var CONFIG_DIR = (() => {
|
|
504
|
-
const configDir = process.platform === "darwin" ?
|
|
1117
|
+
const configDir = process.platform === "darwin" ? path3.join(os2.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path3.join(os2.homedir(), "AppData", "Roaming", "flakiness") : path3.join(os2.homedir(), ".config", "flakiness");
|
|
505
1118
|
return configDir;
|
|
506
1119
|
})();
|
|
507
|
-
var CONFIG_PATH =
|
|
1120
|
+
var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
|
|
508
1121
|
var FlakinessSession = class _FlakinessSession {
|
|
509
1122
|
constructor(_config) {
|
|
510
1123
|
this._config = _config;
|
|
511
1124
|
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
512
1125
|
}
|
|
1126
|
+
static async loadOrDie() {
|
|
1127
|
+
const session2 = await _FlakinessSession.load();
|
|
1128
|
+
if (!session2)
|
|
1129
|
+
throw new Error(`Please login first with 'npx flakiness login'`);
|
|
1130
|
+
return session2;
|
|
1131
|
+
}
|
|
513
1132
|
static async load() {
|
|
514
|
-
const data = await
|
|
1133
|
+
const data = await fs3.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
515
1134
|
if (!data)
|
|
516
1135
|
return void 0;
|
|
517
1136
|
const json = JSON.parse(data);
|
|
518
1137
|
return new _FlakinessSession(json);
|
|
519
1138
|
}
|
|
520
1139
|
static async remove() {
|
|
521
|
-
await
|
|
1140
|
+
await fs3.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
522
1141
|
}
|
|
523
1142
|
api;
|
|
524
1143
|
endpoint() {
|
|
@@ -531,21 +1150,21 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
531
1150
|
return this._config.token;
|
|
532
1151
|
}
|
|
533
1152
|
async save() {
|
|
534
|
-
await
|
|
535
|
-
await
|
|
1153
|
+
await fs3.mkdir(CONFIG_DIR, { recursive: true });
|
|
1154
|
+
await fs3.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
536
1155
|
}
|
|
537
1156
|
};
|
|
538
1157
|
|
|
539
1158
|
// src/cli/cmd-convert.ts
|
|
540
|
-
import
|
|
541
|
-
import
|
|
1159
|
+
import fs5 from "fs/promises";
|
|
1160
|
+
import path5 from "path";
|
|
542
1161
|
|
|
543
1162
|
// src/junit.ts
|
|
544
1163
|
import { FlakinessReport as FK } from "@flakiness/report";
|
|
545
1164
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
546
1165
|
import assert2 from "assert";
|
|
547
|
-
import
|
|
548
|
-
import
|
|
1166
|
+
import fs4 from "fs";
|
|
1167
|
+
import path4 from "path";
|
|
549
1168
|
function getProperties(element) {
|
|
550
1169
|
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
551
1170
|
if (!propertiesNodes.length)
|
|
@@ -587,8 +1206,8 @@ function extractStdout(testcase, stdio) {
|
|
|
587
1206
|
}));
|
|
588
1207
|
}
|
|
589
1208
|
async function parseAttachment(value) {
|
|
590
|
-
let absolutePath =
|
|
591
|
-
if (
|
|
1209
|
+
let absolutePath = path4.resolve(process.cwd(), value);
|
|
1210
|
+
if (fs4.existsSync(absolutePath)) {
|
|
592
1211
|
const id = await sha1File(absolutePath);
|
|
593
1212
|
return {
|
|
594
1213
|
contentType: "image/png",
|
|
@@ -660,7 +1279,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
660
1279
|
id: attachment.id,
|
|
661
1280
|
contentType: attachment.contentType,
|
|
662
1281
|
//TODO: better default names for attachments?
|
|
663
|
-
name: attachment.path ?
|
|
1282
|
+
name: attachment.path ? path4.basename(attachment.path) : `attachment`
|
|
664
1283
|
});
|
|
665
1284
|
} else {
|
|
666
1285
|
annotations.push({
|
|
@@ -735,15 +1354,15 @@ async function parseJUnit(xmls, options) {
|
|
|
735
1354
|
|
|
736
1355
|
// src/cli/cmd-convert.ts
|
|
737
1356
|
async function cmdConvert(junitPath, options) {
|
|
738
|
-
const fullPath =
|
|
739
|
-
if (!await
|
|
1357
|
+
const fullPath = path5.resolve(junitPath);
|
|
1358
|
+
if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
|
|
740
1359
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
741
1360
|
process.exit(1);
|
|
742
1361
|
}
|
|
743
|
-
const stat = await
|
|
1362
|
+
const stat = await fs5.stat(fullPath);
|
|
744
1363
|
let xmlContents = [];
|
|
745
1364
|
if (stat.isFile()) {
|
|
746
|
-
const xmlContent = await
|
|
1365
|
+
const xmlContent = await fs5.readFile(fullPath, "utf-8");
|
|
747
1366
|
xmlContents.push(xmlContent);
|
|
748
1367
|
} else if (stat.isDirectory()) {
|
|
749
1368
|
const xmlFiles = await findXmlFiles(fullPath);
|
|
@@ -753,7 +1372,7 @@ async function cmdConvert(junitPath, options) {
|
|
|
753
1372
|
}
|
|
754
1373
|
console.log(`Found ${xmlFiles.length} XML files`);
|
|
755
1374
|
for (const xmlFile of xmlFiles) {
|
|
756
|
-
const xmlContent = await
|
|
1375
|
+
const xmlContent = await fs5.readFile(xmlFile, "utf-8");
|
|
757
1376
|
xmlContents.push(xmlContent);
|
|
758
1377
|
}
|
|
759
1378
|
} else {
|
|
@@ -777,26 +1396,26 @@ async function cmdConvert(junitPath, options) {
|
|
|
777
1396
|
runStartTimestamp: Date.now(),
|
|
778
1397
|
runDuration: 0
|
|
779
1398
|
});
|
|
780
|
-
await
|
|
1399
|
+
await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
|
|
781
1400
|
console.log("\u2713 Saved report to fkreport.json");
|
|
782
1401
|
if (attachments.length > 0) {
|
|
783
|
-
await
|
|
1402
|
+
await fs5.mkdir("fkattachments", { recursive: true });
|
|
784
1403
|
for (const attachment of attachments) {
|
|
785
1404
|
if (attachment.path) {
|
|
786
|
-
const destPath =
|
|
787
|
-
await
|
|
1405
|
+
const destPath = path5.join("fkattachments", attachment.id);
|
|
1406
|
+
await fs5.copyFile(attachment.path, destPath);
|
|
788
1407
|
} else if (attachment.body) {
|
|
789
|
-
const destPath =
|
|
790
|
-
await
|
|
1408
|
+
const destPath = path5.join("fkattachments", attachment.id);
|
|
1409
|
+
await fs5.writeFile(destPath, attachment.body);
|
|
791
1410
|
}
|
|
792
1411
|
}
|
|
793
1412
|
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
794
1413
|
}
|
|
795
1414
|
}
|
|
796
1415
|
async function findXmlFiles(dir, result = []) {
|
|
797
|
-
const entries = await
|
|
1416
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
798
1417
|
for (const entry of entries) {
|
|
799
|
-
const fullPath =
|
|
1418
|
+
const fullPath = path5.join(dir, entry.name);
|
|
800
1419
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
801
1420
|
result.push(fullPath);
|
|
802
1421
|
else if (entry.isDirectory())
|
|
@@ -807,53 +1426,8 @@ async function findXmlFiles(dir, result = []) {
|
|
|
807
1426
|
|
|
808
1427
|
// src/cli/cmd-download.ts
|
|
809
1428
|
import fs6 from "fs";
|
|
810
|
-
import
|
|
811
|
-
|
|
812
|
-
// src/flakinessLink.ts
|
|
813
|
-
import fs5 from "fs/promises";
|
|
814
|
-
import path4 from "path";
|
|
815
|
-
var GIT_ROOT = computeGitRoot(process.cwd());
|
|
816
|
-
var CONFIG_DIR2 = path4.join(GIT_ROOT, ".flakiness");
|
|
817
|
-
var CONFIG_PATH2 = path4.join(CONFIG_DIR2, "config.json");
|
|
818
|
-
var FlakinessLink = class _FlakinessLink {
|
|
819
|
-
constructor(_config) {
|
|
820
|
-
this._config = _config;
|
|
821
|
-
}
|
|
822
|
-
static async load() {
|
|
823
|
-
const data = await fs5.readFile(CONFIG_PATH2, "utf-8").catch((e) => void 0);
|
|
824
|
-
if (!data)
|
|
825
|
-
return void 0;
|
|
826
|
-
const json = JSON.parse(data);
|
|
827
|
-
return new _FlakinessLink(json);
|
|
828
|
-
}
|
|
829
|
-
static async remove() {
|
|
830
|
-
await fs5.unlink(CONFIG_PATH2).catch((e) => void 0);
|
|
831
|
-
}
|
|
832
|
-
path() {
|
|
833
|
-
return CONFIG_PATH2;
|
|
834
|
-
}
|
|
835
|
-
projectId() {
|
|
836
|
-
return this._config.projectId;
|
|
837
|
-
}
|
|
838
|
-
async save() {
|
|
839
|
-
await fs5.mkdir(CONFIG_DIR2, { recursive: true });
|
|
840
|
-
await fs5.writeFile(CONFIG_PATH2, JSON.stringify(this._config, null, 2));
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
// src/cli/cmd-download.ts
|
|
845
|
-
async function cmdDownload(runId) {
|
|
846
|
-
const session2 = await FlakinessSession.load();
|
|
847
|
-
if (!session2) {
|
|
848
|
-
console.log(`Please login first`);
|
|
849
|
-
process.exit(1);
|
|
850
|
-
}
|
|
851
|
-
const link = await FlakinessLink.load();
|
|
852
|
-
if (!link) {
|
|
853
|
-
console.log(`Please run 'npx flakiness link' to link to the project`);
|
|
854
|
-
process.exit(1);
|
|
855
|
-
}
|
|
856
|
-
const project = await session2.api.project.getProject.GET({ projectPublicId: link.projectId() });
|
|
1429
|
+
import path6 from "path";
|
|
1430
|
+
async function cmdDownload(session2, project, runId) {
|
|
857
1431
|
const urls = await session2.api.run.downloadURLs.GET({
|
|
858
1432
|
orgSlug: project.org.orgSlug,
|
|
859
1433
|
projectSlug: project.projectSlug,
|
|
@@ -864,7 +1438,7 @@ async function cmdDownload(runId) {
|
|
|
864
1438
|
console.log(`Directory ${rootDir} already exists!`);
|
|
865
1439
|
return;
|
|
866
1440
|
}
|
|
867
|
-
const attachmentsDir =
|
|
1441
|
+
const attachmentsDir = path6.join(rootDir, "attachments");
|
|
868
1442
|
await fs6.promises.mkdir(rootDir, { recursive: true });
|
|
869
1443
|
if (urls.attachmentURLs.length)
|
|
870
1444
|
await fs6.promises.mkdir(attachmentsDir, { recursive: true });
|
|
@@ -872,7 +1446,7 @@ async function cmdDownload(runId) {
|
|
|
872
1446
|
if (!response.ok)
|
|
873
1447
|
throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
|
|
874
1448
|
const reportContent = await response.text();
|
|
875
|
-
await fs6.promises.writeFile(
|
|
1449
|
+
await fs6.promises.writeFile(path6.join(rootDir, "report.json"), reportContent);
|
|
876
1450
|
const attachmentDownloader = async () => {
|
|
877
1451
|
while (urls.attachmentURLs.length) {
|
|
878
1452
|
const url = urls.attachmentURLs.pop();
|
|
@@ -880,8 +1454,8 @@ async function cmdDownload(runId) {
|
|
|
880
1454
|
if (!response2.ok)
|
|
881
1455
|
throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
|
|
882
1456
|
const fileBuffer = Buffer.from(await response2.arrayBuffer());
|
|
883
|
-
const filename =
|
|
884
|
-
await fs6.promises.writeFile(
|
|
1457
|
+
const filename = path6.basename(new URL(url).pathname);
|
|
1458
|
+
await fs6.promises.writeFile(path6.join(attachmentsDir, filename), fileBuffer);
|
|
885
1459
|
}
|
|
886
1460
|
};
|
|
887
1461
|
const workerPromises = [];
|
|
@@ -907,11 +1481,10 @@ async function cmdLink(slug) {
|
|
|
907
1481
|
console.log(`Failed to find project ${slug}`);
|
|
908
1482
|
process.exit(1);
|
|
909
1483
|
}
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
console.log(`\u2713 Link successful! Config saved to ${link.path()}`);
|
|
1484
|
+
const config = FlakinessConfig.createEmpty();
|
|
1485
|
+
config.setProjectPublicId(project.projectPublicId);
|
|
1486
|
+
await config.save();
|
|
1487
|
+
console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
915
1488
|
}
|
|
916
1489
|
|
|
917
1490
|
// ../server/lib/common/knownClientIds.js
|
|
@@ -921,6 +1494,7 @@ var KNOWN_CLIENT_IDS = {
|
|
|
921
1494
|
};
|
|
922
1495
|
|
|
923
1496
|
// src/cli/cmd-login.ts
|
|
1497
|
+
import open from "open";
|
|
924
1498
|
import os3 from "os";
|
|
925
1499
|
async function cmdLogin(endpoint) {
|
|
926
1500
|
const api = createServerAPI(endpoint);
|
|
@@ -928,6 +1502,7 @@ async function cmdLogin(endpoint) {
|
|
|
928
1502
|
clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
|
|
929
1503
|
name: os3.hostname()
|
|
930
1504
|
});
|
|
1505
|
+
await open(new URL(data.verificationUrl, endpoint).href);
|
|
931
1506
|
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
932
1507
|
let token;
|
|
933
1508
|
while (Date.now() < data.deadline) {
|
|
@@ -955,9 +1530,209 @@ async function cmdLogin(endpoint) {
|
|
|
955
1530
|
|
|
956
1531
|
// src/cli/cmd-logout.ts
|
|
957
1532
|
async function cmdLogout() {
|
|
1533
|
+
const session2 = await FlakinessSession.load();
|
|
1534
|
+
if (!session2)
|
|
1535
|
+
return;
|
|
1536
|
+
const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
|
|
1537
|
+
if (currentSession)
|
|
1538
|
+
await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
958
1539
|
await FlakinessSession.remove();
|
|
959
1540
|
}
|
|
960
1541
|
|
|
1542
|
+
// src/cli/cmd-serve.ts
|
|
1543
|
+
import open2 from "open";
|
|
1544
|
+
import path7 from "path";
|
|
1545
|
+
|
|
1546
|
+
// src/localReportServer.ts
|
|
1547
|
+
import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
|
|
1548
|
+
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
1549
|
+
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
1550
|
+
import bodyParser from "body-parser";
|
|
1551
|
+
import compression from "compression";
|
|
1552
|
+
import debug from "debug";
|
|
1553
|
+
import express from "express";
|
|
1554
|
+
import "express-async-errors";
|
|
1555
|
+
import fs8 from "fs";
|
|
1556
|
+
import http2 from "http";
|
|
1557
|
+
|
|
1558
|
+
// src/localGit.ts
|
|
1559
|
+
import { exec } from "child_process";
|
|
1560
|
+
import { promisify } from "util";
|
|
1561
|
+
var execAsync = promisify(exec);
|
|
1562
|
+
async function listLocalCommits(gitRoot, head, count) {
|
|
1563
|
+
const FIELD_SEPARATOR = "|~|";
|
|
1564
|
+
const RECORD_SEPARATOR = "\0";
|
|
1565
|
+
const prettyFormat = [
|
|
1566
|
+
"%H",
|
|
1567
|
+
// %H: Full commit hash
|
|
1568
|
+
"%at",
|
|
1569
|
+
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
1570
|
+
"%an",
|
|
1571
|
+
// %an: Author name
|
|
1572
|
+
"%s"
|
|
1573
|
+
// %s: Subject (the first line of the commit message)
|
|
1574
|
+
].join(FIELD_SEPARATOR);
|
|
1575
|
+
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
1576
|
+
try {
|
|
1577
|
+
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
1578
|
+
if (!stdout) {
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
1582
|
+
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
1583
|
+
return {
|
|
1584
|
+
commitId,
|
|
1585
|
+
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
1586
|
+
// Convert timestamp from seconds to milliseconds
|
|
1587
|
+
author,
|
|
1588
|
+
message,
|
|
1589
|
+
walkIndex: 0
|
|
1590
|
+
};
|
|
1591
|
+
});
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/localReportApi.ts
|
|
1599
|
+
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
1600
|
+
import fs7 from "fs";
|
|
1601
|
+
import { z } from "zod/v4";
|
|
1602
|
+
var t = TypedHTTP2.Router.create();
|
|
1603
|
+
var localReportRouter = {
|
|
1604
|
+
ping: t.get({
|
|
1605
|
+
handler: async () => {
|
|
1606
|
+
return "pong";
|
|
1607
|
+
}
|
|
1608
|
+
}),
|
|
1609
|
+
lastCommits: t.get({
|
|
1610
|
+
handler: async ({ ctx }) => {
|
|
1611
|
+
return ctx.commits;
|
|
1612
|
+
}
|
|
1613
|
+
}),
|
|
1614
|
+
report: {
|
|
1615
|
+
attachment: t.rawMethod("GET", {
|
|
1616
|
+
input: z.object({
|
|
1617
|
+
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
1618
|
+
}),
|
|
1619
|
+
handler: async ({ ctx, input }) => {
|
|
1620
|
+
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
1621
|
+
if (!idx)
|
|
1622
|
+
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
1623
|
+
const buffer = await fs7.promises.readFile(idx.path);
|
|
1624
|
+
return TypedHTTP2.ok(buffer, idx.contentType);
|
|
1625
|
+
}
|
|
1626
|
+
}),
|
|
1627
|
+
json: t.get({
|
|
1628
|
+
handler: async ({ ctx }) => {
|
|
1629
|
+
return ctx.report;
|
|
1630
|
+
}
|
|
1631
|
+
})
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
// src/localReportServer.ts
|
|
1636
|
+
var logHTTPServer = debug("fk:http");
|
|
1637
|
+
var LocalReportServer = class _LocalReportServer {
|
|
1638
|
+
constructor(_server, _port, _authToken) {
|
|
1639
|
+
this._server = _server;
|
|
1640
|
+
this._port = _port;
|
|
1641
|
+
this._authToken = _authToken;
|
|
1642
|
+
}
|
|
1643
|
+
static async create(options) {
|
|
1644
|
+
const app = express();
|
|
1645
|
+
app.set("etag", false);
|
|
1646
|
+
const authToken = `fk-` + randomUUIDBase62();
|
|
1647
|
+
app.use(compression());
|
|
1648
|
+
app.use(bodyParser.json({ limit: 256 * 1024 }));
|
|
1649
|
+
app.use((req, res, next) => {
|
|
1650
|
+
if (!req.path.startsWith("/" + authToken))
|
|
1651
|
+
throw TypedHTTP3.HttpError.withCode("UNAUTHORIZED");
|
|
1652
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
1653
|
+
res.setHeader("Access-Control-Allow-Origin", options.endpoint);
|
|
1654
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
1655
|
+
if (req.method === "OPTIONS") {
|
|
1656
|
+
res.writeHead(204);
|
|
1657
|
+
res.end();
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
|
|
1661
|
+
res.on("close", () => {
|
|
1662
|
+
if (!res.headersSent)
|
|
1663
|
+
logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
|
|
1664
|
+
});
|
|
1665
|
+
next();
|
|
1666
|
+
});
|
|
1667
|
+
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
1668
|
+
router: localReportRouter,
|
|
1669
|
+
createRootContext: async ({ req, res, input }) => {
|
|
1670
|
+
const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
|
|
1671
|
+
const attachmentsDir = options.attachmentsFolder;
|
|
1672
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1673
|
+
if (missingAttachments.length) {
|
|
1674
|
+
const first = missingAttachments.slice(0, 3);
|
|
1675
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
1676
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
1677
|
+
if (missingAttachments.length > 3)
|
|
1678
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
report,
|
|
1682
|
+
commits: await listLocalCommits(process.cwd(), report.commitId, 100),
|
|
1683
|
+
attachmentIdToPath
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
}));
|
|
1687
|
+
app.use((err, req, res, next) => {
|
|
1688
|
+
if (err instanceof TypedHTTP3.HttpError)
|
|
1689
|
+
return res.status(err.status).send({ error: err.message });
|
|
1690
|
+
logHTTPServer(err);
|
|
1691
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
1692
|
+
});
|
|
1693
|
+
const server = http2.createServer(app);
|
|
1694
|
+
server.on("error", (err) => {
|
|
1695
|
+
if (err.code === "ECONNRESET") {
|
|
1696
|
+
logHTTPServer("Client connection reset. Ignoring.");
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
throw err;
|
|
1700
|
+
});
|
|
1701
|
+
const port = await new Promise((resolve) => server.listen(options.port, () => {
|
|
1702
|
+
resolve(server.address().port);
|
|
1703
|
+
}));
|
|
1704
|
+
return new _LocalReportServer(server, port, authToken);
|
|
1705
|
+
}
|
|
1706
|
+
authToken() {
|
|
1707
|
+
return this._authToken;
|
|
1708
|
+
}
|
|
1709
|
+
port() {
|
|
1710
|
+
return this._port;
|
|
1711
|
+
}
|
|
1712
|
+
async dispose() {
|
|
1713
|
+
await new Promise((x) => this._server.close(x));
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
// src/cli/cmd-serve.ts
|
|
1718
|
+
async function cmdServe(reportFolder) {
|
|
1719
|
+
const reportPath = path7.join(reportFolder, "report.json");
|
|
1720
|
+
const session2 = await FlakinessSession.load();
|
|
1721
|
+
const config = await FlakinessConfig.load();
|
|
1722
|
+
const projectPublicId = config.projectPublicId();
|
|
1723
|
+
const project = projectPublicId && session2 ? await session2.api.project.getProject.GET({ projectPublicId }) : void 0;
|
|
1724
|
+
const endpoint = session2?.endpoint() ?? "https://flakiness.io";
|
|
1725
|
+
const server = await LocalReportServer.create({
|
|
1726
|
+
endpoint,
|
|
1727
|
+
port: 9373,
|
|
1728
|
+
reportPath,
|
|
1729
|
+
attachmentsFolder: reportFolder
|
|
1730
|
+
});
|
|
1731
|
+
const reportEndpoint = project ? `${endpoint}/localreport/${project.org.orgSlug}/${project.projectSlug}?port=${server.port()}&token=${server.authToken()}` : `${endpoint}/localreport?port=${server.port()}&token=${server.authToken()}`;
|
|
1732
|
+
console.log(`Navigate to ${reportEndpoint}`);
|
|
1733
|
+
await open2(reportEndpoint);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
961
1736
|
// src/cli/cmd-status.ts
|
|
962
1737
|
async function cmdStatus() {
|
|
963
1738
|
const session2 = await FlakinessSession.load();
|
|
@@ -967,31 +1742,34 @@ async function cmdStatus() {
|
|
|
967
1742
|
}
|
|
968
1743
|
const user = await session2.api.user.whoami.GET();
|
|
969
1744
|
console.log(`user: ${user.userName} (${user.userLogin})`);
|
|
970
|
-
const
|
|
971
|
-
|
|
1745
|
+
const config = await FlakinessConfig.load();
|
|
1746
|
+
const projectPublicId = config.projectPublicId();
|
|
1747
|
+
if (!projectPublicId) {
|
|
972
1748
|
console.log(`project: <not linked>`);
|
|
973
1749
|
return;
|
|
974
1750
|
}
|
|
975
|
-
const project = await session2.api.project.getProject.GET({
|
|
976
|
-
projectPublicId: link.projectId()
|
|
977
|
-
});
|
|
1751
|
+
const project = await session2.api.project.getProject.GET({ projectPublicId });
|
|
978
1752
|
console.log(`project: ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
979
1753
|
}
|
|
980
1754
|
|
|
981
1755
|
// src/cli/cmd-unlink.ts
|
|
982
1756
|
async function cmdUnlink() {
|
|
983
|
-
await
|
|
1757
|
+
const config = await FlakinessConfig.load();
|
|
1758
|
+
if (!config.projectPublicId())
|
|
1759
|
+
return;
|
|
1760
|
+
config.setProjectPublicId(void 0);
|
|
1761
|
+
await config.save();
|
|
984
1762
|
}
|
|
985
1763
|
|
|
986
1764
|
// src/cli/cmd-upload-playwright-json.ts
|
|
987
|
-
import
|
|
988
|
-
import
|
|
1765
|
+
import fs10 from "fs/promises";
|
|
1766
|
+
import path8 from "path";
|
|
989
1767
|
|
|
990
1768
|
// src/playwrightJSONReport.ts
|
|
991
|
-
import { FlakinessReport as FK2, FlakinessReport } from "@flakiness/report";
|
|
992
|
-
import
|
|
1769
|
+
import { FlakinessReport as FK2, FlakinessReport as FlakinessReport2 } from "@flakiness/report";
|
|
1770
|
+
import debug2 from "debug";
|
|
993
1771
|
import { posix as posixPath2 } from "path";
|
|
994
|
-
var dlog =
|
|
1772
|
+
var dlog = debug2("flakiness:json-report");
|
|
995
1773
|
var PlaywrightJSONReport;
|
|
996
1774
|
((PlaywrightJSONReport2) => {
|
|
997
1775
|
function collectMetadata(somePathInsideProject = process.cwd()) {
|
|
@@ -1023,7 +1801,7 @@ var PlaywrightJSONReport;
|
|
|
1023
1801
|
};
|
|
1024
1802
|
const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
|
|
1025
1803
|
const report = {
|
|
1026
|
-
category:
|
|
1804
|
+
category: FlakinessReport2.CATEGORY_PLAYWRIGHT,
|
|
1027
1805
|
commitId: metadata.commitId,
|
|
1028
1806
|
configPath,
|
|
1029
1807
|
url: metadata.runURL,
|
|
@@ -1163,9 +1941,10 @@ function parseJSONError(context, error) {
|
|
|
1163
1941
|
}
|
|
1164
1942
|
|
|
1165
1943
|
// src/reportUploader.ts
|
|
1166
|
-
import
|
|
1944
|
+
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
1945
|
+
import assert3 from "assert";
|
|
1946
|
+
import fs9 from "fs";
|
|
1167
1947
|
import { URL as URL2 } from "url";
|
|
1168
|
-
import { brotliCompressSync as brotliCompressSync2 } from "zlib";
|
|
1169
1948
|
var ReportUploader = class _ReportUploader {
|
|
1170
1949
|
static optionsFromEnv(overrides) {
|
|
1171
1950
|
const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
@@ -1210,11 +1989,10 @@ var ReportUpload = class {
|
|
|
1210
1989
|
this._options = options;
|
|
1211
1990
|
this._report = report;
|
|
1212
1991
|
this._attachments = attachments;
|
|
1213
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
1992
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
1214
1993
|
}
|
|
1215
1994
|
async upload(options) {
|
|
1216
1995
|
const response = await this._api.run.startUpload.POST({
|
|
1217
|
-
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
1218
1996
|
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
1219
1997
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
1220
1998
|
if (response?.error || !response.result)
|
|
@@ -1225,7 +2003,7 @@ var ReportUpload = class {
|
|
|
1225
2003
|
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
1226
2004
|
if (!uploadURL)
|
|
1227
2005
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
1228
|
-
return this._uploadAttachment(attachment, uploadURL);
|
|
2006
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
1229
2007
|
})
|
|
1230
2008
|
]);
|
|
1231
2009
|
const response2 = await this._api.run.completeUpload.POST({
|
|
@@ -1235,7 +2013,7 @@ var ReportUpload = class {
|
|
|
1235
2013
|
return { success: true, reportUrl: url };
|
|
1236
2014
|
}
|
|
1237
2015
|
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
1238
|
-
const compressed = syncCompression ?
|
|
2016
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
1239
2017
|
const headers = {
|
|
1240
2018
|
"Content-Type": "application/json",
|
|
1241
2019
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -1252,11 +2030,34 @@ var ReportUpload = class {
|
|
|
1252
2030
|
await responseDataPromise;
|
|
1253
2031
|
}, HTTP_BACKOFF);
|
|
1254
2032
|
}
|
|
1255
|
-
async _uploadAttachment(attachment, uploadUrl) {
|
|
1256
|
-
const
|
|
2033
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
2034
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
2035
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
2036
|
+
if (!compressable && attachment.path) {
|
|
2037
|
+
const attachmentPath = attachment.path;
|
|
2038
|
+
await retryWithBackoff(async () => {
|
|
2039
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
2040
|
+
url: uploadUrl,
|
|
2041
|
+
headers: {
|
|
2042
|
+
"Content-Type": attachment.contentType,
|
|
2043
|
+
"Content-Length": (await fs9.promises.stat(attachmentPath)).size + ""
|
|
2044
|
+
},
|
|
2045
|
+
method: "put"
|
|
2046
|
+
});
|
|
2047
|
+
fs9.createReadStream(attachmentPath).pipe(request);
|
|
2048
|
+
await responseDataPromise;
|
|
2049
|
+
}, HTTP_BACKOFF);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs9.promises.readFile(attachment.path) : void 0;
|
|
2053
|
+
assert3(buffer);
|
|
2054
|
+
const encoding = compressable ? "br" : void 0;
|
|
2055
|
+
if (compressable)
|
|
2056
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
1257
2057
|
const headers = {
|
|
1258
2058
|
"Content-Type": attachment.contentType,
|
|
1259
|
-
"Content-Length":
|
|
2059
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
2060
|
+
"Content-Encoding": encoding
|
|
1260
2061
|
};
|
|
1261
2062
|
await retryWithBackoff(async () => {
|
|
1262
2063
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
@@ -1264,13 +2065,8 @@ var ReportUpload = class {
|
|
|
1264
2065
|
headers,
|
|
1265
2066
|
method: "put"
|
|
1266
2067
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
} else {
|
|
1270
|
-
if (attachment.body)
|
|
1271
|
-
request.write(attachment.body);
|
|
1272
|
-
request.end();
|
|
1273
|
-
}
|
|
2068
|
+
request.write(buffer);
|
|
2069
|
+
request.end();
|
|
1274
2070
|
await responseDataPromise;
|
|
1275
2071
|
}, HTTP_BACKOFF);
|
|
1276
2072
|
}
|
|
@@ -1278,12 +2074,12 @@ var ReportUpload = class {
|
|
|
1278
2074
|
|
|
1279
2075
|
// src/cli/cmd-upload-playwright-json.ts
|
|
1280
2076
|
async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
1281
|
-
const fullPath =
|
|
1282
|
-
if (!await
|
|
2077
|
+
const fullPath = path8.resolve(relativePath);
|
|
2078
|
+
if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1283
2079
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1284
2080
|
process.exit(1);
|
|
1285
2081
|
}
|
|
1286
|
-
const text = await
|
|
2082
|
+
const text = await fs10.readFile(fullPath, "utf-8");
|
|
1287
2083
|
const playwrightJson = JSON.parse(text);
|
|
1288
2084
|
const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
|
|
1289
2085
|
extractAttachments: true
|
|
@@ -1304,38 +2100,18 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
|
1304
2100
|
}
|
|
1305
2101
|
|
|
1306
2102
|
// src/cli/cmd-upload.ts
|
|
1307
|
-
import
|
|
1308
|
-
import
|
|
1309
|
-
import path7 from "path";
|
|
2103
|
+
import fs11 from "fs/promises";
|
|
2104
|
+
import path9 from "path";
|
|
1310
2105
|
async function cmdUpload(relativePath, options) {
|
|
1311
|
-
const fullPath =
|
|
1312
|
-
if (!await
|
|
2106
|
+
const fullPath = path9.resolve(relativePath);
|
|
2107
|
+
if (!await fs11.access(fullPath, fs11.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1313
2108
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1314
2109
|
process.exit(1);
|
|
1315
2110
|
}
|
|
1316
|
-
const
|
|
1317
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
1318
|
-
const attachmentIdToPath = new Map(attachmentFiles.map((file) => [path7.basename(file), file]));
|
|
1319
|
-
const text = await fs9.readFile(fullPath, "utf-8");
|
|
2111
|
+
const text = await fs11.readFile(fullPath, "utf-8");
|
|
1320
2112
|
const report = JSON.parse(text);
|
|
1321
|
-
const
|
|
1322
|
-
const missingAttachments =
|
|
1323
|
-
FlakinessReport2.visitTests(report, (test) => {
|
|
1324
|
-
for (const attempt of test.attempts) {
|
|
1325
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
1326
|
-
const file = attachmentIdToPath.get(attachment.id);
|
|
1327
|
-
if (!file) {
|
|
1328
|
-
missingAttachments.push(attachment);
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
attachments.push({
|
|
1332
|
-
contentType: attachment.contentType,
|
|
1333
|
-
id: attachment.id,
|
|
1334
|
-
path: file
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
});
|
|
2113
|
+
const attachmentsDir = options.attachmentsDir ?? path9.dirname(fullPath);
|
|
2114
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1339
2115
|
if (missingAttachments.length && !options.ignoreMissingAttachments) {
|
|
1340
2116
|
console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
|
|
1341
2117
|
process.exit(1);
|
|
@@ -1344,7 +2120,7 @@ async function cmdUpload(relativePath, options) {
|
|
|
1344
2120
|
flakinessAccessToken: options.accessToken,
|
|
1345
2121
|
flakinessEndpoint: options.endpoint
|
|
1346
2122
|
});
|
|
1347
|
-
const upload = uploader.createUpload(report,
|
|
2123
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
1348
2124
|
const uploadResult = await upload.upload();
|
|
1349
2125
|
if (!uploadResult.success) {
|
|
1350
2126
|
console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
@@ -1352,17 +2128,6 @@ async function cmdUpload(relativePath, options) {
|
|
|
1352
2128
|
console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
1353
2129
|
}
|
|
1354
2130
|
}
|
|
1355
|
-
async function listFilesRecursively(dir, result = []) {
|
|
1356
|
-
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
1357
|
-
for (const entry of entries) {
|
|
1358
|
-
const fullPath = path7.join(dir, entry.name);
|
|
1359
|
-
if (entry.isDirectory())
|
|
1360
|
-
await listFilesRecursively(fullPath, result);
|
|
1361
|
-
else
|
|
1362
|
-
result.push(fullPath);
|
|
1363
|
-
}
|
|
1364
|
-
return result;
|
|
1365
|
-
}
|
|
1366
2131
|
|
|
1367
2132
|
// src/cli/cmd-whoami.ts
|
|
1368
2133
|
async function cmdWhoami() {
|
|
@@ -1378,7 +2143,7 @@ async function cmdWhoami() {
|
|
|
1378
2143
|
|
|
1379
2144
|
// src/cli/cli.ts
|
|
1380
2145
|
var session = await FlakinessSession.load();
|
|
1381
|
-
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").
|
|
2146
|
+
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
1382
2147
|
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
|
|
1383
2148
|
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
1384
2149
|
var optIgnoreMissingAttachments = new Option("--ignore-missing-attachments", "Upload report even if some attachments are missing.").default("", "Same directory as the report file");
|
|
@@ -1388,20 +2153,42 @@ async function runCommand(callback) {
|
|
|
1388
2153
|
} catch (e) {
|
|
1389
2154
|
if (!(e instanceof Error))
|
|
1390
2155
|
throw e;
|
|
1391
|
-
|
|
1392
|
-
console.error(e.stack);
|
|
1393
|
-
else
|
|
1394
|
-
console.error(e.message);
|
|
2156
|
+
console.error(errorText(e));
|
|
1395
2157
|
process.exit(1);
|
|
1396
2158
|
}
|
|
1397
2159
|
}
|
|
1398
|
-
var PACKAGE_JSON = JSON.parse(
|
|
2160
|
+
var PACKAGE_JSON = JSON.parse(fs12.readFileSync(path10.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
|
|
1399
2161
|
var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
|
|
2162
|
+
async function ensureAccessToken(options) {
|
|
2163
|
+
let accessToken = options.accessToken;
|
|
2164
|
+
if (!accessToken) {
|
|
2165
|
+
const config = await FlakinessConfig.load();
|
|
2166
|
+
const projectPublicId = config.projectPublicId();
|
|
2167
|
+
if (session && projectPublicId) {
|
|
2168
|
+
try {
|
|
2169
|
+
accessToken = (await session.api.project.getProject.GET({ projectPublicId })).readWriteAccessToken;
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
if (e instanceof TypedHTTP4.HttpError && e.status === 404) {
|
|
2172
|
+
} else {
|
|
2173
|
+
throw e;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
assert4(accessToken, `Please either pass FLAKINESS_ACCESS_TOKEN or run login + link`);
|
|
2179
|
+
return {
|
|
2180
|
+
...options,
|
|
2181
|
+
accessToken
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
1400
2184
|
program.command("upload-playwright-json", { hidden: true }).description("Upload Playwright Test JSON report to the flakiness.io service").argument("<relative-path-to-json>", "Path to the Playwright JSON report file").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePath, options) => runCommand(async () => {
|
|
1401
|
-
await cmdUploadPlaywrightJson(relativePath, options);
|
|
2185
|
+
await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
|
|
1402
2186
|
}));
|
|
1403
|
-
|
|
2187
|
+
var optLink = new Option("--link <org/proj>", "A project to link to");
|
|
2188
|
+
program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
|
|
1404
2189
|
await cmdLogin(options.endpoint);
|
|
2190
|
+
if (options.link)
|
|
2191
|
+
await cmdLink(options.link);
|
|
1405
2192
|
}));
|
|
1406
2193
|
program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
|
|
1407
2194
|
await cmdLogout();
|
|
@@ -1418,14 +2205,46 @@ program.command("unlink").description("Unlink repository from the flakiness proj
|
|
|
1418
2205
|
program.command("status").description("Status repository from the flakiness project").action(async () => runCommand(async () => {
|
|
1419
2206
|
await cmdStatus();
|
|
1420
2207
|
}));
|
|
1421
|
-
|
|
1422
|
-
|
|
2208
|
+
var optRunId = new Option("--run-id <runId>", "RunId flakiness.io access token").argParser((value) => {
|
|
2209
|
+
const parsed = parseInt(value, 10);
|
|
2210
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
2211
|
+
throw new Error("runId must be a number >= 1");
|
|
2212
|
+
}
|
|
2213
|
+
return parsed;
|
|
2214
|
+
});
|
|
2215
|
+
var optSince = new Option("--since <date>", "Start date for filtering").argParser((value) => {
|
|
2216
|
+
const parsed = new Date(value);
|
|
2217
|
+
if (isNaN(parsed.getTime())) {
|
|
2218
|
+
throw new Error("Invalid date format");
|
|
2219
|
+
}
|
|
2220
|
+
return parsed;
|
|
2221
|
+
});
|
|
2222
|
+
program.command("download").description("Download run").addOption(optSince).addOption(optRunId).action(async (options) => runCommand(async () => {
|
|
2223
|
+
const session2 = await FlakinessSession.loadOrDie();
|
|
2224
|
+
const project = await FlakinessConfig.projectOrDie(session2);
|
|
2225
|
+
let runIds = [];
|
|
2226
|
+
if (options.runId) {
|
|
2227
|
+
runIds = [options.runId, options.runId];
|
|
2228
|
+
} else if (options.since) {
|
|
2229
|
+
runIds = await session2.api.project.listRuns.GET({
|
|
2230
|
+
orgSlug: project.org.orgSlug,
|
|
2231
|
+
projectSlug: project.projectSlug,
|
|
2232
|
+
sinceTimestampMs: +options.since
|
|
2233
|
+
});
|
|
2234
|
+
console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
|
|
2235
|
+
}
|
|
2236
|
+
for (const runId of Ranges.iterate(runIds))
|
|
2237
|
+
await cmdDownload(session2, project, runId);
|
|
1423
2238
|
}));
|
|
1424
2239
|
program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-path>", "Path to the Flakiness report file").addOption(optAccessToken).addOption(optEndpoint).addOption(optAttachmentsDir).addOption(optIgnoreMissingAttachments).action(async (relativePath, options) => {
|
|
1425
2240
|
await runCommand(async () => {
|
|
1426
|
-
await cmdUpload(relativePath, options);
|
|
2241
|
+
await cmdUpload(relativePath, await ensureAccessToken(options));
|
|
1427
2242
|
});
|
|
1428
2243
|
});
|
|
2244
|
+
program.command("show-report [report]").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`.").action(async (arg) => runCommand(async () => {
|
|
2245
|
+
const dir = path10.join(process.cwd(), arg ?? "flakiness-report");
|
|
2246
|
+
await cmdServe(dir);
|
|
2247
|
+
}));
|
|
1429
2248
|
program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "default").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").action(async (junitPath, options) => {
|
|
1430
2249
|
await runCommand(async () => {
|
|
1431
2250
|
await cmdConvert(junitPath, options);
|