@node-core/utils 7.2.2 → 7.3.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/bin/ncu-ci.js +52 -6
- package/lib/ci/ci_failure_parser.js +132 -117
- package/lib/ci/failure_file_scanner.js +153 -0
- package/lib/ci/resume_ci.js +150 -0
- package/lib/ci/run_ci.js +14 -4
- package/lib/cli.js +7 -1
- package/lib/prepare_security.js +56 -107
- package/lib/request.js +52 -8
- package/lib/security-release/draft.js +32 -0
- package/lib/security-release/preparation.js +168 -0
- package/lib/security-release/security-release.js +32 -2
- package/package.json +1 -1
package/bin/ncu-ci.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
RunPRJob
|
|
25
25
|
} from '../lib/ci/run_ci.js';
|
|
26
|
+
import { ResumePRJob } from '../lib/ci/resume_ci.js';
|
|
26
27
|
import { writeJson, writeFile } from '../lib/file.js';
|
|
27
28
|
import { getMergedConfig } from '../lib/config.js';
|
|
28
29
|
import { runPromise } from '../lib/run.js';
|
|
@@ -134,6 +135,26 @@ const args = yargs(hideBin(process.argv))
|
|
|
134
135
|
},
|
|
135
136
|
handler
|
|
136
137
|
})
|
|
138
|
+
.command({
|
|
139
|
+
command: 'resume <prid>',
|
|
140
|
+
desc: 'Resume the latest CI run for given PR',
|
|
141
|
+
builder: (yargs) => {
|
|
142
|
+
yargs
|
|
143
|
+
.positional('prid', {
|
|
144
|
+
describe: 'ID of the PR or URL to the PR',
|
|
145
|
+
type: 'string'
|
|
146
|
+
})
|
|
147
|
+
.option('owner', {
|
|
148
|
+
default: '',
|
|
149
|
+
describe: 'GitHub repository owner'
|
|
150
|
+
})
|
|
151
|
+
.option('repo', {
|
|
152
|
+
default: '',
|
|
153
|
+
describe: 'GitHub repository name'
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
handler
|
|
157
|
+
})
|
|
137
158
|
.command({
|
|
138
159
|
command: 'url <url>',
|
|
139
160
|
desc: 'Automatically detect CI type and show results',
|
|
@@ -278,11 +299,13 @@ class RunPRJobCommand {
|
|
|
278
299
|
return this.argv.prid;
|
|
279
300
|
}
|
|
280
301
|
|
|
281
|
-
|
|
282
|
-
const {
|
|
283
|
-
cli, request, prid, repo, owner
|
|
284
|
-
} = this;
|
|
302
|
+
validate() {
|
|
303
|
+
const { cli, prid, repo, owner } = this;
|
|
285
304
|
let validArgs = true;
|
|
305
|
+
if (!Number.isSafeInteger(prid) || prid <= 0) {
|
|
306
|
+
validArgs = false;
|
|
307
|
+
cli.error('Pull request ID must be a positive integer');
|
|
308
|
+
}
|
|
286
309
|
if (!repo) {
|
|
287
310
|
validArgs = false;
|
|
288
311
|
cli.error('GitHub repository is missing, please set it via ncu-config ' +
|
|
@@ -295,6 +318,13 @@ class RunPRJobCommand {
|
|
|
295
318
|
}
|
|
296
319
|
if (!validArgs) {
|
|
297
320
|
this.cli.setExitCode(1);
|
|
321
|
+
}
|
|
322
|
+
return validArgs;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async start() {
|
|
326
|
+
const { cli, request, prid, repo, owner } = this;
|
|
327
|
+
if (!this.validate()) {
|
|
298
328
|
return;
|
|
299
329
|
}
|
|
300
330
|
const { certifySafe, checkForDuplicates } = this.argv;
|
|
@@ -308,6 +338,20 @@ class RunPRJobCommand {
|
|
|
308
338
|
}
|
|
309
339
|
}
|
|
310
340
|
|
|
341
|
+
class ResumePRJobCommand extends RunPRJobCommand {
|
|
342
|
+
async start() {
|
|
343
|
+
const { cli, request, prid, repo, owner } = this;
|
|
344
|
+
if (!this.validate()) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const jobRunner = new ResumePRJob(cli, request, owner, repo, prid);
|
|
348
|
+
if (!(await jobRunner.resume())) {
|
|
349
|
+
cli.setExitCode(1);
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
311
355
|
class CICommand {
|
|
312
356
|
constructor(cli, request, argv) {
|
|
313
357
|
this.cli = cli;
|
|
@@ -565,7 +609,8 @@ async function main(command, argv) {
|
|
|
565
609
|
let commandHandler;
|
|
566
610
|
// Prepare queue.
|
|
567
611
|
switch (command) {
|
|
568
|
-
case 'run':
|
|
612
|
+
case 'run':
|
|
613
|
+
case 'resume': {
|
|
569
614
|
const maybeURL = URL.parse(argv.prid);
|
|
570
615
|
if (maybeURL?.host === 'github.com') {
|
|
571
616
|
const [, owner, repo, , prid, , commit_sha] = maybeURL.pathname.split('/');
|
|
@@ -575,7 +620,8 @@ async function main(command, argv) {
|
|
|
575
620
|
argv.prid = prid;
|
|
576
621
|
}
|
|
577
622
|
argv.prid = Number(argv.prid);
|
|
578
|
-
const
|
|
623
|
+
const Command = command === 'run' ? RunPRJobCommand : ResumePRJobCommand;
|
|
624
|
+
const jobRunner = new Command(cli, request, argv);
|
|
579
625
|
return jobRunner.start();
|
|
580
626
|
}
|
|
581
627
|
case 'rate': {
|
|
@@ -132,35 +132,54 @@ function failureMatcher(Failure, patterns, ctx, text) {
|
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
const fatalPattern = /fatal: .+/g;
|
|
136
|
+
|
|
137
|
+
// Shared by the whole-log parser and the streaming file scanner.
|
|
138
|
+
export const FAILURE_MARKERS = {
|
|
139
|
+
tap: {
|
|
140
|
+
start: /^not ok \d+/,
|
|
141
|
+
end: / {2}\.\.\.\r?\n/,
|
|
142
|
+
todo: '# TODO :'
|
|
143
|
+
},
|
|
144
|
+
cpp: /\[ {2}FAILED {2}\]/,
|
|
145
|
+
git: [{
|
|
146
|
+
start: /Changes not staged for commit:/,
|
|
147
|
+
end: /no changes added to commit/
|
|
148
|
+
}, {
|
|
149
|
+
start: /error: Your local changes to the following files/,
|
|
150
|
+
end: /Failed to merge in the changes./
|
|
151
|
+
}]
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const tapPattern = new RegExp(
|
|
155
|
+
`${FAILURE_MARKERS.tap.start.source}[\\s\\S]+?${FAILURE_MARKERS.tap.end.source}`, 'mg');
|
|
156
|
+
|
|
135
157
|
// The elements are ranked by priority
|
|
136
158
|
const FAILURE_FILTERS = [{
|
|
137
159
|
// NOTE(mmarchini): infra-related issues should have the highest priority, as
|
|
138
160
|
// they can cause other issues to happen.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
];
|
|
153
|
-
return failureMatcher(InfraFailure, patterns, ctx, text);
|
|
161
|
+
Failure: InfraFailure,
|
|
162
|
+
patterns: [{
|
|
163
|
+
pattern: /Read-only file system/g,
|
|
164
|
+
context: { index: 0, contextBefore: 1, contextAfter: 0 }
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
pattern: /Device or resource busy/g,
|
|
168
|
+
context: { index: 0, contextBefore: 1, contextAfter: 0 }
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
pattern: /There is not enough space in the file system./g,
|
|
172
|
+
context: { index: 0, contextBefore: 1, contextAfter: 0 }
|
|
154
173
|
}
|
|
174
|
+
],
|
|
155
175
|
}, {
|
|
156
176
|
// TODO: match indentation to avoid skipping context with '...'
|
|
157
177
|
filter(ctx, text) {
|
|
158
|
-
const
|
|
159
|
-
const matches = text.match(pattern);
|
|
178
|
+
const matches = text.match(tapPattern);
|
|
160
179
|
if (!matches) {
|
|
161
180
|
return null;
|
|
162
181
|
}
|
|
163
|
-
const nonFlaky = matches.filter((m) => !m.includes(
|
|
182
|
+
const nonFlaky = matches.filter((m) => !m.includes(FAILURE_MARKERS.tap.todo));
|
|
164
183
|
if (!nonFlaky.length) {
|
|
165
184
|
return null;
|
|
166
185
|
}
|
|
@@ -169,89 +188,71 @@ const FAILURE_FILTERS = [{
|
|
|
169
188
|
);
|
|
170
189
|
}
|
|
171
190
|
}, {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return failureMatcher(CCTestFailure, patterns, ctx, text);
|
|
178
|
-
}
|
|
191
|
+
Failure: CCTestFailure,
|
|
192
|
+
patterns: [{
|
|
193
|
+
pattern: new RegExp(`${FAILURE_MARKERS.cpp.source}.+`, 'g'),
|
|
194
|
+
context: { index: 0, contextBefore: 5, contextAfter: 0 }
|
|
195
|
+
}],
|
|
179
196
|
}, {
|
|
180
197
|
// VS compilation error
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return failureMatcher(BuildFailure, patterns, ctx, text);
|
|
187
|
-
}
|
|
198
|
+
Failure: BuildFailure,
|
|
199
|
+
patterns: [{
|
|
200
|
+
pattern: /error C\d+:/mg,
|
|
201
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
202
|
+
}],
|
|
188
203
|
}, {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return failureMatcher(JenkinsFailure, patterns, ctx, text);
|
|
198
|
-
}
|
|
204
|
+
Failure: JenkinsFailure,
|
|
205
|
+
patterns: [{
|
|
206
|
+
pattern: /java\.io\.IOException.+/g,
|
|
207
|
+
context: { index: -1, contextBefore: 0, contextAfter: 5 }
|
|
208
|
+
}, {
|
|
209
|
+
pattern: /Build timed out/g,
|
|
210
|
+
context: { index: 0, contextBefore: 0, contextAfter: 1 }
|
|
211
|
+
}],
|
|
199
212
|
}, {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
context: { index: 0, contextBefore: 0, contextAfter: 0 }
|
|
209
|
-
}, {
|
|
210
|
-
pattern: /warning: failed to remove .+/g,
|
|
211
|
-
context: { index: 0, contextBefore: 0, contextAfter: 0 }
|
|
212
|
-
}];
|
|
213
|
-
return failureMatcher(GitFailure, patterns, ctx, text);
|
|
214
|
-
}
|
|
213
|
+
Failure: GitFailure,
|
|
214
|
+
patterns: [...FAILURE_MARKERS.git.map(({ start, end }) => ({
|
|
215
|
+
pattern: new RegExp(`${start.source}[\\s\\S]+${end.source}`, 'mg'),
|
|
216
|
+
context: { index: 0, contextBefore: 0, contextAfter: 0 }
|
|
217
|
+
})), {
|
|
218
|
+
pattern: /warning: failed to remove .+/g,
|
|
219
|
+
context: { index: 0, contextBefore: 0, contextAfter: 0 }
|
|
220
|
+
}],
|
|
215
221
|
}, {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return failureMatcher(GitFailure, patterns, ctx, text);
|
|
228
|
-
}
|
|
222
|
+
Failure: GitFailure,
|
|
223
|
+
patterns: [{
|
|
224
|
+
pattern: /ERROR: Error fetching .+/g,
|
|
225
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
226
|
+
}, {
|
|
227
|
+
pattern: /hudson\.plugins\.git\.GitException+/g,
|
|
228
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
229
|
+
}, {
|
|
230
|
+
pattern: /Cannot rebase: .+/g,
|
|
231
|
+
context: { index: 0, contextBefore: 0, contextAfter: 1 }
|
|
232
|
+
}],
|
|
229
233
|
}, {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return failureMatcher(BuildFailure, patterns, ctx, text);
|
|
250
|
-
}
|
|
234
|
+
Failure: BuildFailure,
|
|
235
|
+
patterns: [{
|
|
236
|
+
pattern: /sh: line /g,
|
|
237
|
+
context: { index: 0, contextBefore: 0, contextAfter: 1 }
|
|
238
|
+
}, {
|
|
239
|
+
pattern: /fatal error:/g,
|
|
240
|
+
context: { index: 0, contextBefore: 0, contextAfter: 1 }
|
|
241
|
+
}, {
|
|
242
|
+
pattern: /dtrace: failed to compile script/g,
|
|
243
|
+
context: { index: 0, contextBefore: 0, contextAfter: 1 }
|
|
244
|
+
}, {
|
|
245
|
+
pattern: /ERROR: .+/g,
|
|
246
|
+
// Pick the last one
|
|
247
|
+
context: { index: -1, contextBefore: 0, contextAfter: 5 }
|
|
248
|
+
}, {
|
|
249
|
+
// Pick the first one
|
|
250
|
+
pattern: /Error: .+/g,
|
|
251
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
252
|
+
}],
|
|
251
253
|
}, {
|
|
252
254
|
filter(ctx, text) {
|
|
253
|
-
const
|
|
254
|
-
const matches = text.match(pattern);
|
|
255
|
+
const matches = text.match(fatalPattern);
|
|
255
256
|
if (!matches) {
|
|
256
257
|
return null;
|
|
257
258
|
}
|
|
@@ -259,30 +260,42 @@ const FAILURE_FILTERS = [{
|
|
|
259
260
|
return [new BuildFailure(ctx, reason)];
|
|
260
261
|
}
|
|
261
262
|
}, {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return failureMatcher(BuildFailure, patterns, ctx, text);
|
|
283
|
-
}
|
|
263
|
+
Failure: BuildFailure,
|
|
264
|
+
patterns: [{
|
|
265
|
+
pattern: /FATAL: .+/g,
|
|
266
|
+
context: { index: -1, contextBefore: 0, contextAfter: 5 }
|
|
267
|
+
}, {
|
|
268
|
+
pattern: /make.*: write error/mg,
|
|
269
|
+
context: { index: 0, contextBefore: 0, contextAfter: 3 }
|
|
270
|
+
}, {
|
|
271
|
+
pattern: /error: .+/g,
|
|
272
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
273
|
+
}, {
|
|
274
|
+
pattern: /Makefile:.+failed/g,
|
|
275
|
+
context: { index: 0, contextBefore: 0, contextAfter: 5 }
|
|
276
|
+
}, {
|
|
277
|
+
pattern: /make.*: .+ Error \d.*/g,
|
|
278
|
+
context: { index: 0, contextBefore: 0, contextAfter: 3 }
|
|
279
|
+
}, {
|
|
280
|
+
pattern: /warning: failed .+/g,
|
|
281
|
+
context: { index: 0, contextBefore: 0, contextAfter: 3 }
|
|
282
|
+
}],
|
|
284
283
|
}];
|
|
285
284
|
|
|
285
|
+
// Share recognition patterns with streaming consumers without exposing the
|
|
286
|
+
// parser's constructors, filter priority, or context-selection machinery.
|
|
287
|
+
export const FAILURE_PATTERNS = {
|
|
288
|
+
infrastructure: FAILURE_FILTERS
|
|
289
|
+
.filter(({ Failure }) => Failure === InfraFailure)
|
|
290
|
+
.flatMap(({ patterns }) => patterns.map(({ pattern }) => pattern)),
|
|
291
|
+
diagnostic: [
|
|
292
|
+
...FAILURE_FILTERS
|
|
293
|
+
.filter(({ Failure }) => Failure && Failure !== InfraFailure && Failure !== CCTestFailure)
|
|
294
|
+
.flatMap(({ patterns }) => patterns.map(({ pattern }) => pattern)),
|
|
295
|
+
fatalPattern
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
|
|
286
299
|
export default class CIFailureParser {
|
|
287
300
|
constructor(ctx, text) {
|
|
288
301
|
this.ctx = ctx;
|
|
@@ -291,8 +304,10 @@ export default class CIFailureParser {
|
|
|
291
304
|
|
|
292
305
|
parse() {
|
|
293
306
|
const text = this.text;
|
|
294
|
-
for (const { filter } of FAILURE_FILTERS) {
|
|
295
|
-
const result = filter
|
|
307
|
+
for (const { filter, Failure, patterns } of FAILURE_FILTERS) {
|
|
308
|
+
const result = filter
|
|
309
|
+
? filter(this.ctx, text)
|
|
310
|
+
: failureMatcher(Failure, patterns, this.ctx, text);
|
|
296
311
|
// TODO: we may want to concat certain types of failures
|
|
297
312
|
if (result) {
|
|
298
313
|
return result;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
|
|
3
|
+
import { FAILURE_MARKERS, FAILURE_PATTERNS } from './ci_failure_parser.js';
|
|
4
|
+
|
|
5
|
+
function createMatcher(patterns) {
|
|
6
|
+
// Preserve matching flags without sharing RegExp.lastIndex between scans.
|
|
7
|
+
const matchers = patterns.map(pattern =>
|
|
8
|
+
new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, '')));
|
|
9
|
+
return text => matchers.some(pattern => pattern.test(text));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const diagnostic = createMatcher(FAILURE_PATTERNS.diagnostic);
|
|
13
|
+
const infrastructure = createMatcher(FAILURE_PATTERNS.infrastructure);
|
|
14
|
+
const gitStart = createMatcher(FAILURE_MARKERS.git.map(({ start }) => start));
|
|
15
|
+
const gitEnd = createMatcher(FAILURE_MARKERS.git.map(({ end }) => end));
|
|
16
|
+
|
|
17
|
+
function fileAliases(filename) {
|
|
18
|
+
const paths = new Set([filename]);
|
|
19
|
+
if (filename.startsWith('test/')) {
|
|
20
|
+
paths.add(filename.slice(5));
|
|
21
|
+
paths.add(filename.slice(5).replace(/\.(?:js|mjs|cjs|out)$/, ''));
|
|
22
|
+
} else if (filename.startsWith('lib/')) {
|
|
23
|
+
paths.add(filename.slice(4));
|
|
24
|
+
}
|
|
25
|
+
return [...paths];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Emit overlapping, normalized windows rather than buffering whole log lines.
|
|
29
|
+
// Explicit line boundaries keep network chunk boundaries out of the matching rules.
|
|
30
|
+
async function * logWindows(source, overlap) {
|
|
31
|
+
const decoder = new StringDecoder('utf8');
|
|
32
|
+
let tail = '';
|
|
33
|
+
let lineStart = true;
|
|
34
|
+
let afterBackslash = false;
|
|
35
|
+
|
|
36
|
+
function * consume(raw) {
|
|
37
|
+
raw = raw.replace(/\r/g, '');
|
|
38
|
+
let text = raw.replace(/\\+/g, '/');
|
|
39
|
+
if (afterBackslash && raw.startsWith('\\')) text = text.slice(1);
|
|
40
|
+
if (raw) afterBackslash = raw.endsWith('\\');
|
|
41
|
+
for (let offset = 0; offset < text.length;) {
|
|
42
|
+
const newline = text.indexOf('\n', offset);
|
|
43
|
+
const end = Math.min(offset + 8192, newline < 0 ? text.length : newline + 1);
|
|
44
|
+
const window = tail + text.slice(offset, end);
|
|
45
|
+
const lineEnd = end === newline + 1;
|
|
46
|
+
yield { text: window, lineStart, lineEnd };
|
|
47
|
+
if (lineEnd) {
|
|
48
|
+
tail = '';
|
|
49
|
+
lineStart = true;
|
|
50
|
+
} else {
|
|
51
|
+
lineStart &&= window.length <= overlap;
|
|
52
|
+
tail = window.slice(-overlap);
|
|
53
|
+
}
|
|
54
|
+
offset = end;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for await (const chunk of source) yield * consume(decoder.write(chunk));
|
|
59
|
+
yield * consume(decoder.end());
|
|
60
|
+
if (tail) yield { text: `${tail}\n`, lineStart, lineEnd: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Each line is reduced to a filename and failure markers. Neither a giant line
|
|
64
|
+
// nor a giant TAP block needs to survive in memory.
|
|
65
|
+
async function * failureLines(windows, matchFile) {
|
|
66
|
+
let line = {};
|
|
67
|
+
for await (const { text, lineStart, lineEnd } of windows) {
|
|
68
|
+
line.file ??= matchFile(text, lineStart);
|
|
69
|
+
line.diagnostic ||= diagnostic(text);
|
|
70
|
+
line.infrastructure ||= infrastructure(text);
|
|
71
|
+
line.cpp ||= FAILURE_MARKERS.cpp.test(text);
|
|
72
|
+
line.tapStart ||= lineStart && FAILURE_MARKERS.tap.start.test(text);
|
|
73
|
+
line.tapEnd ||= lineEnd && FAILURE_MARKERS.tap.end.test(text);
|
|
74
|
+
line.todo ||= text.includes(FAILURE_MARKERS.tap.todo);
|
|
75
|
+
line.gitStart ||= gitStart(text);
|
|
76
|
+
line.gitEnd ||= gitEnd(text);
|
|
77
|
+
if (lineEnd) {
|
|
78
|
+
yield line;
|
|
79
|
+
line = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function findFailure(lines) {
|
|
85
|
+
let history = [];
|
|
86
|
+
let followingLines = 0;
|
|
87
|
+
let tap = null;
|
|
88
|
+
let git = null;
|
|
89
|
+
|
|
90
|
+
for await (const line of lines) {
|
|
91
|
+
const precedingLines = history;
|
|
92
|
+
history = [...history, line.file].slice(-5);
|
|
93
|
+
if (line.tapStart) {
|
|
94
|
+
tap = {};
|
|
95
|
+
followingLines = 0;
|
|
96
|
+
}
|
|
97
|
+
if (tap) {
|
|
98
|
+
tap.file ??= line.file;
|
|
99
|
+
tap.todo ||= line.todo;
|
|
100
|
+
// A later TODO can mark this as an expected failure; wait for the ending.
|
|
101
|
+
if (line.tapEnd) {
|
|
102
|
+
if (!tap.todo && tap.file) return tap.file;
|
|
103
|
+
tap = null;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (line.gitStart) git = {};
|
|
109
|
+
if (git) {
|
|
110
|
+
git.file ??= line.file;
|
|
111
|
+
if (line.gitEnd) {
|
|
112
|
+
if (git.file) return git.file;
|
|
113
|
+
git = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (line.infrastructure || line.cpp) {
|
|
117
|
+
const before = line.cpp ? 5 : 1;
|
|
118
|
+
const file = line.file || precedingLines.slice(-before).find(Boolean);
|
|
119
|
+
if (file) return file;
|
|
120
|
+
}
|
|
121
|
+
if (line.diagnostic) followingLines = 6;
|
|
122
|
+
if (followingLines > 0) {
|
|
123
|
+
followingLines--;
|
|
124
|
+
if (line.file) return line.file;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class FailureFileScanner {
|
|
130
|
+
constructor(filenames) {
|
|
131
|
+
const aliases = [...filenames].map(filename => ({ filename, paths: fileAliases(filename) }));
|
|
132
|
+
this.overlap = Math.max(256,
|
|
133
|
+
...aliases.flatMap(({ paths }) => paths.map(path => path.length + 2)));
|
|
134
|
+
const matchers = aliases.map(({ filename, paths }) => ({
|
|
135
|
+
filename,
|
|
136
|
+
patterns: paths.map(path => {
|
|
137
|
+
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
138
|
+
// Both delimiters must be real: a window boundary is not a path boundary.
|
|
139
|
+
return new RegExp(`[^\\w.-]${escaped}(?=[^\\w./-])`);
|
|
140
|
+
})
|
|
141
|
+
}));
|
|
142
|
+
this.matchFile = (text, lineStart) => {
|
|
143
|
+
// The start of a real log line is also a valid left delimiter.
|
|
144
|
+
if (lineStart) text = `\n${text}`;
|
|
145
|
+
return matchers.find(({ patterns }) =>
|
|
146
|
+
patterns.some(pattern => pattern.test(text)))?.filename;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
scan(source) {
|
|
151
|
+
return findFailure(failureLines(logWindows(source, this.overlap), this.matchFile));
|
|
152
|
+
}
|
|
153
|
+
}
|