function unique(arr) {
return Array.from(new Set(arr).values());
}
function pickContext(matches, text, {
index = 0, // which one in the matches should be picked
contextBefore = 0,
contextAfter = 0
}) {
if (index < 0) { index = matches.length + index; }
const match = matches[index];
const offset = text.indexOf(match);
let after = offset + match.length;
for (let i = 0; i < contextAfter; ++i) {
const next = text.indexOf('\n', after + 1);
after = next > 0 ? next : after;
}
let before = offset;
for (let i = 0; i < contextBefore; ++i) {
const next = text.lastIndexOf('\n', before - 1);
before = next > 0 ? next : before;
}
return text.slice(before, after);
}
const BUILD_FAILURE = 'BUILD_FAILURE';
const JS_TEST_FAILURE = 'JS_TEST_FAILURE';
const CC_TEST_FAILURE = 'CC_TEST_FAILURE';
const JENKINS_FAILURE = 'JENKINS_FAILURE';
const GIT_FAILURE = 'GIT_FAILURE';
const NCU_FAILURE = 'NCU_FAILURE';
const RESUME_FAILURE = 'RESUME_FAILURE';
const INFRA_FAILURE = 'INFRA_FAILURE';
const FAILURE_TYPES = {
BUILD_FAILURE, JS_TEST_FAILURE, CC_TEST_FAILURE,
JENKINS_FAILURE, GIT_FAILURE, NCU_FAILURE, RESUME_FAILURE,
INFRA_FAILURE
};
class CIResult {
constructor(ctx, reason) {
this.url = ctx.url || ctx.consoleUIUrl || ctx.jobUrl;
this.builtOn = ctx.builtOn;
this.reason = reason;
// Default: the first line is the highlight, we will slice
// the context to make it so.
// TODO: better highlights
this.highlight = 0;
}
}
// Usually need a fix to the build files
class BuildFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = BUILD_FAILURE;
}
}
// Usually needs to fix something in the Jenkins agent (or just restart it)
class InfraFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = INFRA_FAILURE;
}
}
// Usually needs a fix in the test or the core
class JSTestFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = JS_TEST_FAILURE;
// Example: not ok 749 parallel/test-http-readable-data-event
this.file = this.reason.split('\n')[this.highlight].split(' ').pop();
this.severity = this.reason.match(/^\s+severity: (\w+)/m)[1];
}
}
// Usually needs a fix in the test or the core
class CCTestFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = CC_TEST_FAILURE;
}
}
// Usually needs someone to log into the machines and fix it
class JenkinsFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = JENKINS_FAILURE;
}
}
// Usually need a fix to the build scripts or in workers
class GitFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = GIT_FAILURE;
}
}
// Failures in this tool, we wrap them to avoid exceptions when
// walking the CI
class NCUFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = NCU_FAILURE;
}
}
// Refs: https://github.com/nodejs/build/issues/1496
class ResumeFailure extends CIResult {
constructor(ctx, reason) {
super(ctx, reason);
this.type = RESUME_FAILURE;
}
}
function failureMatcher(Failure, patterns, ctx, text) {
for (const pattern of patterns) {
const matches = text.match(pattern.pattern);
if (!matches) {
continue;
}
const reason = pickContext(matches, text, pattern.context).trim();
return [new Failure(ctx, reason)];
}
return null;
}
const fatalPattern = /fatal: .+/g;
// Shared by the whole-log parser and the streaming file scanner.
export const FAILURE_MARKERS = {
tap: {
start: /^not ok \d+/,
end: / {2}\.\.\.\r?\n/,
todo: '# TODO :'
},
cpp: /\[ {2}FAILED {2}\]/,
git: [{
start: /Changes not staged for commit:/,
end: /no changes added to commit/
}, {
start: /error: Your local changes to the following files/,
end: /Failed to merge in the changes./
}]
};
const tapPattern = new RegExp(
`${FAILURE_MARKERS.tap.start.source}[\\s\\S]+?${FAILURE_MARKERS.tap.end.source}`, 'mg');
// The elements are ranked by priority
const FAILURE_FILTERS = [{
// NOTE(mmarchini): infra-related issues should have the highest priority, as
// they can cause other issues to happen.
Failure: InfraFailure,
patterns: [{
pattern: /Read-only file system/g,
context: { index: 0, contextBefore: 1, contextAfter: 0 }
},
{
pattern: /Device or resource busy/g,
context: { index: 0, contextBefore: 1, contextAfter: 0 }
},
{
pattern: /There is not enough space in the file system./g,
context: { index: 0, contextBefore: 1, contextAfter: 0 }
}
],
}, {
// TODO: match indentation to avoid skipping context with '...'
filter(ctx, text) {
const matches = text.match(tapPattern);
if (!matches) {
return null;
}
const nonFlaky = matches.filter((m) => !m.includes(FAILURE_MARKERS.tap.todo));
if (!nonFlaky.length) {
return null;
}
return unique(nonFlaky).map(
match => new JSTestFailure(ctx, match)
);
}
}, {
Failure: CCTestFailure,
patterns: [{
pattern: new RegExp(`${FAILURE_MARKERS.cpp.source}.+`, 'g'),
context: { index: 0, contextBefore: 5, contextAfter: 0 }
}],
}, {
// VS compilation error
Failure: BuildFailure,
patterns: [{
pattern: /error C\d+:/mg,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}],
}, {
Failure: JenkinsFailure,
patterns: [{
pattern: /java\.io\.IOException.+/g,
context: { index: -1, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /Build timed out/g,
context: { index: 0, contextBefore: 0, contextAfter: 1 }
}],
}, {
Failure: GitFailure,
patterns: [...FAILURE_MARKERS.git.map(({ start, end }) => ({
pattern: new RegExp(`${start.source}[\\s\\S]+${end.source}`, 'mg'),
context: { index: 0, contextBefore: 0, contextAfter: 0 }
})), {
pattern: /warning: failed to remove .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 0 }
}],
}, {
Failure: GitFailure,
patterns: [{
pattern: /ERROR: Error fetching .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /hudson\.plugins\.git\.GitException+/g,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /Cannot rebase: .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 1 }
}],
}, {
Failure: BuildFailure,
patterns: [{
pattern: /sh: line /g,
context: { index: 0, contextBefore: 0, contextAfter: 1 }
}, {
pattern: /fatal error:/g,
context: { index: 0, contextBefore: 0, contextAfter: 1 }
}, {
pattern: /dtrace: failed to compile script/g,
context: { index: 0, contextBefore: 0, contextAfter: 1 }
}, {
pattern: /ERROR: .+/g,
// Pick the last one
context: { index: -1, contextBefore: 0, contextAfter: 5 }
}, {
// Pick the first one
pattern: /Error: .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}],
}, {
filter(ctx, text) {
const matches = text.match(fatalPattern);
if (!matches) {
return null;
}
const reason = unique(matches).join('\n');
return [new BuildFailure(ctx, reason)];
}
}, {
Failure: BuildFailure,
patterns: [{
pattern: /FATAL: .+/g,
context: { index: -1, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /make.*: write error/mg,
context: { index: 0, contextBefore: 0, contextAfter: 3 }
}, {
pattern: /error: .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /Makefile:.+failed/g,
context: { index: 0, contextBefore: 0, contextAfter: 5 }
}, {
pattern: /make.*: .+ Error \d.*/g,
context: { index: 0, contextBefore: 0, contextAfter: 3 }
}, {
pattern: /warning: failed .+/g,
context: { index: 0, contextBefore: 0, contextAfter: 3 }
}],
}];
// Share recognition patterns with streaming consumers without exposing the
// parser's constructors, filter priority, or context-selection machinery.
export const FAILURE_PATTERNS = {
infrastructure: FAILURE_FILTERS
.filter(({ Failure }) => Failure === InfraFailure)
.flatMap(({ patterns }) => patterns.map(({ pattern }) => pattern)),
diagnostic: [
...FAILURE_FILTERS
.filter(({ Failure }) => Failure && Failure !== InfraFailure && Failure !== CCTestFailure)
.flatMap(({ patterns }) => patterns.map(({ pattern }) => pattern)),
fatalPattern
]
};
export default class CIFailureParser {
constructor(ctx, text) {
this.ctx = ctx;
this.text = text;
}
parse() {
const text = this.text;
for (const { filter, Failure, patterns } of FAILURE_FILTERS) {
const result = filter
? filter(this.ctx, text)
: failureMatcher(Failure, patterns, this.ctx, text);
// TODO: we may want to concat certain types of failures
if (result) {
return result;
}
}
return null;
}
}
CIFailureParser.FAILURE_TYPES = FAILURE_TYPES;
CIFailureParser.FAILURE_CONSTRUCTORS = {
BUILD_FAILURE: BuildFailure,
JENKINS_FAILURE: JenkinsFailure,
JS_TEST_FAILURE: JSTestFailure,
CC_TEST_FAILURE: CCTestFailure,
GIT_FAILURE: GitFailure,
NCU_FAILURE: NCUFailure,
RESUME_FAILURE: ResumeFailure
};
CIFailureParser.CIResult = CIResult;
CIFailureParser.FAILURE_TYPES_NAME = {
BUILD_FAILURE: 'Build Failure',
JENKINS_FAILURE: 'Jenkins Failure',
JS_TEST_FAILURE: 'JSTest Failure',
CC_TEST_FAILURE: 'CCTest Failure',
GIT_FAILURE: 'Git Failure',
NCU_FAILURE: 'node-core-utils failure',
RESUME_FAILURE: 'resume failure'
};