| 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!' |
| ); |
| }); |
| }); |