blob: 2fd3f15f8981271aa5f40febd98d292f05f71166 [file]
import {assert} from '@open-wc/testing';
import './test-setup';
import {
BuildbucketV2Client,
Builder,
BuildStatus,
RETRY_FAILED_TAG,
} from '../buildbucket-client';
import {
ResultDbV1Client,
TestVariantStatus,
TestStatus,
TestVariant,
} from '../resultdb-client';
import {ChecksFetcher, Build} from '../checks-fetcher';
import {ChecksDB} from '../checks-db';
import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
import {
Category,
ChangeData,
LinkIcon,
RunStatus,
} from '@gerritcodereview/typescript-api/checks';
import {
AccountInfo,
BranchName,
ChangeId,
ChangeInfo,
ChangeInfoId,
ChangeStatus,
GitRef,
NumericChangeId,
PatchSetNumber,
RevisionPatchSetNum,
RepoName,
RevisionKind,
Timestamp,
} from '@gerritcodereview/typescript-api/rest-api';
import {FaultAttribute} from '../failure-analysis';
suite('checks-fetcher tests', () => {
let fetcher: ChecksFetcher;
let cache: ChecksDB;
let sandbox: sinon.SinonSandbox;
let plugin: PluginApi;
let changeObject: any;
setup(() => {
sandbox = sinon.createSandbox();
sandbox
.stub(BuildbucketV2Client.prototype, 'getAuthorizationHeader')
.returns(
new Promise((resolve: any, _) =>
resolve({access_token: 'accessToken', expires_at: 0})
)
);
sandbox
.stub(ResultDbV1Client.prototype, 'getAuthorizationHeader')
.returns(
new Promise((resolve: any, _) =>
resolve({access_token: 'accessToken', expires_at: 0})
)
);
sandbox
.stub(ChecksFetcher.prototype, 'getAuthorizationHeader')
.returns(
new Promise((resolve: any, _) =>
resolve({access_token: 'accessToken', expires_at: 0})
)
);
plugin = {
restApi: () => {
return {
get: () => {
return {
buckets: ['foo.bar.baz'],
hideRetryButton: false,
};
},
};
},
getPluginName: () => 'buildbucket',
checks: () => {
return {
announceUpdate: () => {},
};
},
} as unknown as PluginApi;
cache = new ChecksDB('checks');
fetcher = new ChecksFetcher(
plugin,
'buildbucket.example.com',
2,
cache,
15
);
fetcher.retryEnabled = true;
changeObject = {
changeNumber: 123456,
patchsetNumber: 1,
repo: 'foo/bar',
changeInfo: {
status: ChangeStatus.NEW,
revisions: [{kind: RevisionKind.REWORK, _number: 1}],
},
};
});
teardown(() => {
fetcher.cache.delete();
localStorage.clear();
sandbox.restore();
});
function stubFetch(response: any) {
sandbox.stub(window, 'fetch').returns(
Promise.resolve({
ok: true,
text: async () => `)]}'${JSON.stringify(response)}`,
} as Response)
);
}
test('fetchTestVariants gets an empty list for no TestVariants', async () => {
stubFetch({nextPageToken: 'foo'});
const build: Build = {
infra: {
resultdb: {
hostname: 'resultdb.example.com',
invocation: '123456',
},
},
} as Build;
assert.deepEqual(await fetcher.fetchTestVariants(build, false, 123456), []);
});
test('fetchTestVariants can query successful builds', async () => {
const variant = {testId: 'foo'} as TestVariant;
stubFetch({testVariants: [variant]});
const build: Build = {
status: BuildStatus.SUCCESS,
infra: {
resultdb: {
hostname: 'resultdb.example.com',
invocation: '123456',
},
},
} as Build;
assert.deepEqual(await fetcher.fetchTestVariants(build, false, 123456), []);
assert.deepEqual(await fetcher.fetchTestVariants(build, true, 123456), [
variant,
]);
});
test('fetchTestVariants queries unexpected TestVariants', async () => {
stubFetch({
nextPageToken: 'foo',
testVariants: [
{status: TestVariantStatus.UNEXPECTED},
{status: TestVariantStatus.UNEXPECTED},
],
});
const build = {
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
} as Build;
assert.deepEqual(await fetcher.fetchTestVariants(build, false, 123456), [
{status: TestVariantStatus.UNEXPECTED} as TestVariant,
{status: TestVariantStatus.UNEXPECTED} as TestVariant,
]);
sinon.assert.calledOnce(fetch as any);
});
test('fetchBuilds fetches builds', async () => {
const firstResponse = {
responses: [
{
searchBuilds: {
builds: [
{
id: '1',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
nextPageToken: 'foo',
},
},
{searchBuilds: {}},
{
searchBuilds: {
builds: [
{
id: '3',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
nextPageToken: 'bar',
},
},
],
};
const secondResponse = {
responses: [
{
searchBuilds: {
builds: [
{
id: '2',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
nextPageToken: 'baz',
},
},
{searchBuilds: {}},
],
};
const thirdResponse = {responses: [{searchBuilds: {}}]};
const fetch = sandbox.stub(window, 'fetch');
fetch.onCall(0).returns(
Promise.resolve({
ok: true,
text: async () => `)]}'${JSON.stringify(firstResponse)}`,
} as Response)
);
fetch.onCall(1).returns(
Promise.resolve({
ok: true,
text: async () => `)]}'${JSON.stringify(secondResponse)}`,
} as Response)
);
fetch.onCall(2).returns(
Promise.resolve({
ok: true,
text: async () => `)]}'${JSON.stringify(thirdResponse)}`,
} as Response)
);
assert.deepEqual(
await fetcher.fetchBuilds(
123456,
[1, 2, 3],
'',
false,
new AbortController()
),
[
{
id: '1',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '2',
tags: [],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
]
);
sinon.assert.calledThrice(fetch);
});
test('fetchBuilds filters experimental builds', async () => {
const expTag = {key: 'cq_experimental', value: 'true'};
const nonExpTag = {key: 'cq_experimental', value: 'false'};
stubFetch({
responses: [
{
searchBuilds: {
builds: [
{
id: '1',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
},
},
{
searchBuilds: {
builds: [
{
id: '2',
tags: [expTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
},
},
],
});
assert.deepEqual(
await fetcher.fetchBuilds(
123456,
[1, 2],
'project',
false,
new AbortController()
),
[
{
id: '1',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
]
);
assert.deepEqual(
await fetcher.fetchBuilds(
123456,
[1, 2],
'project',
true,
new AbortController()
),
[
{
id: '1',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '2',
tags: [expTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [nonExpTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
]
);
});
test('fetchBuilds hides builds tagged hide-in-gerrit', async () => {
const hiddenTag = {key: 'hide-in-gerrit', value: 'pointless'};
const nonHiddenTag = {key: 'hide-in-gerrit', value: 'false'};
stubFetch({
responses: [
{
searchBuilds: {
builds: [
{
id: '1',
tags: [nonHiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
},
},
{
searchBuilds: {
builds: [
{
id: '2',
tags: [hiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [nonHiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
},
},
{
searchBuilds: {
builds: [
{
id: '4',
tags: [hiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
],
},
},
],
});
assert.deepEqual(
await fetcher.fetchBuilds(
123456,
[1, 2],
'project',
true,
new AbortController()
),
[
{
id: '1',
tags: [nonHiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
tags: [nonHiddenTag],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
},
]
);
});
test('fetchDisplayedBuilds handles equivalent patchsets', async () => {
const stub = sandbox.stub(ChecksFetcher.prototype, 'fetchBuilds');
const build5: Build = {
id: '5',
tags: [{key: 'cq_experimental', value: 'false'}],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
};
const build3: Build = {
id: '3',
tags: [{key: 'cq_experimental', value: 'false'}],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
};
const build2: Build = {
id: '2',
tags: [{key: 'cq_experimental', value: 'false'}],
builder: {},
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
};
stub
.withArgs(123, [6], 'project', sinon.match.any, sinon.match.any)
.returns(Promise.resolve([]));
stub
.withArgs(123, [5], 'project', sinon.match.any, sinon.match.any)
.returns(Promise.resolve([build5]));
stub
.withArgs(123, [2, 3], 'project', sinon.match.any, sinon.match.any)
.returns(Promise.resolve([build2, build3]));
stub
.withArgs(123, [2], 'project', sinon.match.any, sinon.match.any)
.returns(Promise.resolve([build2]));
const changeInfo: ChangeInfo = {
revisions: {
'1': {
kind: RevisionKind.REWORK,
_number: 6 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
'2': {
kind: RevisionKind.REWORK,
_number: 5 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
'3': {
kind: RevisionKind.TRIVIAL_REBASE,
_number: 4 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
'4': {
kind: RevisionKind.NO_CHANGE,
_number: 3 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
'5': {
kind: RevisionKind.REWORK,
_number: 2 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
'6': {
kind: RevisionKind.REWORK,
_number: 1 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
},
status: ChangeStatus.MERGED,
id: '' as ChangeInfoId,
project: '' as RepoName,
branch: '' as BranchName,
change_id: '' as ChangeId,
subject: '',
created: '' as Timestamp,
updated: '' as Timestamp,
insertions: 0,
deletions: 0,
_number: 0 as NumericChangeId,
owner: '' as AccountInfo,
reviewers: {},
current_revision_number: 6 as PatchSetNumber,
};
// Fetching PS 3 gets builds 2 and 3.
assert.deepEqual(
await fetcher.fetchDisplayedBuilds(
123,
3,
'project',
changeInfo,
new AbortController()
),
[build2, build3]
);
// Fetching PS 2 gets build 2.
assert.deepEqual(
await fetcher.fetchDisplayedBuilds(
123,
2,
'project',
changeInfo,
new AbortController()
),
[build2]
);
// Fetching PS 6 gets build 5.
assert.deepEqual(
await fetcher.fetchDisplayedBuilds(
123,
6,
'project',
changeInfo,
new AbortController()
),
[build5]
);
});
test('convertAttemptsToRuns creates all runs for all attempts', async () => {
const attempts: Build[] = [
{
id: '3',
status: BuildStatus.INFRA_FAILURE,
builder: {},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '2',
status: BuildStatus.SUCCESS,
builder: {},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '1',
status: BuildStatus.FAILURE,
builder: {},
createTime: '',
startTime: '',
endTime: '',
},
];
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
foo: attempts,
});
assert.strictEqual(runs.length, 3);
assert.strictEqual(runs[0].externalId, '//buildbucket.example.com/build/3');
assert.strictEqual(runs[0].attempt, 1);
assert.strictEqual(runs[1].externalId, '//buildbucket.example.com/build/2');
assert.strictEqual(runs[1].attempt, 2);
assert.strictEqual(runs[2].externalId, '//buildbucket.example.com/build/1');
assert.strictEqual(runs[2].attempt, 3);
});
test('convertAttemptsToRuns normalizes runs against ancestors', async () => {
const attemptsAncestors: Build[] = [
{
id: '6',
status: BuildStatus.FAILURE,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '5',
status: BuildStatus.FAILURE,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '4',
status: BuildStatus.SUCCESS,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '3',
status: BuildStatus.STARTED,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '2',
status: BuildStatus.STARTED,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
{
id: '1',
status: BuildStatus.SCHEDULED,
builder: {builder: 'ancestors'},
createTime: '',
startTime: '',
endTime: '',
},
];
// We want to hide builds that are children of irrelevant builds.
const attemptsIrr: Build[] = [
{
id: '101',
status: BuildStatus.SUCCESS,
builder: {builder: 'maybeirr'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['4', '404'], // Ensure the first ancestorId is used.
},
{
id: '102',
status: BuildStatus.STARTED,
builder: {builder: 'maybeirr'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['3', '404'],
},
{
id: '103',
status: BuildStatus.SCHEDULED,
builder: {builder: 'maybeirr'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['2', '404'],
},
];
// We want to hide failures that are no longer relevant or may
// have another attempt soon.
const attemptsFailures: Build[] = [
{
id: '30',
status: BuildStatus.INFRA_FAILURE,
builder: {builder: 'failures'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['6', '404'], // Ensure the last ancestorId is used.
},
{
id: '20',
status: BuildStatus.FAILURE,
builder: {builder: 'failures'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['5', '405'],
},
];
// We want successes to stay visible even if they might be irrelevant.
const attemptsSuccess: Build[] = [
{
id: '200',
status: BuildStatus.SUCCESS,
builder: {builder: 'successes'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['5', '405'],
},
];
// We want to handle cases where a build's ancestors may have changed between
// attempts.
const attemptsModified: Build[] = [
{
id: '3000',
status: BuildStatus.SUCCESS,
builder: {builder: 'modified'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['6', '404'],
},
{
id: '2000',
status: BuildStatus.SUCCESS,
builder: {builder: 'modified'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['404'],
},
];
const attemptsExtra: Build[] = [
{
id: '404',
status: BuildStatus.SUCCESS,
builder: {builder: 'extra'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['6'],
},
];
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
ancestors: attemptsAncestors,
failures: attemptsFailures,
successes: attemptsSuccess,
modified: attemptsModified,
extra: attemptsExtra,
maybeirr: attemptsIrr,
});
assert.strictEqual(runs.length, 4 + 4 + 2 + 2 + 1 + 4);
const runsAncestors = runs.slice(0, 4);
const actualAncestors = runsAncestors.map(run => [
run.checkName,
run.status,
run.results?.length,
]);
const expectedAncestors = [
['ancestors', RunStatus.COMPLETED, 1],
['ancestors', RunStatus.COMPLETED, 1],
['ancestors', RunStatus.COMPLETED, 1],
['ancestors', RunStatus.RUNNING, 2],
]; // We created an extra result for hidden runs.
assert.deepEqual(expectedAncestors, actualAncestors);
const runsFailures = runs.slice(4, 8);
const actualFailures = runsFailures.map(run => [
run.checkName,
run.status,
run.results?.length,
]);
const expectedFailures = [
['failures', RunStatus.COMPLETED, 1],
['failures', RunStatus.COMPLETED, 1],
['failures', RunStatus.RUNNABLE, 0],
['failures', RunStatus.RUNNABLE, 0],
];
assert.deepEqual(expectedFailures, actualFailures);
const runsSuccesses = runs.slice(8, 10);
const actualSuccesses = runsSuccesses.map(run => [
run.checkName,
run.status,
run.results?.length,
]);
const expectedSuccesses = [
['successes', RunStatus.RUNNABLE, 0],
['successes', RunStatus.COMPLETED, 1],
];
assert.deepEqual(expectedSuccesses, actualSuccesses);
const runsModified = runs.slice(10, 12);
const actualModified = runsModified.map(run => [
run.checkName,
run.status,
run.results?.length,
]);
const expectedModified = [
['modified', RunStatus.COMPLETED, 1],
['modified', RunStatus.COMPLETED, 1],
];
assert.deepEqual(expectedModified, actualModified);
const runExtra = runs[12];
assert.deepEqual(
[runExtra.checkName, runExtra.status, runExtra.results?.length],
['extra', RunStatus.COMPLETED, 1]
);
const runsIrr = runs.slice(13);
const actualIrr = runsIrr.map(run => [
run.checkName,
run.status,
run.results?.length,
]);
const expectedIrr = [
['maybeirr', RunStatus.RUNNABLE, 0],
['maybeirr', RunStatus.RUNNABLE, 0],
['maybeirr', RunStatus.COMPLETED, 1],
['maybeirr', RunStatus.RUNNING, 1],
];
assert.deepEqual(expectedIrr, actualIrr);
});
test('normalizeAttempts ignores builds without ancestorIds ', () => {
const firstAncestorBuild = {
id: '1',
status: BuildStatus.FAILURE,
builder: {builder: 'ancestors'},
} as Build;
const secondAncestorBuild = {
id: '2',
status: BuildStatus.FAILURE,
builder: {builder: 'ancestors'},
} as Build;
const firstChildBuild = {
id: '101',
status: BuildStatus.FAILURE,
builder: {builder: 'child'},
ancestorIds: [firstAncestorBuild.id],
} as Build;
// Build without ancestorIds.
const secondChildBuild = {
id: '102',
status: BuildStatus.FAILURE,
builder: {builder: 'child'},
} as Build;
const thirdChildBuild = {
id: '103',
status: BuildStatus.FAILURE,
builder: {builder: 'child'},
ancestorIds: [secondAncestorBuild.id],
} as Build;
const buildsByID = new Map([
[firstAncestorBuild.id, firstAncestorBuild],
[secondAncestorBuild.id, secondAncestorBuild],
[firstChildBuild.id, firstChildBuild],
[secondChildBuild.id, secondChildBuild],
[thirdChildBuild.id, thirdChildBuild],
]);
const childBuildAttempts = [
firstChildBuild,
secondChildBuild,
thirdChildBuild,
] as Build[];
const attemptsByBuilder = {
[firstAncestorBuild.builder.builder!]: [
firstAncestorBuild,
secondAncestorBuild,
],
[firstChildBuild.builder.builder!]: childBuildAttempts,
};
const normalizedAttempts = fetcher.normalizeAttempts(
buildsByID,
attemptsByBuilder,
childBuildAttempts
);
assert.deepEqual(normalizedAttempts, [
childBuildAttempts[0],
childBuildAttempts[2],
]);
});
test('convertAttemptsToRuns handles missing ancestors', async () => {
const runs = await fetcher.convertAttemptsToRuns(1, 1234, 'repo', 1, {
builder: [
{
id: '1',
status: BuildStatus.SCHEDULED,
builder: {builder: 'builder'},
createTime: '',
startTime: '',
endTime: '',
ancestorIds: ['99'], // ID that doesn't exist in attempts.
},
],
});
assert.deepEqual(runs.length, 1);
});
test('convertAttemptsToRuns creates statuses for SCHEDULED and STARTED builds', async () => {
const scheduledBuild: Build = {
builder: {builder: 'foo'},
id: '12345',
status: BuildStatus.SCHEDULED,
createTime: '',
startTime: '',
endTime: '',
};
const startedBuild: Build = {
builder: {builder: 'foo_2'},
id: '12346',
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
};
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
foo: [scheduledBuild],
foo_2: [startedBuild],
});
assert.deepEqual(runs.length, 2);
assert.deepEqual(runs[0].status, RunStatus.SCHEDULED);
assert.deepEqual(runs[1].status, RunStatus.RUNNING);
});
test('fetch returns build variant results for builds', async () => {
sandbox.stub(ChecksFetcher.prototype, 'fetchDisplayedBuilds').returns(
Promise.resolve([
{
builder: {builder: 'foo'},
id: '123456',
endTime: '2017-12-15T01:30:15.05Z',
status: BuildStatus.SUCCESS,
infra: {resultdb: {hostname: 'host', invocation: ''}},
createTime: '',
startTime: '',
},
])
);
sandbox.stub(ChecksFetcher.prototype, 'fetchTestVariants').returns(
Promise.resolve([
{
testMetadata: {name: 'test'},
results: [{result: {name: '', tags: []}}],
variant: '',
exonerations: [],
status: TestVariantStatus.EXPECTED,
},
])
);
fetcher.changeData = changeObject;
await fetcher.remoteFetch(false);
const res = await fetcher.fetch(changeObject);
const {results} = res.runs![0];
assert.strictEqual(results!.length, 2);
assert.strictEqual(results![0].summary, 'foo succeeded.');
assert.strictEqual(
results![1].summary,
'Test test failed but the build succeeded.'
);
});
test(
'fetch does not fetch variant results for builds tagged with ' +
'hide-test-results-in-gerrit',
async () => {
sandbox.stub(ChecksFetcher.prototype, 'fetchDisplayedBuilds').returns(
Promise.resolve([
{
builder: {builder: 'foo'},
id: '123456',
endTime: '2017-12-15T01:30:15.05Z',
status: BuildStatus.FAILURE,
infra: {resultdb: {hostname: 'host', invocation: ''}},
tags: [{key: 'hide-test-results-in-gerrit', value: 'true'}],
createTime: '',
startTime: '',
},
])
);
const variantSpy = sandbox.spy(
ResultDbV1Client.prototype,
'fetchTestVariants'
);
fetcher.changeData = changeObject;
await fetcher.remoteFetch(false);
const res = await fetcher.fetch(changeObject);
const {results} = res.runs![0];
assert.strictEqual(results!.length, 1);
assert.strictEqual(results![0].summary, 'foo failed.');
sinon.assert.notCalled(variantSpy);
}
);
test('convertAttemptsToRuns gets Buildbucket CheckResults', async () => {
const build: Build = {
builder: {builder: 'foo'},
infra: {},
id: '123456',
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
foo: [build],
});
assert.strictEqual(runs.length, 1);
assert.strictEqual(runs[0].results!.length, 1);
assert.strictEqual(runs[0].results![0].summary, 'foo failed.');
});
test('fetch orders Buildbucket CheckResults first', async () => {
sandbox.stub(ChecksFetcher.prototype, 'fetchDisplayedBuilds').returns(
Promise.resolve([
{
builder: {builder: 'foo'},
id: '123456',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
},
])
);
sandbox.stub(ChecksFetcher.prototype, 'fetchTestVariants').returns(
Promise.resolve([
{
testMetadata: {name: 'test0'},
results: [{result: {name: '', tags: []}}],
variant: '',
exonerations: [],
status: TestVariantStatus.EXPECTED,
},
{
testMetadata: {name: 'test1'},
results: [{result: {name: '', tags: []}}],
variant: '',
exonerations: [],
status: TestVariantStatus.EXPECTED,
},
])
);
fetcher.changeData = changeObject;
await fetcher.remoteFetch(false);
const res = await fetcher.fetch(changeObject);
const {results} = res.runs![0];
assert.strictEqual(results!.length, 4);
assert.strictEqual(results![0].summary, 'foo failed.');
assert.strictEqual(
results![3].message,
'More tests may have failed. View all results on the build page.'
);
});
test('convertAttemptsToRuns handles non-critical builds', async () => {
const build: Build = {
builder: {builder: 'foo'},
critical: 'NO',
infra: {},
id: '123456',
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
foo: [build],
});
assert.strictEqual(runs.length, 1);
assert.strictEqual(runs[0].results!.length, 1);
assert.strictEqual(runs[0].results![0].category, Category.WARNING);
assert.strictEqual(runs[0].results![0].summary, 'foo failed.');
assert.isTrue(
runs[0].results![0].tags!.map(t => t.name).includes('Non-critical')
);
});
test('convertAttemptsToRuns sets CheckName based on latest attempt', async () => {
const failedBuild: Build = {
builder: {builder: 'foo'},
critical: 'NO',
infra: {},
id: '1',
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const startedBuild: Build = {
builder: {builder: 'foo'},
critical: 'NO',
infra: {},
id: '2',
status: BuildStatus.STARTED,
createTime: '',
startTime: '',
endTime: '',
};
const infraFailBuild: Build = {
builder: {builder: 'foo'},
critical: 'NO',
infra: {},
id: '3',
status: BuildStatus.INFRA_FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const startedRuns = await fetcher.convertAttemptsToRuns(
4,
1234,
'repo',
4,
{
foo: [failedBuild, infraFailBuild, startedBuild],
}
);
assert.strictEqual(startedRuns[0].checkName, 'foo');
const infraFailedRuns = await fetcher.convertAttemptsToRuns(
4,
1234,
'repo',
4,
{
foo: [failedBuild, failedBuild, infraFailBuild],
}
);
assert.strictEqual(infraFailedRuns[0].checkName, '🟪\u2005\u2005foo');
});
test('convertAttemptsToRuns tags CheckResults for experimental builds', async () => {
const baseBuild: Build = {
builder: {builder: 'foo'},
id: '123456',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const expBuild = {...baseBuild};
expBuild.tags = [{key: 'cq_experimental', value: 'true'}];
const expAttempts: Build[] = [expBuild];
const nonExpBuild = {...baseBuild};
nonExpBuild.tags = [{key: 'cq_experimental', value: 'false'}];
const nonExpAttempts: Build[] = [nonExpBuild];
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
fooExp: expAttempts,
fooNonExp: nonExpAttempts,
});
assert.strictEqual(runs.length, 2);
const expRun = runs[0];
expRun.results!.forEach(result => {
assert.isTrue(result.tags!.map(t => t.name).includes('Experimental'));
});
const nonExpRun = runs[1];
nonExpRun.results!.forEach(result => {
assert.isFalse(result.tags!.map(t => t.name).includes('Experimental'));
});
});
test('convertAttemptsToRuns tags infra failures', async () => {
const baseBuild: Build = {
builder: {builder: 'foo'},
id: '123456',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const infraFailedBuild = {...baseBuild};
infraFailedBuild.status = BuildStatus.INFRA_FAILURE;
const infraFailedAttempts: Build[] = [infraFailedBuild];
const failedBuild = {...baseBuild};
failedBuild.status = BuildStatus.FAILURE;
const failedAttempts: Build[] = [failedBuild];
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
fooInfraFail: infraFailedAttempts,
fooFail: failedAttempts,
});
const infraRun = runs[0];
assert.strictEqual(infraRun.checkName, '🟪\u2005\u2005fooInfraFail');
infraRun.results!.forEach(r => {
assert.isTrue(r.tags!.map(t => t.name).includes('Infra Failure'));
});
const nonInfraRun = runs[1];
assert.strictEqual(nonInfraRun.checkName, 'fooFail');
nonInfraRun.results!.forEach(r => {
assert.isFalse(r.tags!.map(t => t.name).includes('Infra Failure'));
});
});
test(
'convertAttemptsToRuns categorizes failed test results as WARNING if ' +
'build succeeded',
async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
endTime: '2017-12-15T01:30:15.05Z',
status: BuildStatus.SUCCESS,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'test'},
status: TestVariantStatus.UNEXPECTED,
results: [
{
result: {
status: TestStatus.FAIL,
summaryHtml: '<p>foo</p>',
name: '',
tags: [],
},
},
],
variant: '',
variantHash: '123',
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
assert.strictEqual(results.length, 1);
const [result] = results;
assert.strictEqual(result.category, Category.WARNING);
assert.strictEqual(
result.summary,
'Test test failed but the build succeeded.'
);
assert.strictEqual(result.externalId, 'test:123');
assert.strictEqual(result.message, 'Run #1: unexpectedly failed.');
}
);
test('convertVariantsToCheckResults handles flaky results', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.FLAKY,
results: [
{
result: {
status: TestStatus.FAIL,
name: '',
tags: [],
},
},
{
result: {
status: TestStatus.PASS,
expected: true,
name: '',
tags: [],
},
},
],
variant: '',
variantHash: '123',
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const checkResult = results[0];
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(checkResult.summary, 'Test foo-test flaked.');
assert.strictEqual(checkResult.message, 'Run #1: unexpectedly failed.');
assert.strictEqual(checkResult.externalId, 'foo-test:123');
// If includeAdditionalResults is true, show all results.
fetcher.includeAdditionalResults = true;
const additionalResults = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const additionalCheckResult = additionalResults[0];
assert.strictEqual(additionalCheckResult.category, Category.WARNING);
assert.strictEqual(additionalCheckResult.summary, 'Test foo-test flaked.');
assert.strictEqual(
additionalCheckResult.message,
'Run #1: unexpectedly failed. Run #2: expectedly passed.'
);
});
test('convertVariantsToCheckResults handles exonerated results', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.EXONERATED,
results: [{result: {status: TestStatus.PASS, name: '', tags: []}}],
variant: '',
variantHash: '123',
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const checkResult = results[0];
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(checkResult.summary, 'Test foo-test was exonerated.');
assert.strictEqual(checkResult.message, 'Run #1: unexpectedly passed.');
assert.strictEqual(checkResult.externalId, 'foo-test:123');
});
test('convertVariantsToCheckResults handles unexpectedly skipped results', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.UNEXPECTEDLY_SKIPPED,
results: [{result: {status: TestStatus.SKIP, name: '', tags: []}}],
variant: '',
variantHash: '123',
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const checkResult = results[0];
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(
checkResult.summary,
'Test foo-test was unexpectedly skipped.'
);
assert.strictEqual(checkResult.message, 'Run #1: unexpectedly skipped.');
assert.strictEqual(checkResult.externalId, 'foo-test:123');
});
test('convertVariantsToCheckResults categorizes preliminary results', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.STARTED,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
endTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'test0'},
status: TestVariantStatus.UNEXPECTED,
results: [
{
result: {
status: TestStatus.FAIL,
summaryHtml: '<p>foo</p>',
name: '',
tags: [],
},
},
],
variant: '',
exonerations: [],
},
{
testMetadata: {name: 'test1'},
status: TestVariantStatus.UNEXPECTED,
results: [
{
result: {
status: TestStatus.FAIL,
summaryHtml: '<p>foo</p>',
name: '',
tags: [],
},
},
],
variant: '',
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
assert.strictEqual(results.length, 3);
const [checkResult, _, extraResult] = results;
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(checkResult.summary, 'Test test0 failed.');
assert.isTrue(checkResult.tags!.map(t => t.name).includes('Preliminary'));
assert.strictEqual(extraResult.category, Category.INFO);
assert.isTrue(extraResult.tags!.map(t => t.name).includes('Preliminary'));
});
test('convertVariantsToCheckResults tags results with variant def', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
endTime: '2017-12-15T01:30:15.05Z',
status: BuildStatus.FAILURE,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
variant: {def: {test_suite: 'suite_foo', gpu: 'gpu_foo'}},
testMetadata: {name: 'test2'},
status: TestVariantStatus.UNEXPECTED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
exonerations: [],
},
{
variant: {def: {test_suite: 'suite_foo', build_target: 'build_foo'}},
testMetadata: {name: 'test1'},
status: TestVariantStatus.UNEXPECTED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
exonerations: [],
},
{
variant: {def: {test_suite: 'suite_bar', gpu: 'gpu_bar'}},
testMetadata: {name: 'test1'},
status: TestVariantStatus.UNEXPECTED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
exonerations: [],
},
{
variant: {def: {}},
testMetadata: {name: 'test0'},
status: TestVariantStatus.UNEXPECTED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
exonerations: [],
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
assert.isTrue(results[0].summary.includes('test0'));
assert.strictEqual(results[0].tags!.length, 0);
assert.isTrue(results[1].summary.includes('test1'));
assert.strictEqual(results[1].tags!.length, 2);
assert.isTrue(results[1].tags![0].tooltip!.includes('suite_bar'));
assert.isTrue(results[1].tags![1].tooltip!.includes('gpu_bar'));
assert.isTrue(results[2].summary.includes('test1'));
assert.strictEqual(results[2].tags!.length, 2);
assert.isTrue(results[2].tags![0].tooltip!.includes('suite_foo'));
assert.isTrue(results[2].tags![1].tooltip!.includes('build_foo'));
assert.isTrue(results[3].summary.includes('test2'));
assert.strictEqual(results[3].tags!.length, 2);
assert.isTrue(results[3].tags![0].tooltip!.includes('suite_foo'));
assert.isTrue(results[3].tags![1].tooltip!.includes('gpu_foo'));
});
test('convertVariantsToCheckResults includes fault attributions in summary if present in test variant and preexists', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.EXONERATED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
variant: '',
variantHash: '123',
exonerations: [],
faultAttribute: {
comparisonSnapshot: {
sourceBuildId: '8775275012462334081',
sourceStartedUnixTimestamp: '1689668613',
},
snapshotComparisonFaultAttribution:
FaultAttribute.MATCHING_FAILURE_FOUND,
testName: 'foo-test',
},
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const checkResult = results[0];
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(checkResult.summary, 'Test foo-test was exonerated.');
assert.strictEqual(
checkResult.message,
'Run #1: unexpectedly failed as of 7/18/2023, 8:23:33 AM.'
);
assert.strictEqual(checkResult.externalId, 'foo-test:123');
});
test('convertVariantsToCheckResults uses build ID for fault attribution summary if compared snapshot is not finished', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.EXONERATED,
results: [{result: {status: TestStatus.FAIL, name: '', tags: []}}],
variant: '',
variantHash: '123',
exonerations: [],
faultAttribute: {
comparisonSnapshot: {
sourceBuildId: '8775275012462334081',
sourceStartedUnixTimestamp: '',
},
snapshotComparisonFaultAttribution:
FaultAttribute.MATCHING_FAILURE_FOUND,
testName: 'foo-test',
},
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
const checkResult = results[0];
assert.strictEqual(checkResult.category, Category.WARNING);
assert.strictEqual(checkResult.summary, 'Test foo-test was exonerated.');
assert.strictEqual(
checkResult.message,
'Run #1: unexpectedly failed as of b8775275012462334081.'
);
assert.strictEqual(checkResult.externalId, 'foo-test:123');
});
test('convertVariantsToCheckResults tags result rows with fault attributes if present', async () => {
const build: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'foo-test'},
status: TestVariantStatus.EXONERATED,
results: [{result: {status: TestStatus.PASS, name: '', tags: []}}],
variant: '',
exonerations: [],
faultAttribute: {
comparisonSnapshot: {
sourceBuildId: '8775275012462334081',
sourceStartedUnixTimestamp: '1689668613',
},
snapshotComparisonFaultAttribution: FaultAttribute.SUCCESS_FOUND,
testName: 'foo-test',
},
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
assert.isTrue(results[0].summary.includes('foo-test'));
assert.strictEqual(results[0].tags!.length, 1);
assert.strictEqual(results[0].tags![0].name!, 'New failure');
});
test('createScheduleActions returns correct actions', () => {
const startedBuild: Build = {
builder: {builder: 'foo'},
id: '123',
status: BuildStatus.STARTED,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
endTime: '',
};
const startedActions = fetcher.createScheduleActions(
123,
startedBuild,
'repo',
1,
[startedBuild]
);
assert.strictEqual(startedActions.length, 1);
assert.strictEqual(startedActions[0].name, 'Cancel');
const noRetryBuild: Build = {
builder: {builder: 'foo'},
id: '123456',
status: BuildStatus.FAILURE,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
tags: [{key: 'skip-retry-in-gerrit', value: 'true'}],
createTime: '',
startTime: '',
endTime: '',
};
const noRetryActions = fetcher.createScheduleActions(
123,
noRetryBuild,
'repo',
1,
[noRetryBuild]
);
assert.strictEqual(noRetryActions.length, 0);
});
test('convertAttemptsToRuns returns correct build links', async () => {
const successBuild: Build = {
builder: {builder: 'bar', project: 'chromium', bucket: 'baz'},
id: '123456',
summaryMarkdown: 'Build passed',
number: 987,
status: BuildStatus.SUCCESS,
createTime: '',
startTime: '',
endTime: '',
};
const failedBuild: Build = {
builder: {builder: 'foo', project: 'chromium', bucket: 'baz'},
id: '123455',
summaryMarkdown: 'Build failed',
number: 987,
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
};
const runs = await fetcher.convertAttemptsToRuns(4, 1234, 'repo', 4, {
bar: [successBuild],
foo: [failedBuild],
});
const successRun = runs[0];
assert.strictEqual(successRun.statusDescription, 'bar succeeded.');
assert.strictEqual(successRun.results![0].summary, 'bar succeeded.');
assert.strictEqual(successRun.results![0].message, 'Build passed');
assert.deepEqual(successRun.results![0].links, [
{
url: 'https://buildbucket.example.com/build/123456',
tooltip: 'Build',
primary: true,
icon: LinkIcon.EXTERNAL,
},
]);
const failedRun = runs[1];
assert.strictEqual(failedRun.results![0].summary, 'foo failed.');
assert.strictEqual(failedRun.results![0].message, 'Build failed');
assert.deepEqual(failedRun.results![0].links, [
{
url: 'https://buildbucket.example.com/build/123455',
tooltip: 'Build',
primary: true,
icon: LinkIcon.EXTERNAL,
},
]);
});
test('convertVariantsToCheckResults returns correct links', async () => {
// Build is SUCCESSful and should only contain TestVariant CheckResults.
const build: Build = {
builder: {builder: 'foo.1.2', project: 'bar', bucket: 'baz'},
id: '123456',
number: 987,
status: BuildStatus.SUCCESS,
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
createTime: '',
startTime: '',
endTime: '',
};
const variants: TestVariant[] = [
{
testMetadata: {name: 'test0'},
status: TestVariantStatus.UNEXPECTED,
variantHash: 'deadbeef',
testId: 'ninja://:blink_web_tests/bar',
results: [
{
result: {
name: 'invocations/task-foo.com-123',
tags: [{key: 'step_name', value: 'foo on baz (with bar) on baz'}],
},
},
{result: {name: 'this is a bad name', tags: []}},
],
exonerations: [],
variant: '',
},
];
const results = await fetcher.convertVariantsToCheckResults(
build,
variants,
123456
);
assert.deepEqual(results[0].links, [
{
url:
'https://ci.chromium.org/ui/p/bar/builders/baz/foo.1.2/b123456/' +
'test-results?q=ExactID%3Aninja%3A%2F%2F%3Ablink_web_tests%2F' +
'bar+VHash%3Adeadbeef&clean=',
tooltip: 'Test Results',
primary: true,
icon: LinkIcon.EXTERNAL,
},
{
url:
'https://chromium-layout-test-archives.storage.googleapis.com/' +
'results.html?json=bar/baz/foo_1_2/987/foo%20%28with%20bar%29/' +
'full_results_jsonp.js',
tooltip: 'Web Test Results',
primary: false,
icon: LinkIcon.EXTERNAL,
},
{
url: 'https://foo.com/task?id=123',
tooltip: 'Swarming Task',
primary: false,
icon: LinkIcon.EXTERNAL,
},
]);
});
test('fetch experimental builds', async () => {
stubFetch({
responses: [
{
searchBuilds: {
builds: [
{
builder: {builder: 'bar'},
id: '123456',
tags: [{key: 'cq_experimental', value: 'true'}],
infra: {resultdb: {}, swarming: {}},
status: BuildStatus.FAILURE,
},
{
builder: {builder: 'foo'},
id: '654321',
tags: [{key: 'cq_experimental', value: 'false'}],
infra: {resultdb: {}, swarming: {}},
status: BuildStatus.FAILURE,
},
],
},
},
],
});
sandbox
.stub(ChecksFetcher.prototype, 'fetchTestVariants')
.returns(Promise.resolve([]));
const update = sandbox.stub().returns({});
const checks = sandbox.stub().returns({announceUpdate: update});
const plugin: PluginApi = {
checks,
restApi: () => {
return {
get: () => {
return {
buckets: ['foo.bar.baz'],
hideRetryButton: false,
};
},
};
},
getPluginName: () => 'buildbucket',
} as unknown as PluginApi;
// CheckResult should not contain experimental runs.
const cache = new ChecksDB('temp');
const stubbedFetcher = new ChecksFetcher(
plugin,
'buildbucket.example.com',
2,
cache,
15
);
stubbedFetcher.changeData = changeObject;
await stubbedFetcher.remoteFetch(false);
const nonExpResult = await stubbedFetcher.fetch(changeObject);
assert.strictEqual(nonExpResult.runs![0].results!.length, 1);
assert.isFalse(
nonExpResult
.runs![0].results![0].tags!.map(t => t.name)
.includes('Experimental')
);
assert.strictEqual(
nonExpResult.actions![2].name,
'Show Additional Results'
);
// Simulate "Show/Hide Additional" click.
await nonExpResult.actions![2].callback(1);
sinon.assert.calledOnce(update);
// CheckResult should contain experimental runs.
await stubbedFetcher.remoteFetch(false);
const expResult = await stubbedFetcher.fetch(changeObject);
assert.strictEqual(expResult.runs!.length, 2);
assert.isTrue(
expResult
.runs![0].results![0].tags!.map(t => t.name)
.includes('Experimental')
);
assert.isFalse(
expResult
.runs![1].results![0].tags!.map(t => t.name)
.includes('Experimental')
);
assert.strictEqual(expResult.actions![2].name, 'Hide Additional Results');
});
test('fetch handles ResultDB failures', async () => {
const builds: Build[] = [
{
builder: {builder: 'bar'},
id: '421532',
infra: {resultdb: {hostname: 'host', invocation: 'inv'}},
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
endTime: '',
},
];
sandbox
.stub(ChecksFetcher.prototype, 'fetchDisplayedBuilds')
.returns(Promise.resolve(builds));
sandbox
.stub(ChecksFetcher.prototype, 'fetchTestVariants')
.returns(Promise.reject('ResultDB failed!'));
// A notice run should be generated.
fetcher.changeData = changeObject;
await fetcher.remoteFetch(false);
const res = await fetcher.fetch(changeObject);
assert.strictEqual(res.runs!.length, 2);
assert.strictEqual(res.runs![1].checkName, 'Notice');
assert.strictEqual(
res.runs![1].results![0].summary,
'Test results may be missing.'
);
});
test('retry failed without retriable builds', async () => {
stubFetch({
responses: [
{
searchBuilds: {
builds: [],
},
},
{
searchBuilds: {
builds: [],
},
},
{
searchBuilds: {
builds: [],
},
},
],
});
assert.deepEqual(await fetcher.retryFailedBuildsCallback(50, 3, 'repo'), {
message: 'No failed builds to retry',
});
});
test('retry failed with retriable builds', async () => {
stubFetch({
responses: [
{
searchBuilds: {
builds: [
{
id: '10',
builder: {builder: 'foo'},
status: BuildStatus.FAILURE,
},
],
},
},
{
searchBuilds: {
builds: [
{
id: '9',
builder: {builder: 'bar'},
status: BuildStatus.FAILURE,
},
{
id: '8',
builder: {builder: 'baz'},
status: BuildStatus.FAILURE,
},
],
},
},
{
searchBuilds: {
builds: [
{
id: '7',
builder: {builder: 'foo'},
status: BuildStatus.SUCCESS,
},
{
id: '6',
builder: {builder: 'baz'},
status: BuildStatus.STARTED,
},
],
},
},
],
});
const stub = sandbox.stub(
ChecksFetcher.prototype,
'scheduleBuildsCallback'
);
await fetcher.retryFailedBuildsCallback(50, 3, 'repo');
sinon.assert.calledWith(
stub,
50,
3,
'repo',
[{builder: 'bar'}],
[RETRY_FAILED_TAG]
);
});
test('run action returns an error message if failed', async () => {
const plugin = {
checks: () => {
return {announceUpdate: sandbox.stub()};
},
} as unknown as PluginApi;
const cache = new ChecksDB('checks');
const stubbedFetcher = new ChecksFetcher(
plugin,
'buildbucket.example.com',
2,
cache,
15
);
stubbedFetcher.retryEnabled = true;
const batchStub = sandbox.stub(BuildbucketV2Client.prototype, 'batch');
batchStub.onCall(0).returns(
Promise.resolve({
responses: [{error: {code: 2, message: 'bad cancel'}}],
})
);
const runAction = stubbedFetcher.createScheduleActions(
123456,
{
id: '987',
builder: {builder: 'foo'} as Builder,
endTime: '2017-12-15T01:30:15.05Z',
status: BuildStatus.FAILURE,
createTime: '',
startTime: '',
},
'project',
1,
[
{
status: BuildStatus.FAILURE,
endTime: '2017-12-15T01:30:15.05Z',
} as unknown as Build,
]
)[0].callback;
let res = await runAction(123456, 0, undefined, undefined, undefined, '');
assert.strictEqual(
res!.message,
'1 of 1 cancel requests failed. See console logs.'
);
batchStub.onCall(1).returns(Promise.resolve({responses: []}));
batchStub.onCall(2).returns(
Promise.resolve({
responses: [{error: {code: 2, message: 'bad schedule'}}],
})
);
res = await runAction(123456, 0, undefined, undefined, undefined, '');
assert.strictEqual(
res!.message,
'1 of 1 schedule requests failed. See console logs.'
);
});
test('chooseTryjobsCallback opens a tryjob picker', async () => {
// TODO(gavinmak): Rework test and move to cr-tryjobs-picker_test.html.
stubFetch({responses: [{searchBuilds: {}}]});
sandbox
.stub(ChecksFetcher.prototype, 'fetchTestVariants')
.returns(Promise.resolve([]));
const element = sandbox.mock({
buildbucketHost: 'oldhost',
pluginConfig: {config: 'old'},
change: {_number: 678, project: 'old/project'},
revision: {_number: 9},
}) as any;
const picker = {
close: sandbox.stub(),
_getElement() {
return {querySelector: sandbox.stub().returns(element)};
},
};
const config = {
buckets: ['foo.bar.baz'],
hideRetryButton: false,
};
const plugin = {
popup: sandbox.stub().returns(picker),
restApi: () => {
return {get: sandbox.stub().returns(config)};
},
getPluginName: () => 'buildbucket',
checks: () => {
return {announceUpdate: sandbox.stub()};
},
} as unknown as PluginApi;
const cache = new ChecksDB('checks');
const fetcherWithPlugin = new ChecksFetcher(
plugin,
'newhost',
2,
cache,
15
);
fetcherWithPlugin.changeData = changeObject;
await fetcherWithPlugin.remoteFetch(false);
const result = await fetcherWithPlugin.fetch(changeObject);
assert.strictEqual(result.actions![0].name, 'Choose Tryjobs');
await result.actions![0].callback(123456);
sinon.assert.calledOnce(plugin.popup as any);
assert.strictEqual(element.buildbucketHost, 'newhost');
assert.deepEqual(element.pluginConfig, config);
assert.deepEqual(element.change._number, 123456);
assert.deepEqual(element.change.project, 'foo/bar');
assert.deepEqual(element.revision, {_number: 1});
});
test('choose tryjobs is hidden if buckets are empty', async () => {
stubFetch({responses: [{searchBuilds: {}}]});
const plugin = {
restApi: () => {
return {get: sandbox.stub().returns({buckets: []})};
},
getPluginName: () => 'buildbucket',
checks: () => {
return {announceUpdate: sandbox.stub()};
},
} as unknown as PluginApi;
const cache = new ChecksDB('checks');
const fetcherWithPlugin = new ChecksFetcher(
plugin,
'newhost',
2,
cache,
15
);
fetcherWithPlugin.changeData = changeObject;
await fetcherWithPlugin.remoteFetch(false);
const result = await fetcherWithPlugin.fetch(changeObject);
assert.isFalse(result.actions!.some(a => a.name === 'Choose Tryjobs'));
});
test('retry failed is hidden if disabled in config', async () => {
stubFetch({
responses: [
{
searchBuilds: {
builds: [
{
id: '10',
builder: {builder: 'foo'},
status: BuildStatus.FAILURE,
},
],
},
},
],
});
const plugin = {
restApi: () => {
return {get: sandbox.stub().returns({hideRetryButton: true})};
},
getPluginName: () => 'buildbucket',
checks: () => {
return {announceUpdate: sandbox.stub()};
},
} as unknown as PluginApi;
const cache = new ChecksDB('checks');
const fetcherWithPlugin = new ChecksFetcher(
plugin,
'newhost',
2,
cache,
15
);
fetcherWithPlugin.changeData = changeObject;
await fetcherWithPlugin.remoteFetch(false);
const result = await fetcherWithPlugin.fetch(changeObject);
assert.isFalse(result.actions!.some(a => a.name === 'Retry Failed Builds'));
assert.strictEqual(result.runs![0].actions!.length, 1);
assert.strictEqual(result.runs![0].actions![0].name, 'Copy Name');
});
test('show additional results and give feedback actions can be primary', async () => {
stubFetch({responses: [{searchBuilds: {}}]});
const plugin = {
restApi: () => {
return {
get: sandbox.stub().returns({
buckets: [],
hideRetryButton: true,
}),
};
},
getPluginName: () => 'buildbucket',
checks: () => {
return {announceUpdate: sandbox.stub()};
},
} as unknown as PluginApi;
const cache = new ChecksDB('checks');
const fetcherWithPlugin = new ChecksFetcher(
plugin,
'newhost',
2,
cache,
15
);
fetcherWithPlugin.changeData = changeObject;
await fetcherWithPlugin.remoteFetch(false);
const result = await fetcherWithPlugin.fetch(changeObject);
assert.strictEqual(result.actions![0].name, 'Show Additional Results');
assert.isTrue(result.actions![0].primary);
assert.strictEqual(result.actions![1].name, 'Give Feedback');
assert.isTrue(result.actions![1].primary);
});
test('fetch creates dummy run for undefined builds', async () => {
// A notice run should be generated.
fetcher.changeData = changeObject;
fetcher.fetchBuildsStarted = true;
let res = await fetcher.fetch(changeObject);
assert.strictEqual(res.runs!.length, 1);
assert.strictEqual(res.runs![0].checkName, 'Fetching results...');
assert.strictEqual(res.runs![0].results![0].summary, 'Fetching results...');
await fetcher.cache.storeBuilds(
[],
changeObject.repo,
changeObject.changeNumber,
changeObject.patchsetNumber,
false
);
res = await fetcher.fetch(changeObject);
assert.strictEqual(res.runs!.length, 0);
});
test('fetch aborts pending fetch requests', async () => {
sandbox.stub(window, 'fetch').returns(
new Promise((resolve, _) => {
const wait = setTimeout(() => {
clearTimeout(wait);
resolve('foo' as unknown as Response);
}, 9999999);
})
);
// First set of fetches should not be aborted.
const controllerKey = fetcher.getChangeDataId(changeObject);
assert.strictEqual(fetcher.controllers.get(controllerKey), undefined);
fetcher.changeData = changeObject;
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
const firstController = fetcher.controllers.get(controllerKey);
assert.strictEqual(firstController!.signal.aborted, false);
// Following fetches should be aborted.
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
const secondController = fetcher.controllers.get(controllerKey);
assert.notStrictEqual(firstController, secondController);
assert.strictEqual(firstController!.signal.aborted, true);
assert.strictEqual(secondController!.signal.aborted, false);
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
assert.strictEqual(secondController!.signal.aborted, true);
});
test('fetch aborts based on repo, change, and patchset', async () => {
const firstChangeObject: ChangeData = {
changeNumber: 123456,
patchsetNumber: 1,
repo: 'foo/bar',
changeInfo: {
status: ChangeStatus.NEW,
revisions: {
1: {
kind: RevisionKind.REWORK,
_number: 1 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
},
id: '' as ChangeInfoId,
project: '' as RepoName,
branch: '' as BranchName,
change_id: '' as ChangeId,
subject: '',
created: '' as Timestamp,
updated: '' as Timestamp,
insertions: 0,
deletions: 0,
_number: 0 as NumericChangeId,
owner: '' as AccountInfo,
reviewers: {},
current_revision_number: 1 as PatchSetNumber,
},
patchsetSha: '',
};
const secondChangeObject: ChangeData = {
changeNumber: 123456,
patchsetNumber: 2,
repo: 'foo/bar',
changeInfo: {
status: ChangeStatus.NEW,
revisions: {
1: {
kind: RevisionKind.REWORK,
_number: 1 as RevisionPatchSetNum,
created: '' as Timestamp,
uploader: '' as AccountInfo,
ref: '' as GitRef,
},
},
id: '' as ChangeInfoId,
project: '' as RepoName,
branch: '' as BranchName,
change_id: '' as ChangeId,
subject: '',
created: '' as Timestamp,
updated: '' as Timestamp,
insertions: 0,
deletions: 0,
_number: 0 as NumericChangeId,
owner: '' as AccountInfo,
reviewers: {},
current_revision_number: 1 as PatchSetNumber,
},
patchsetSha: '',
};
// Fetch from firstChangeObject.
const firstControllerKey = fetcher.getChangeDataId(firstChangeObject);
assert.strictEqual(fetcher.controllers.get(firstControllerKey), undefined);
fetcher.changeData = firstChangeObject;
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
const firstController = fetcher.controllers.get(firstControllerKey);
assert.strictEqual(firstController!.signal.aborted, false);
// A fetch from secondChangeObject should not abort firstController.
fetcher.changeData = secondChangeObject;
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
const secondControllerKey = fetcher.getChangeDataId(secondChangeObject);
const secondController = fetcher.controllers.get(secondControllerKey);
assert.notStrictEqual(firstController, secondController);
assert.strictEqual(firstController!.signal.aborted, false);
assert.strictEqual(secondController!.signal.aborted, false);
// Aborting secondChangeObject should not abort firstController.
fetcher.remoteFetch(false);
await new Promise(r => setTimeout(r, 100));
assert.strictEqual(firstController!.signal.aborted, false);
assert.strictEqual(secondController!.signal.aborted, true);
});
test('copyNameAction warns on writeText error', async () => {
sandbox
.stub(navigator.clipboard, 'writeText')
.returns(Promise.reject('no permissions!'));
const action = fetcher.copyNameAction('foobar');
const res = await action.callback(
1,
1,
undefined,
undefined,
undefined,
'actionName'
);
assert.strictEqual(
res!.message,
'Failed to copy "foobar" to the clipboard: no permissions!'
);
});
});