blob: fdaacd2ea16b41594ea401d0fbbeecda0d073240 [file] [log] [blame]
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as sinon from 'sinon';
import '../../test/common-test-setup';
import './checks-model';
import {
CheckResult,
ChecksModel,
ChecksPatchset,
ChecksProviderState,
RunResult,
collectRunResults,
} from './checks-model';
import {
Action,
Category,
CheckRun,
ChecksApiConfig,
ChecksProvider,
ResponseCode,
RunStatus,
} from '../../api/checks';
import {getAppContext} from '../../services/app-context';
import {
createCheckResult,
createParsedChange,
createRun,
} from '../../test/test-data-generators';
import {waitUntil, waitUntilCalled} from '../../test/test-utils';
import {ParsedChangeInfo} from '../../types/types';
import {
changeModelToken,
updateRevisionsWithCommitShas,
} from '../change/change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {changeViewModelToken} from '../views/change';
import {NumericChangeId, PatchSetNumber} from '../../api/rest-api';
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {deepEqual} from '../../utils/deep-util';
const PLUGIN_NAME = 'test-plugin';
const RUNS: CheckRun[] = [
{
checkName: 'MacCheck',
change: 123,
patchset: 1,
attempt: 1,
status: RunStatus.COMPLETED,
results: [
{
externalId: 'id-314',
category: Category.WARNING,
summary: 'Meddle cheddle check and you are weg.',
},
],
},
];
const CONFIG_POLLING_5S: ChecksApiConfig = {
fetchPollingIntervalSeconds: 5,
};
const CONFIG_POLLING_NONE: ChecksApiConfig = {
fetchPollingIntervalSeconds: 0,
};
function createProvider(): ChecksProvider {
return {
fetch: () =>
Promise.resolve({
responseCode: ResponseCode.OK,
runs: [],
}),
};
}
suite('checks-model tests', () => {
let model: ChecksModel;
let current: ChecksProviderState;
setup(() => {
model = new ChecksModel(
testResolver(changeViewModelToken),
testResolver(changeModelToken),
getAppContext().reportingService,
testResolver(pluginLoaderToken).pluginsModel
);
model.checksLatest$.subscribe(c => (current = c[PLUGIN_NAME]));
});
teardown(() => {
model.finalize();
});
test('register and fetch', async () => {
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
model.register({
pluginName: 'test-plugin',
provider,
config: CONFIG_POLLING_NONE,
});
await waitUntil(() => change === undefined);
const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => deepEqual(change, testChange));
await waitUntilCalled(fetchSpy, 'fetch');
assert.equal(
model.latestPatchNum,
testChange!.revisions[testChange!.current_revision]
._number as PatchSetNumber
);
assert.equal(model.changeNum, testChange!._number);
});
test('fetch throttle', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
model.register({
pluginName: 'test-plugin',
provider,
config: CONFIG_POLLING_NONE,
});
await waitUntil(() => change === undefined);
const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => deepEqual(change, testChange));
model.reload('test-plugin');
model.reload('test-plugin');
model.reload('test-plugin');
// Does not emit at 'leading' of throttle interval,
// because fetch() is not called when change is undefined.
assert.equal(fetchSpy.callCount, 0);
// 600 ms is greater than the 500 ms throttle time.
clock.tick(600);
// emits at 'trailing' of throttle interval
assert.equal(fetchSpy.callCount, 1);
model.reload('test-plugin');
model.reload('test-plugin');
model.reload('test-plugin');
model.reload('test-plugin');
// emits at 'leading' of throttle interval
assert.equal(fetchSpy.callCount, 2);
// 600 ms is greater than the 500 ms throttle time.
clock.tick(600);
// emits at 'trailing' of throttle interval
assert.equal(fetchSpy.callCount, 3);
});
test('triggerAction', async () => {
model.changeNum = 314 as NumericChangeId;
model.latestPatchNum = 13 as PatchSetNumber;
const action: Action = {
name: 'test action',
callback: () => undefined,
};
const spy = sinon.spy(action, 'callback');
model.triggerAction(action, undefined, 'none');
assert.isTrue(spy.calledOnce);
assert.equal(spy.lastCall.args[0], 314);
assert.equal(spy.lastCall.args[1], 13);
});
test('model.updateStateSetProvider', () => {
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
assert.deepEqual(current, {
pluginName: PLUGIN_NAME,
loading: false,
firstTimeLoad: true,
runs: [],
actions: [],
links: [],
});
});
test('loading and first time load', () => {
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
assert.isFalse(current.loading);
assert.isTrue(current.firstTimeLoad);
model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
assert.isTrue(current.loading);
assert.isTrue(current.firstTimeLoad);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.LATEST
);
assert.isFalse(current.loading);
assert.isFalse(current.firstTimeLoad);
model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
assert.isTrue(current.loading);
assert.isFalse(current.firstTimeLoad);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.LATEST
);
assert.isFalse(current.loading);
assert.isFalse(current.firstTimeLoad);
});
test('model.updateStateSetResults', () => {
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.LATEST
);
assert.lengthOf(current.runs, 1);
assert.lengthOf(current.runs[0].results!, 1);
});
test('model.updateStateSetResults ignore empty name or status', () => {
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
model.updateStateSetResults(
PLUGIN_NAME,
[
{
checkName: 'test-check-name',
status: RunStatus.COMPLETED,
},
// Will be ignored, because the checkName is empty.
{
checkName: undefined as unknown as string,
status: RunStatus.COMPLETED,
},
// Will be ignored, because the status is empty.
{
checkName: 'test-check-name',
status: undefined as unknown as RunStatus,
},
],
[],
[],
undefined,
ChecksPatchset.LATEST
);
// 2 out of 3 runs are ignored.
assert.lengthOf(current.runs, 1);
});
test('model.updateStateUpdateResult', () => {
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.LATEST
);
assert.equal(
current.runs[0].results![0].summary,
RUNS[0]!.results![0].summary
);
const result = RUNS[0].results![0];
const updatedResult = {...result, summary: 'new'};
model.updateStateUpdateResult(
PLUGIN_NAME,
RUNS[0],
updatedResult,
ChecksPatchset.LATEST
);
assert.lengthOf(current.runs, 1);
assert.lengthOf(current.runs[0].results!, 1);
assert.equal(current.runs[0].results![0].summary, 'new');
});
test('allResults$', async () => {
let results: CheckResult[] | undefined = undefined;
model.allResults$.subscribe(allResults => (results = allResults));
testResolver(changeViewModelToken).updateState({
checksPatchset: 1 as PatchSetNumber,
});
testResolver(changeModelToken).updateStateChange(createParsedChange());
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.SELECTED);
model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
assert.equal(results!.length, 0);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.LATEST
);
assert.equal(results!.length, 1);
model.updateStateSetResults(
PLUGIN_NAME,
RUNS,
[],
[],
undefined,
ChecksPatchset.SELECTED
);
assert.equal(results!.length, 1);
});
test('polls for changes', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
model.register({
pluginName: 'test-plugin',
provider,
config: CONFIG_POLLING_5S,
});
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => deepEqual(change, testChange));
clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
const pollCount = fetchSpy.callCount;
// polling should continue while we wait
clock.tick(CONFIG_POLLING_5S.fetchPollingIntervalSeconds * 1000 * 2);
assert.isTrue(fetchSpy.callCount > pollCount);
});
test('does not poll when config specifies 0 seconds', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
model.register({
pluginName: 'test-plugin',
provider,
config: CONFIG_POLLING_NONE,
});
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => deepEqual(change, testChange));
clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
clock.tick(1);
const pollCount = fetchSpy.callCount;
// polling should not happen
clock.tick(60 * 1000);
assert.equal(fetchSpy.callCount, pollCount);
});
test('collectRunResults does not incur quadratic size increase', async () => {
const results: CheckResult[] = [];
for (let i = 0; i < 100; i++) {
results.push({
...createCheckResult({
message: 'some message',
}),
});
}
const run = createRun({results});
let collected: RunResult[] = [];
collected = collectRunResults(collected, {
runs: [run],
} as ChecksProviderState);
const collectedString = JSON.stringify(collected);
// If the `results` property would not be removed from every check run, then
// this combined string would be >1MB in size.
assert.isAtMost(collectedString.length, 50000);
});
});