@flakiness/sdk 0.95.0 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/cli.js +1229 -386
- package/lib/cli/cmd-convert.js +9 -12
- package/lib/cli/cmd-download.js +9 -459
- package/lib/cli/cmd-link.js +84 -271
- package/lib/cli/cmd-login.js +26 -232
- package/lib/cli/cmd-logout.js +22 -230
- package/lib/cli/cmd-show-report.js +488 -0
- package/lib/cli/cmd-status.js +84 -271
- package/lib/cli/cmd-unlink.js +68 -35
- package/lib/cli/cmd-upload-playwright-json.js +52 -257
- package/lib/cli/cmd-upload.js +92 -288
- package/lib/cli/cmd-whoami.js +16 -230
- package/lib/{flakinessLink.js → flakinessConfig.js} +65 -36
- 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 +496 -318
- package/lib/playwrightJSONReport.js +12 -15
- package/lib/reportUploader.js +47 -248
- package/lib/serverapi.js +10 -230
- package/lib/utils.js +46 -18
- package/package.json +16 -5
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cmd-serve.js +0 -7
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);
|
|
104
40
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
41
|
+
let result = "";
|
|
42
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
43
|
+
result += encode_integer(value[i]);
|
|
108
44
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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;
|
|
112
53
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
113
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
push(element, score) {
|
|
115
|
+
this._heap.push({ element, score });
|
|
116
|
+
this._up(this._heap.length - 1);
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
125
|
-
data
|
|
126
|
-
};
|
|
118
|
+
get size() {
|
|
119
|
+
return this._heap.length;
|
|
127
120
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return { status, url };
|
|
121
|
+
peekEntry() {
|
|
122
|
+
return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
|
|
131
123
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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];
|
|
135
134
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
140
163
|
}
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
143
255
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
256
|
+
return lo;
|
|
257
|
+
}
|
|
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
|
+
}
|
|
369
|
+
}
|
|
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);
|
|
378
|
+
}
|
|
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;
|
|
390
|
+
}
|
|
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
|
+
}
|
|
172
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
|
+
}
|
|
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));
|
|
@@ -362,13 +873,11 @@ function shell(command, args, options) {
|
|
|
362
873
|
try {
|
|
363
874
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
364
875
|
if (result.status !== 0) {
|
|
365
|
-
console.log(result);
|
|
366
|
-
console.log(options);
|
|
367
876
|
return void 0;
|
|
368
877
|
}
|
|
369
878
|
return result.stdout.trim();
|
|
370
879
|
} catch (e) {
|
|
371
|
-
console.
|
|
880
|
+
console.error(e);
|
|
372
881
|
return void 0;
|
|
373
882
|
}
|
|
374
883
|
}
|
|
@@ -424,6 +933,40 @@ function gitCommitInfo(gitRepo) {
|
|
|
424
933
|
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
425
934
|
return sha.trim();
|
|
426
935
|
}
|
|
936
|
+
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
937
|
+
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
938
|
+
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
939
|
+
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
940
|
+
const missingAttachments = /* @__PURE__ */ new Set();
|
|
941
|
+
FlakinessReport.visitTests(report, (test) => {
|
|
942
|
+
for (const attempt of test.attempts) {
|
|
943
|
+
for (const attachment of attempt.attachments ?? []) {
|
|
944
|
+
const attachmentPath = filenameToPath.get(attachment.id);
|
|
945
|
+
if (!attachmentPath) {
|
|
946
|
+
missingAttachments.add(attachment.id);
|
|
947
|
+
} else {
|
|
948
|
+
attachmentIdToPath.set(attachment.id, {
|
|
949
|
+
contentType: attachment.contentType,
|
|
950
|
+
id: attachment.id,
|
|
951
|
+
path: attachmentPath
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
958
|
+
}
|
|
959
|
+
async function listFilesRecursively(dir, result = []) {
|
|
960
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
961
|
+
for (const entry of entries) {
|
|
962
|
+
const fullPath = path.join(dir, entry.name);
|
|
963
|
+
if (entry.isDirectory())
|
|
964
|
+
await listFilesRecursively(fullPath, result);
|
|
965
|
+
else
|
|
966
|
+
result.push(fullPath);
|
|
967
|
+
}
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
427
970
|
function computeGitRoot(somePathInsideGitRepo) {
|
|
428
971
|
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
429
972
|
cwd: somePathInsideGitRepo,
|
|
@@ -484,7 +1027,75 @@ function createEnvironments(projects) {
|
|
|
484
1027
|
return result;
|
|
485
1028
|
}
|
|
486
1029
|
|
|
1030
|
+
// src/flakinessConfig.ts
|
|
1031
|
+
function createConfigPath(dir) {
|
|
1032
|
+
return path2.join(dir, ".flakiness", "config.json");
|
|
1033
|
+
}
|
|
1034
|
+
var gConfigPath;
|
|
1035
|
+
function ensureConfigPath() {
|
|
1036
|
+
if (!gConfigPath)
|
|
1037
|
+
gConfigPath = computeConfigPath();
|
|
1038
|
+
return gConfigPath;
|
|
1039
|
+
}
|
|
1040
|
+
function computeConfigPath() {
|
|
1041
|
+
for (let p = process.cwd(); p !== path2.resolve(p, ".."); p = path2.resolve(p, "..")) {
|
|
1042
|
+
const configPath = createConfigPath(p);
|
|
1043
|
+
if (fs2.existsSync(configPath))
|
|
1044
|
+
return configPath;
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const gitRoot = computeGitRoot(process.cwd());
|
|
1048
|
+
return createConfigPath(gitRoot);
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
return createConfigPath(process.cwd());
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
var FlakinessConfig = class _FlakinessConfig {
|
|
1054
|
+
constructor(_configPath, _config) {
|
|
1055
|
+
this._configPath = _configPath;
|
|
1056
|
+
this._config = _config;
|
|
1057
|
+
}
|
|
1058
|
+
static async load() {
|
|
1059
|
+
const configPath = ensureConfigPath();
|
|
1060
|
+
const data = await fs2.promises.readFile(configPath, "utf-8").catch((e) => void 0);
|
|
1061
|
+
const json = data ? JSON.parse(data) : {};
|
|
1062
|
+
return new _FlakinessConfig(configPath, json);
|
|
1063
|
+
}
|
|
1064
|
+
static async projectOrDie(session2) {
|
|
1065
|
+
const config = await _FlakinessConfig.load();
|
|
1066
|
+
const projectPublicId = config.projectPublicId();
|
|
1067
|
+
if (!projectPublicId)
|
|
1068
|
+
throw new Error(`Please link to flakiness project with 'npx flakiness link'`);
|
|
1069
|
+
const project = await session2.api.project.getProject.GET({ projectPublicId }).catch((e) => void 0);
|
|
1070
|
+
if (!project)
|
|
1071
|
+
throw new Error(`Failed to fetch linked project; please re-link with 'npx flakiness link'`);
|
|
1072
|
+
return project;
|
|
1073
|
+
}
|
|
1074
|
+
static createEmpty() {
|
|
1075
|
+
return new _FlakinessConfig(ensureConfigPath(), {});
|
|
1076
|
+
}
|
|
1077
|
+
path() {
|
|
1078
|
+
return this._configPath;
|
|
1079
|
+
}
|
|
1080
|
+
projectPublicId() {
|
|
1081
|
+
return this._config.projectPublicId;
|
|
1082
|
+
}
|
|
1083
|
+
setProjectPublicId(projectId) {
|
|
1084
|
+
this._config.projectPublicId = projectId;
|
|
1085
|
+
}
|
|
1086
|
+
async save() {
|
|
1087
|
+
await fs2.promises.mkdir(path2.dirname(this._configPath), { recursive: true });
|
|
1088
|
+
await fs2.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// src/flakinessSession.ts
|
|
1093
|
+
import fs3 from "fs/promises";
|
|
1094
|
+
import os2 from "os";
|
|
1095
|
+
import path3 from "path";
|
|
1096
|
+
|
|
487
1097
|
// src/serverapi.ts
|
|
1098
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
488
1099
|
function createServerAPI(endpoint, options) {
|
|
489
1100
|
endpoint += "/api/";
|
|
490
1101
|
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
@@ -501,24 +1112,30 @@ function createServerAPI(endpoint, options) {
|
|
|
501
1112
|
|
|
502
1113
|
// src/flakinessSession.ts
|
|
503
1114
|
var CONFIG_DIR = (() => {
|
|
504
|
-
const configDir = process.platform === "darwin" ?
|
|
1115
|
+
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
1116
|
return configDir;
|
|
506
1117
|
})();
|
|
507
|
-
var CONFIG_PATH =
|
|
1118
|
+
var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
|
|
508
1119
|
var FlakinessSession = class _FlakinessSession {
|
|
509
1120
|
constructor(_config) {
|
|
510
1121
|
this._config = _config;
|
|
511
1122
|
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
512
1123
|
}
|
|
1124
|
+
static async loadOrDie() {
|
|
1125
|
+
const session2 = await _FlakinessSession.load();
|
|
1126
|
+
if (!session2)
|
|
1127
|
+
throw new Error(`Please login first with 'npx flakiness login'`);
|
|
1128
|
+
return session2;
|
|
1129
|
+
}
|
|
513
1130
|
static async load() {
|
|
514
|
-
const data = await
|
|
1131
|
+
const data = await fs3.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
515
1132
|
if (!data)
|
|
516
1133
|
return void 0;
|
|
517
1134
|
const json = JSON.parse(data);
|
|
518
1135
|
return new _FlakinessSession(json);
|
|
519
1136
|
}
|
|
520
1137
|
static async remove() {
|
|
521
|
-
await
|
|
1138
|
+
await fs3.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
522
1139
|
}
|
|
523
1140
|
api;
|
|
524
1141
|
endpoint() {
|
|
@@ -531,21 +1148,21 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
531
1148
|
return this._config.token;
|
|
532
1149
|
}
|
|
533
1150
|
async save() {
|
|
534
|
-
await
|
|
535
|
-
await
|
|
1151
|
+
await fs3.mkdir(CONFIG_DIR, { recursive: true });
|
|
1152
|
+
await fs3.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
536
1153
|
}
|
|
537
1154
|
};
|
|
538
1155
|
|
|
539
1156
|
// src/cli/cmd-convert.ts
|
|
540
|
-
import
|
|
541
|
-
import
|
|
1157
|
+
import fs5 from "fs/promises";
|
|
1158
|
+
import path5 from "path";
|
|
542
1159
|
|
|
543
1160
|
// src/junit.ts
|
|
544
1161
|
import { FlakinessReport as FK } from "@flakiness/report";
|
|
545
1162
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
546
1163
|
import assert2 from "assert";
|
|
547
|
-
import
|
|
548
|
-
import
|
|
1164
|
+
import fs4 from "fs";
|
|
1165
|
+
import path4 from "path";
|
|
549
1166
|
function getProperties(element) {
|
|
550
1167
|
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
551
1168
|
if (!propertiesNodes.length)
|
|
@@ -587,8 +1204,8 @@ function extractStdout(testcase, stdio) {
|
|
|
587
1204
|
}));
|
|
588
1205
|
}
|
|
589
1206
|
async function parseAttachment(value) {
|
|
590
|
-
let absolutePath =
|
|
591
|
-
if (
|
|
1207
|
+
let absolutePath = path4.resolve(process.cwd(), value);
|
|
1208
|
+
if (fs4.existsSync(absolutePath)) {
|
|
592
1209
|
const id = await sha1File(absolutePath);
|
|
593
1210
|
return {
|
|
594
1211
|
contentType: "image/png",
|
|
@@ -660,7 +1277,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
660
1277
|
id: attachment.id,
|
|
661
1278
|
contentType: attachment.contentType,
|
|
662
1279
|
//TODO: better default names for attachments?
|
|
663
|
-
name: attachment.path ?
|
|
1280
|
+
name: attachment.path ? path4.basename(attachment.path) : `attachment`
|
|
664
1281
|
});
|
|
665
1282
|
} else {
|
|
666
1283
|
annotations.push({
|
|
@@ -735,15 +1352,15 @@ async function parseJUnit(xmls, options) {
|
|
|
735
1352
|
|
|
736
1353
|
// src/cli/cmd-convert.ts
|
|
737
1354
|
async function cmdConvert(junitPath, options) {
|
|
738
|
-
const fullPath =
|
|
739
|
-
if (!await
|
|
1355
|
+
const fullPath = path5.resolve(junitPath);
|
|
1356
|
+
if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
|
|
740
1357
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
741
1358
|
process.exit(1);
|
|
742
1359
|
}
|
|
743
|
-
const stat = await
|
|
1360
|
+
const stat = await fs5.stat(fullPath);
|
|
744
1361
|
let xmlContents = [];
|
|
745
1362
|
if (stat.isFile()) {
|
|
746
|
-
const xmlContent = await
|
|
1363
|
+
const xmlContent = await fs5.readFile(fullPath, "utf-8");
|
|
747
1364
|
xmlContents.push(xmlContent);
|
|
748
1365
|
} else if (stat.isDirectory()) {
|
|
749
1366
|
const xmlFiles = await findXmlFiles(fullPath);
|
|
@@ -753,7 +1370,7 @@ async function cmdConvert(junitPath, options) {
|
|
|
753
1370
|
}
|
|
754
1371
|
console.log(`Found ${xmlFiles.length} XML files`);
|
|
755
1372
|
for (const xmlFile of xmlFiles) {
|
|
756
|
-
const xmlContent = await
|
|
1373
|
+
const xmlContent = await fs5.readFile(xmlFile, "utf-8");
|
|
757
1374
|
xmlContents.push(xmlContent);
|
|
758
1375
|
}
|
|
759
1376
|
} else {
|
|
@@ -777,26 +1394,26 @@ async function cmdConvert(junitPath, options) {
|
|
|
777
1394
|
runStartTimestamp: Date.now(),
|
|
778
1395
|
runDuration: 0
|
|
779
1396
|
});
|
|
780
|
-
await
|
|
1397
|
+
await fs5.writeFile("fkreport.json", JSON.stringify(report, null, 2));
|
|
781
1398
|
console.log("\u2713 Saved report to fkreport.json");
|
|
782
1399
|
if (attachments.length > 0) {
|
|
783
|
-
await
|
|
1400
|
+
await fs5.mkdir("fkattachments", { recursive: true });
|
|
784
1401
|
for (const attachment of attachments) {
|
|
785
1402
|
if (attachment.path) {
|
|
786
|
-
const destPath =
|
|
787
|
-
await
|
|
1403
|
+
const destPath = path5.join("fkattachments", attachment.id);
|
|
1404
|
+
await fs5.copyFile(attachment.path, destPath);
|
|
788
1405
|
} else if (attachment.body) {
|
|
789
|
-
const destPath =
|
|
790
|
-
await
|
|
1406
|
+
const destPath = path5.join("fkattachments", attachment.id);
|
|
1407
|
+
await fs5.writeFile(destPath, attachment.body);
|
|
791
1408
|
}
|
|
792
1409
|
}
|
|
793
1410
|
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
794
1411
|
}
|
|
795
1412
|
}
|
|
796
1413
|
async function findXmlFiles(dir, result = []) {
|
|
797
|
-
const entries = await
|
|
1414
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
798
1415
|
for (const entry of entries) {
|
|
799
|
-
const fullPath =
|
|
1416
|
+
const fullPath = path5.join(dir, entry.name);
|
|
800
1417
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
801
1418
|
result.push(fullPath);
|
|
802
1419
|
else if (entry.isDirectory())
|
|
@@ -807,53 +1424,8 @@ async function findXmlFiles(dir, result = []) {
|
|
|
807
1424
|
|
|
808
1425
|
// src/cli/cmd-download.ts
|
|
809
1426
|
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() });
|
|
1427
|
+
import path6 from "path";
|
|
1428
|
+
async function cmdDownload(session2, project, runId) {
|
|
857
1429
|
const urls = await session2.api.run.downloadURLs.GET({
|
|
858
1430
|
orgSlug: project.org.orgSlug,
|
|
859
1431
|
projectSlug: project.projectSlug,
|
|
@@ -864,7 +1436,7 @@ async function cmdDownload(runId) {
|
|
|
864
1436
|
console.log(`Directory ${rootDir} already exists!`);
|
|
865
1437
|
return;
|
|
866
1438
|
}
|
|
867
|
-
const attachmentsDir =
|
|
1439
|
+
const attachmentsDir = path6.join(rootDir, "attachments");
|
|
868
1440
|
await fs6.promises.mkdir(rootDir, { recursive: true });
|
|
869
1441
|
if (urls.attachmentURLs.length)
|
|
870
1442
|
await fs6.promises.mkdir(attachmentsDir, { recursive: true });
|
|
@@ -872,7 +1444,7 @@ async function cmdDownload(runId) {
|
|
|
872
1444
|
if (!response.ok)
|
|
873
1445
|
throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
|
|
874
1446
|
const reportContent = await response.text();
|
|
875
|
-
await fs6.promises.writeFile(
|
|
1447
|
+
await fs6.promises.writeFile(path6.join(rootDir, "report.json"), reportContent);
|
|
876
1448
|
const attachmentDownloader = async () => {
|
|
877
1449
|
while (urls.attachmentURLs.length) {
|
|
878
1450
|
const url = urls.attachmentURLs.pop();
|
|
@@ -880,8 +1452,8 @@ async function cmdDownload(runId) {
|
|
|
880
1452
|
if (!response2.ok)
|
|
881
1453
|
throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
|
|
882
1454
|
const fileBuffer = Buffer.from(await response2.arrayBuffer());
|
|
883
|
-
const filename =
|
|
884
|
-
await fs6.promises.writeFile(
|
|
1455
|
+
const filename = path6.basename(new URL(url).pathname);
|
|
1456
|
+
await fs6.promises.writeFile(path6.join(attachmentsDir, filename), fileBuffer);
|
|
885
1457
|
}
|
|
886
1458
|
};
|
|
887
1459
|
const workerPromises = [];
|
|
@@ -907,11 +1479,10 @@ async function cmdLink(slug) {
|
|
|
907
1479
|
console.log(`Failed to find project ${slug}`);
|
|
908
1480
|
process.exit(1);
|
|
909
1481
|
}
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
console.log(`\u2713 Link successful! Config saved to ${link.path()}`);
|
|
1482
|
+
const config = FlakinessConfig.createEmpty();
|
|
1483
|
+
config.setProjectPublicId(project.projectPublicId);
|
|
1484
|
+
await config.save();
|
|
1485
|
+
console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
915
1486
|
}
|
|
916
1487
|
|
|
917
1488
|
// ../server/lib/common/knownClientIds.js
|
|
@@ -921,6 +1492,7 @@ var KNOWN_CLIENT_IDS = {
|
|
|
921
1492
|
};
|
|
922
1493
|
|
|
923
1494
|
// src/cli/cmd-login.ts
|
|
1495
|
+
import open from "open";
|
|
924
1496
|
import os3 from "os";
|
|
925
1497
|
async function cmdLogin(endpoint) {
|
|
926
1498
|
const api = createServerAPI(endpoint);
|
|
@@ -928,6 +1500,7 @@ async function cmdLogin(endpoint) {
|
|
|
928
1500
|
clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
|
|
929
1501
|
name: os3.hostname()
|
|
930
1502
|
});
|
|
1503
|
+
await open(new URL(data.verificationUrl, endpoint).href);
|
|
931
1504
|
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
932
1505
|
let token;
|
|
933
1506
|
while (Date.now() < data.deadline) {
|
|
@@ -949,15 +1522,226 @@ async function cmdLogin(endpoint) {
|
|
|
949
1522
|
endpoint,
|
|
950
1523
|
token
|
|
951
1524
|
});
|
|
952
|
-
|
|
953
|
-
|
|
1525
|
+
try {
|
|
1526
|
+
const user = await session2.api.user.whoami.GET();
|
|
1527
|
+
await session2.save();
|
|
1528
|
+
console.log(`\u2713 Logged in as ${user.userName} (${user.userLogin})`);
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1531
|
+
console.error(`x Failed to login:`, message);
|
|
1532
|
+
}
|
|
954
1533
|
}
|
|
955
1534
|
|
|
956
1535
|
// src/cli/cmd-logout.ts
|
|
957
1536
|
async function cmdLogout() {
|
|
1537
|
+
const session2 = await FlakinessSession.load();
|
|
1538
|
+
if (!session2)
|
|
1539
|
+
return;
|
|
1540
|
+
const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
|
|
1541
|
+
if (currentSession)
|
|
1542
|
+
await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
958
1543
|
await FlakinessSession.remove();
|
|
959
1544
|
}
|
|
960
1545
|
|
|
1546
|
+
// src/cli/cmd-show-report.ts
|
|
1547
|
+
import chalk from "chalk";
|
|
1548
|
+
import open2 from "open";
|
|
1549
|
+
import path7 from "path";
|
|
1550
|
+
|
|
1551
|
+
// src/localReportServer.ts
|
|
1552
|
+
import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
|
|
1553
|
+
import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
|
|
1554
|
+
import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
|
|
1555
|
+
import bodyParser from "body-parser";
|
|
1556
|
+
import compression from "compression";
|
|
1557
|
+
import debug from "debug";
|
|
1558
|
+
import express from "express";
|
|
1559
|
+
import "express-async-errors";
|
|
1560
|
+
import fs8 from "fs";
|
|
1561
|
+
import http2 from "http";
|
|
1562
|
+
|
|
1563
|
+
// src/localGit.ts
|
|
1564
|
+
import { exec } from "child_process";
|
|
1565
|
+
import { promisify } from "util";
|
|
1566
|
+
var execAsync = promisify(exec);
|
|
1567
|
+
async function listLocalCommits(gitRoot, head, count) {
|
|
1568
|
+
const FIELD_SEPARATOR = "|~|";
|
|
1569
|
+
const RECORD_SEPARATOR = "\0";
|
|
1570
|
+
const prettyFormat = [
|
|
1571
|
+
"%H",
|
|
1572
|
+
// %H: Full commit hash
|
|
1573
|
+
"%at",
|
|
1574
|
+
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
1575
|
+
"%an",
|
|
1576
|
+
// %an: Author name
|
|
1577
|
+
"%s"
|
|
1578
|
+
// %s: Subject (the first line of the commit message)
|
|
1579
|
+
].join(FIELD_SEPARATOR);
|
|
1580
|
+
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
1581
|
+
try {
|
|
1582
|
+
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
1583
|
+
if (!stdout) {
|
|
1584
|
+
return [];
|
|
1585
|
+
}
|
|
1586
|
+
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
1587
|
+
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
1588
|
+
return {
|
|
1589
|
+
commitId,
|
|
1590
|
+
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
1591
|
+
// Convert timestamp from seconds to milliseconds
|
|
1592
|
+
author,
|
|
1593
|
+
message,
|
|
1594
|
+
walkIndex: 0
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
1599
|
+
throw error;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/localReportApi.ts
|
|
1604
|
+
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
1605
|
+
import fs7 from "fs";
|
|
1606
|
+
import { z } from "zod/v4";
|
|
1607
|
+
var t = TypedHTTP2.Router.create();
|
|
1608
|
+
var localReportRouter = {
|
|
1609
|
+
ping: t.get({
|
|
1610
|
+
handler: async () => {
|
|
1611
|
+
return "pong";
|
|
1612
|
+
}
|
|
1613
|
+
}),
|
|
1614
|
+
lastCommits: t.get({
|
|
1615
|
+
handler: async ({ ctx }) => {
|
|
1616
|
+
return ctx.commits;
|
|
1617
|
+
}
|
|
1618
|
+
}),
|
|
1619
|
+
report: {
|
|
1620
|
+
attachment: t.rawMethod("GET", {
|
|
1621
|
+
input: z.object({
|
|
1622
|
+
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
1623
|
+
}),
|
|
1624
|
+
handler: async ({ ctx, input }) => {
|
|
1625
|
+
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
1626
|
+
if (!idx)
|
|
1627
|
+
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
1628
|
+
const buffer = await fs7.promises.readFile(idx.path);
|
|
1629
|
+
return TypedHTTP2.ok(buffer, idx.contentType);
|
|
1630
|
+
}
|
|
1631
|
+
}),
|
|
1632
|
+
json: t.get({
|
|
1633
|
+
handler: async ({ ctx }) => {
|
|
1634
|
+
return ctx.report;
|
|
1635
|
+
}
|
|
1636
|
+
})
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// src/localReportServer.ts
|
|
1641
|
+
var logHTTPServer = debug("fk:http");
|
|
1642
|
+
var LocalReportServer = class _LocalReportServer {
|
|
1643
|
+
constructor(_server, _port, _authToken) {
|
|
1644
|
+
this._server = _server;
|
|
1645
|
+
this._port = _port;
|
|
1646
|
+
this._authToken = _authToken;
|
|
1647
|
+
}
|
|
1648
|
+
static async create(options) {
|
|
1649
|
+
const app = express();
|
|
1650
|
+
app.set("etag", false);
|
|
1651
|
+
const authToken = randomUUIDBase62();
|
|
1652
|
+
app.use(compression());
|
|
1653
|
+
app.use(bodyParser.json({ limit: 256 * 1024 }));
|
|
1654
|
+
app.use((req, res, next) => {
|
|
1655
|
+
if (!req.path.startsWith("/" + authToken))
|
|
1656
|
+
throw TypedHTTP3.HttpError.withCode("UNAUTHORIZED");
|
|
1657
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
1658
|
+
res.setHeader("Access-Control-Allow-Origin", options.endpoint);
|
|
1659
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
1660
|
+
if (req.method === "OPTIONS") {
|
|
1661
|
+
res.writeHead(204);
|
|
1662
|
+
res.end();
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
|
|
1666
|
+
res.on("close", () => {
|
|
1667
|
+
if (!res.headersSent)
|
|
1668
|
+
logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
|
|
1669
|
+
});
|
|
1670
|
+
next();
|
|
1671
|
+
});
|
|
1672
|
+
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
1673
|
+
router: localReportRouter,
|
|
1674
|
+
createRootContext: async ({ req, res, input }) => {
|
|
1675
|
+
const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
|
|
1676
|
+
const attachmentsDir = options.attachmentsFolder;
|
|
1677
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1678
|
+
if (missingAttachments.length) {
|
|
1679
|
+
const first = missingAttachments.slice(0, 3);
|
|
1680
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
1681
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
1682
|
+
if (missingAttachments.length > 3)
|
|
1683
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
report,
|
|
1687
|
+
commits: await listLocalCommits(process.cwd(), report.commitId, 100),
|
|
1688
|
+
attachmentIdToPath
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}));
|
|
1692
|
+
app.use((err, req, res, next) => {
|
|
1693
|
+
if (err instanceof TypedHTTP3.HttpError)
|
|
1694
|
+
return res.status(err.status).send({ error: err.message });
|
|
1695
|
+
logHTTPServer(err);
|
|
1696
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
1697
|
+
});
|
|
1698
|
+
const server = http2.createServer(app);
|
|
1699
|
+
server.on("error", (err) => {
|
|
1700
|
+
if (err.code === "ECONNRESET") {
|
|
1701
|
+
logHTTPServer("Client connection reset. Ignoring.");
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
throw err;
|
|
1705
|
+
});
|
|
1706
|
+
const port = await new Promise((resolve) => server.listen(options.port, () => {
|
|
1707
|
+
resolve(server.address().port);
|
|
1708
|
+
}));
|
|
1709
|
+
return new _LocalReportServer(server, port, authToken);
|
|
1710
|
+
}
|
|
1711
|
+
authToken() {
|
|
1712
|
+
return this._authToken;
|
|
1713
|
+
}
|
|
1714
|
+
port() {
|
|
1715
|
+
return this._port;
|
|
1716
|
+
}
|
|
1717
|
+
async dispose() {
|
|
1718
|
+
await new Promise((x) => this._server.close(x));
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// src/cli/cmd-show-report.ts
|
|
1723
|
+
async function cmdShowReport(reportFolder) {
|
|
1724
|
+
const reportPath = path7.join(reportFolder, "report.json");
|
|
1725
|
+
const session2 = await FlakinessSession.load();
|
|
1726
|
+
const config = await FlakinessConfig.load();
|
|
1727
|
+
const projectPublicId = config.projectPublicId();
|
|
1728
|
+
const project = projectPublicId && session2 ? await session2.api.project.getProject.GET({ projectPublicId }) : void 0;
|
|
1729
|
+
const endpoint = session2?.endpoint() ?? "https://flakiness.io";
|
|
1730
|
+
const server = await LocalReportServer.create({
|
|
1731
|
+
endpoint,
|
|
1732
|
+
port: 9373,
|
|
1733
|
+
reportPath,
|
|
1734
|
+
attachmentsFolder: reportFolder
|
|
1735
|
+
});
|
|
1736
|
+
const reportEndpoint = project ? `${endpoint}/localreport/${project.org.orgSlug}/${project.projectSlug}?port=${server.port()}&token=${server.authToken()}` : `${endpoint}/localreport?port=${server.port()}&token=${server.authToken()}`;
|
|
1737
|
+
console.log(chalk.cyan(`
|
|
1738
|
+
Serving Flakiness report at ${reportEndpoint}
|
|
1739
|
+
Press Ctrl+C to quit.`));
|
|
1740
|
+
await open2(reportEndpoint);
|
|
1741
|
+
await new Promise(() => {
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
961
1745
|
// src/cli/cmd-status.ts
|
|
962
1746
|
async function cmdStatus() {
|
|
963
1747
|
const session2 = await FlakinessSession.load();
|
|
@@ -967,31 +1751,34 @@ async function cmdStatus() {
|
|
|
967
1751
|
}
|
|
968
1752
|
const user = await session2.api.user.whoami.GET();
|
|
969
1753
|
console.log(`user: ${user.userName} (${user.userLogin})`);
|
|
970
|
-
const
|
|
971
|
-
|
|
1754
|
+
const config = await FlakinessConfig.load();
|
|
1755
|
+
const projectPublicId = config.projectPublicId();
|
|
1756
|
+
if (!projectPublicId) {
|
|
972
1757
|
console.log(`project: <not linked>`);
|
|
973
1758
|
return;
|
|
974
1759
|
}
|
|
975
|
-
const project = await session2.api.project.getProject.GET({
|
|
976
|
-
projectPublicId: link.projectId()
|
|
977
|
-
});
|
|
1760
|
+
const project = await session2.api.project.getProject.GET({ projectPublicId });
|
|
978
1761
|
console.log(`project: ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
979
1762
|
}
|
|
980
1763
|
|
|
981
1764
|
// src/cli/cmd-unlink.ts
|
|
982
1765
|
async function cmdUnlink() {
|
|
983
|
-
await
|
|
1766
|
+
const config = await FlakinessConfig.load();
|
|
1767
|
+
if (!config.projectPublicId())
|
|
1768
|
+
return;
|
|
1769
|
+
config.setProjectPublicId(void 0);
|
|
1770
|
+
await config.save();
|
|
984
1771
|
}
|
|
985
1772
|
|
|
986
1773
|
// src/cli/cmd-upload-playwright-json.ts
|
|
987
|
-
import
|
|
988
|
-
import
|
|
1774
|
+
import fs10 from "fs/promises";
|
|
1775
|
+
import path8 from "path";
|
|
989
1776
|
|
|
990
1777
|
// src/playwrightJSONReport.ts
|
|
991
|
-
import { FlakinessReport as FK2, FlakinessReport } from "@flakiness/report";
|
|
992
|
-
import
|
|
1778
|
+
import { FlakinessReport as FK2, FlakinessReport as FlakinessReport2 } from "@flakiness/report";
|
|
1779
|
+
import debug2 from "debug";
|
|
993
1780
|
import { posix as posixPath2 } from "path";
|
|
994
|
-
var dlog =
|
|
1781
|
+
var dlog = debug2("flakiness:json-report");
|
|
995
1782
|
var PlaywrightJSONReport;
|
|
996
1783
|
((PlaywrightJSONReport2) => {
|
|
997
1784
|
function collectMetadata(somePathInsideProject = process.cwd()) {
|
|
@@ -1023,7 +1810,7 @@ var PlaywrightJSONReport;
|
|
|
1023
1810
|
};
|
|
1024
1811
|
const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
|
|
1025
1812
|
const report = {
|
|
1026
|
-
category:
|
|
1813
|
+
category: FlakinessReport2.CATEGORY_PLAYWRIGHT,
|
|
1027
1814
|
commitId: metadata.commitId,
|
|
1028
1815
|
configPath,
|
|
1029
1816
|
url: metadata.runURL,
|
|
@@ -1163,9 +1950,10 @@ function parseJSONError(context, error) {
|
|
|
1163
1950
|
}
|
|
1164
1951
|
|
|
1165
1952
|
// src/reportUploader.ts
|
|
1166
|
-
import
|
|
1953
|
+
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
1954
|
+
import assert3 from "assert";
|
|
1955
|
+
import fs9 from "fs";
|
|
1167
1956
|
import { URL as URL2 } from "url";
|
|
1168
|
-
import { brotliCompressSync as brotliCompressSync2 } from "zlib";
|
|
1169
1957
|
var ReportUploader = class _ReportUploader {
|
|
1170
1958
|
static optionsFromEnv(overrides) {
|
|
1171
1959
|
const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
|
|
@@ -1177,7 +1965,8 @@ var ReportUploader = class _ReportUploader {
|
|
|
1177
1965
|
static async upload(options) {
|
|
1178
1966
|
const uploaderOptions = _ReportUploader.optionsFromEnv(options);
|
|
1179
1967
|
if (!uploaderOptions) {
|
|
1180
|
-
|
|
1968
|
+
if (process.env.CI)
|
|
1969
|
+
options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
|
|
1181
1970
|
return void 0;
|
|
1182
1971
|
}
|
|
1183
1972
|
const uploader = new _ReportUploader(uploaderOptions);
|
|
@@ -1210,11 +1999,10 @@ var ReportUpload = class {
|
|
|
1210
1999
|
this._options = options;
|
|
1211
2000
|
this._report = report;
|
|
1212
2001
|
this._attachments = attachments;
|
|
1213
|
-
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF });
|
|
2002
|
+
this._api = createServerAPI(this._options.flakinessEndpoint, { retries: HTTP_BACKOFF, auth: this._options.flakinessAccessToken });
|
|
1214
2003
|
}
|
|
1215
2004
|
async upload(options) {
|
|
1216
2005
|
const response = await this._api.run.startUpload.POST({
|
|
1217
|
-
flakinessAccessToken: this._options.flakinessAccessToken,
|
|
1218
2006
|
attachmentIds: this._attachments.map((attachment) => attachment.id)
|
|
1219
2007
|
}).then((result) => ({ result, error: void 0 })).catch((e) => ({ result: void 0, error: e }));
|
|
1220
2008
|
if (response?.error || !response.result)
|
|
@@ -1225,7 +2013,7 @@ var ReportUpload = class {
|
|
|
1225
2013
|
const uploadURL = response.result.attachment_upload_urls[attachment.id];
|
|
1226
2014
|
if (!uploadURL)
|
|
1227
2015
|
throw new Error("Internal error: missing upload URL for attachment!");
|
|
1228
|
-
return this._uploadAttachment(attachment, uploadURL);
|
|
2016
|
+
return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
|
|
1229
2017
|
})
|
|
1230
2018
|
]);
|
|
1231
2019
|
const response2 = await this._api.run.completeUpload.POST({
|
|
@@ -1235,7 +2023,7 @@ var ReportUpload = class {
|
|
|
1235
2023
|
return { success: true, reportUrl: url };
|
|
1236
2024
|
}
|
|
1237
2025
|
async _uploadReport(data, uploadUrl, syncCompression) {
|
|
1238
|
-
const compressed = syncCompression ?
|
|
2026
|
+
const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
|
|
1239
2027
|
const headers = {
|
|
1240
2028
|
"Content-Type": "application/json",
|
|
1241
2029
|
"Content-Length": Buffer.byteLength(compressed) + "",
|
|
@@ -1252,11 +2040,34 @@ var ReportUpload = class {
|
|
|
1252
2040
|
await responseDataPromise;
|
|
1253
2041
|
}, HTTP_BACKOFF);
|
|
1254
2042
|
}
|
|
1255
|
-
async _uploadAttachment(attachment, uploadUrl) {
|
|
1256
|
-
const
|
|
2043
|
+
async _uploadAttachment(attachment, uploadUrl, syncCompression) {
|
|
2044
|
+
const mimeType = attachment.contentType.toLocaleLowerCase().trim();
|
|
2045
|
+
const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
|
|
2046
|
+
if (!compressable && attachment.path) {
|
|
2047
|
+
const attachmentPath = attachment.path;
|
|
2048
|
+
await retryWithBackoff(async () => {
|
|
2049
|
+
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
2050
|
+
url: uploadUrl,
|
|
2051
|
+
headers: {
|
|
2052
|
+
"Content-Type": attachment.contentType,
|
|
2053
|
+
"Content-Length": (await fs9.promises.stat(attachmentPath)).size + ""
|
|
2054
|
+
},
|
|
2055
|
+
method: "put"
|
|
2056
|
+
});
|
|
2057
|
+
fs9.createReadStream(attachmentPath).pipe(request);
|
|
2058
|
+
await responseDataPromise;
|
|
2059
|
+
}, HTTP_BACKOFF);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs9.promises.readFile(attachment.path) : void 0;
|
|
2063
|
+
assert3(buffer);
|
|
2064
|
+
const encoding = compressable ? "br" : void 0;
|
|
2065
|
+
if (compressable)
|
|
2066
|
+
buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
|
|
1257
2067
|
const headers = {
|
|
1258
2068
|
"Content-Type": attachment.contentType,
|
|
1259
|
-
"Content-Length":
|
|
2069
|
+
"Content-Length": Buffer.byteLength(buffer) + "",
|
|
2070
|
+
"Content-Encoding": encoding
|
|
1260
2071
|
};
|
|
1261
2072
|
await retryWithBackoff(async () => {
|
|
1262
2073
|
const { request, responseDataPromise } = httpUtils.createRequest({
|
|
@@ -1264,13 +2075,8 @@ var ReportUpload = class {
|
|
|
1264
2075
|
headers,
|
|
1265
2076
|
method: "put"
|
|
1266
2077
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
} else {
|
|
1270
|
-
if (attachment.body)
|
|
1271
|
-
request.write(attachment.body);
|
|
1272
|
-
request.end();
|
|
1273
|
-
}
|
|
2078
|
+
request.write(buffer);
|
|
2079
|
+
request.end();
|
|
1274
2080
|
await responseDataPromise;
|
|
1275
2081
|
}, HTTP_BACKOFF);
|
|
1276
2082
|
}
|
|
@@ -1278,12 +2084,12 @@ var ReportUpload = class {
|
|
|
1278
2084
|
|
|
1279
2085
|
// src/cli/cmd-upload-playwright-json.ts
|
|
1280
2086
|
async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
1281
|
-
const fullPath =
|
|
1282
|
-
if (!await
|
|
2087
|
+
const fullPath = path8.resolve(relativePath);
|
|
2088
|
+
if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1283
2089
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1284
2090
|
process.exit(1);
|
|
1285
2091
|
}
|
|
1286
|
-
const text = await
|
|
2092
|
+
const text = await fs10.readFile(fullPath, "utf-8");
|
|
1287
2093
|
const playwrightJson = JSON.parse(text);
|
|
1288
2094
|
const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
|
|
1289
2095
|
extractAttachments: true
|
|
@@ -1304,38 +2110,18 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
|
1304
2110
|
}
|
|
1305
2111
|
|
|
1306
2112
|
// src/cli/cmd-upload.ts
|
|
1307
|
-
import
|
|
1308
|
-
import
|
|
1309
|
-
import path7 from "path";
|
|
2113
|
+
import fs11 from "fs/promises";
|
|
2114
|
+
import path9 from "path";
|
|
1310
2115
|
async function cmdUpload(relativePath, options) {
|
|
1311
|
-
const fullPath =
|
|
1312
|
-
if (!await
|
|
2116
|
+
const fullPath = path9.resolve(relativePath);
|
|
2117
|
+
if (!await fs11.access(fullPath, fs11.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1313
2118
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1314
2119
|
process.exit(1);
|
|
1315
2120
|
}
|
|
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");
|
|
2121
|
+
const text = await fs11.readFile(fullPath, "utf-8");
|
|
1320
2122
|
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
|
-
});
|
|
2123
|
+
const attachmentsDir = options.attachmentsDir ?? path9.dirname(fullPath);
|
|
2124
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1339
2125
|
if (missingAttachments.length && !options.ignoreMissingAttachments) {
|
|
1340
2126
|
console.log(`Missing ${missingAttachments.length} attachments - exiting. Use --ignore-missing-attachments to force upload.`);
|
|
1341
2127
|
process.exit(1);
|
|
@@ -1344,7 +2130,7 @@ async function cmdUpload(relativePath, options) {
|
|
|
1344
2130
|
flakinessAccessToken: options.accessToken,
|
|
1345
2131
|
flakinessEndpoint: options.endpoint
|
|
1346
2132
|
});
|
|
1347
|
-
const upload = uploader.createUpload(report,
|
|
2133
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
1348
2134
|
const uploadResult = await upload.upload();
|
|
1349
2135
|
if (!uploadResult.success) {
|
|
1350
2136
|
console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
@@ -1352,17 +2138,6 @@ async function cmdUpload(relativePath, options) {
|
|
|
1352
2138
|
console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
1353
2139
|
}
|
|
1354
2140
|
}
|
|
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
2141
|
|
|
1367
2142
|
// src/cli/cmd-whoami.ts
|
|
1368
2143
|
async function cmdWhoami() {
|
|
@@ -1378,7 +2153,7 @@ async function cmdWhoami() {
|
|
|
1378
2153
|
|
|
1379
2154
|
// src/cli/cli.ts
|
|
1380
2155
|
var session = await FlakinessSession.load();
|
|
1381
|
-
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").
|
|
2156
|
+
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
1382
2157
|
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? "https://flakiness.io").env("FLAKINESS_ENDPOINT");
|
|
1383
2158
|
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
1384
2159
|
var optIgnoreMissingAttachments = new Option("--ignore-missing-attachments", "Upload report even if some attachments are missing.").default("", "Same directory as the report file");
|
|
@@ -1388,20 +2163,42 @@ async function runCommand(callback) {
|
|
|
1388
2163
|
} catch (e) {
|
|
1389
2164
|
if (!(e instanceof Error))
|
|
1390
2165
|
throw e;
|
|
1391
|
-
|
|
1392
|
-
console.error(e.stack);
|
|
1393
|
-
else
|
|
1394
|
-
console.error(e.message);
|
|
2166
|
+
console.error(errorText(e));
|
|
1395
2167
|
process.exit(1);
|
|
1396
2168
|
}
|
|
1397
2169
|
}
|
|
1398
|
-
var PACKAGE_JSON = JSON.parse(
|
|
2170
|
+
var PACKAGE_JSON = JSON.parse(fs12.readFileSync(path10.resolve(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
|
|
1399
2171
|
var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
|
|
2172
|
+
async function ensureAccessToken(options) {
|
|
2173
|
+
let accessToken = options.accessToken;
|
|
2174
|
+
if (!accessToken) {
|
|
2175
|
+
const config = await FlakinessConfig.load();
|
|
2176
|
+
const projectPublicId = config.projectPublicId();
|
|
2177
|
+
if (session && projectPublicId) {
|
|
2178
|
+
try {
|
|
2179
|
+
accessToken = (await session.api.project.getProject.GET({ projectPublicId })).readWriteAccessToken;
|
|
2180
|
+
} catch (e) {
|
|
2181
|
+
if (e instanceof TypedHTTP4.HttpError && e.status === 404) {
|
|
2182
|
+
} else {
|
|
2183
|
+
throw e;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
assert4(accessToken, `Please either pass FLAKINESS_ACCESS_TOKEN or run login + link`);
|
|
2189
|
+
return {
|
|
2190
|
+
...options,
|
|
2191
|
+
accessToken
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
1400
2194
|
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);
|
|
2195
|
+
await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
|
|
1402
2196
|
}));
|
|
1403
|
-
|
|
2197
|
+
var optLink = new Option("--link <org/proj>", "A project to link to");
|
|
2198
|
+
program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
|
|
1404
2199
|
await cmdLogin(options.endpoint);
|
|
2200
|
+
if (options.link)
|
|
2201
|
+
await cmdLink(options.link);
|
|
1405
2202
|
}));
|
|
1406
2203
|
program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
|
|
1407
2204
|
await cmdLogout();
|
|
@@ -1418,14 +2215,60 @@ program.command("unlink").description("Unlink repository from the flakiness proj
|
|
|
1418
2215
|
program.command("status").description("Status repository from the flakiness project").action(async () => runCommand(async () => {
|
|
1419
2216
|
await cmdStatus();
|
|
1420
2217
|
}));
|
|
1421
|
-
|
|
1422
|
-
|
|
2218
|
+
var optRunId = new Option("--run-id <runId>", "RunId flakiness.io access token").argParser((value) => {
|
|
2219
|
+
const parsed = parseInt(value, 10);
|
|
2220
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
2221
|
+
throw new Error("runId must be a number >= 1");
|
|
2222
|
+
}
|
|
2223
|
+
return parsed;
|
|
2224
|
+
});
|
|
2225
|
+
var optSince = new Option("--since <date>", "Start date for filtering").argParser((value) => {
|
|
2226
|
+
const parsed = new Date(value);
|
|
2227
|
+
if (isNaN(parsed.getTime())) {
|
|
2228
|
+
throw new Error("Invalid date format");
|
|
2229
|
+
}
|
|
2230
|
+
return parsed;
|
|
2231
|
+
});
|
|
2232
|
+
var optParallel = new Option("-j, --parallel <date>", "Parallel jobs to run").argParser((value) => {
|
|
2233
|
+
const parsed = parseInt(value, 10);
|
|
2234
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
2235
|
+
throw new Error("parallel must be a number >= 1");
|
|
2236
|
+
}
|
|
2237
|
+
return parsed;
|
|
2238
|
+
});
|
|
2239
|
+
program.command("download").description("Download run").addOption(optSince).addOption(optRunId).addOption(optParallel).action(async (options) => runCommand(async () => {
|
|
2240
|
+
const session2 = await FlakinessSession.loadOrDie();
|
|
2241
|
+
const project = await FlakinessConfig.projectOrDie(session2);
|
|
2242
|
+
let runIds = [];
|
|
2243
|
+
if (options.runId) {
|
|
2244
|
+
runIds = [options.runId, options.runId];
|
|
2245
|
+
} else if (options.since) {
|
|
2246
|
+
runIds = await session2.api.project.listRuns.GET({
|
|
2247
|
+
orgSlug: project.org.orgSlug,
|
|
2248
|
+
projectSlug: project.projectSlug,
|
|
2249
|
+
sinceTimestampMs: +options.since
|
|
2250
|
+
});
|
|
2251
|
+
console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
|
|
2252
|
+
}
|
|
2253
|
+
const it = Ranges.iterate(runIds);
|
|
2254
|
+
const downloaders = [];
|
|
2255
|
+
for (let i = 0; i < (options.parallel ?? 1); ++i) {
|
|
2256
|
+
downloaders.push((async () => {
|
|
2257
|
+
for (let result = it.next(); !result.done; result = it.next())
|
|
2258
|
+
await cmdDownload(session2, project, result.value);
|
|
2259
|
+
})());
|
|
2260
|
+
}
|
|
2261
|
+
await Promise.all(downloaders);
|
|
1423
2262
|
}));
|
|
1424
2263
|
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
2264
|
await runCommand(async () => {
|
|
1426
|
-
await cmdUpload(relativePath, options);
|
|
2265
|
+
await cmdUpload(relativePath, await ensureAccessToken(options));
|
|
1427
2266
|
});
|
|
1428
2267
|
});
|
|
2268
|
+
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 () => {
|
|
2269
|
+
const dir = path10.join(process.cwd(), arg ?? "flakiness-report");
|
|
2270
|
+
await cmdShowReport(dir);
|
|
2271
|
+
}));
|
|
1429
2272
|
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
2273
|
await runCommand(async () => {
|
|
1431
2274
|
await cmdConvert(junitPath, options);
|