Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Optimize API test order #88654

Merged
merged 17 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@
"base64-js": "^1.3.1",
"base64url": "^3.0.1",
"broadcast-channel": "^3.0.3",
"callsites": "^3.1.0",
"chai": "3.5.0",
"chance": "1.0.18",
"chromedriver": "^87.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
import path from 'path';
import prettier from 'prettier';
import babelTraverse from '@babel/traverse';
import { flatten, once } from 'lodash';
import { once } from 'lodash';
import callsites from 'callsites';
import { Lifecycle } from '../lifecycle';
import { Test, Suite } from '../../fake_mocha_types';
import { Test } from '../../fake_mocha_types';

type ISnapshotState = InstanceType<typeof SnapshotState>;

Expand All @@ -28,40 +29,17 @@ interface SnapshotContext {
currentTestName: string;
}

let testContext: {
file: string;
snapshotTitle: string;
snapshotContext: SnapshotContext;
} | null = null;

let registered: boolean = false;

function getSnapshotMeta(currentTest: Test) {
// Make sure snapshot title is unique per-file, rather than entire
// suite. This allows reuse of tests, for instance to compare
// results for different configurations.

const titles = [currentTest.title];
const file = currentTest.file;

let test: Suite | undefined = currentTest?.parent;

while (test && test.file === file) {
titles.push(test.title);
test = test.parent;
}

const snapshotTitle = titles.reverse().join(' ');

if (!file || !snapshotTitle) {
throw new Error(`file or snapshotTitle not available in Mocha test context`);
}

return {
file,
snapshotTitle,
};
}
const globalState: {
updateSnapshot: SnapshotUpdateState;
registered: boolean;
currentTest: Test | null;
snapshots: Array<{ tests: Test[]; file: string; snapshotState: ISnapshotState }>;
} = {
updateSnapshot: 'none',
registered: false,
currentTest: null,
snapshots: [],
};

const modifyStackTracePrepareOnce = once(() => {
const originalPrepareStackTrace = Error.prepareStackTrace;
Expand All @@ -72,7 +50,7 @@ const modifyStackTracePrepareOnce = once(() => {

Error.prepareStackTrace = (error, structuredStackTrace) => {
let filteredStrackTrace: NodeJS.CallSite[] = structuredStackTrace;
if (registered) {
if (globalState.registered) {
filteredStrackTrace = filteredStrackTrace.filter((callSite) => {
// check for both compiled and uncompiled files
return !callSite.getFileName()?.match(/decorate_snapshot_ui\.(js|ts)/);
Expand All @@ -94,21 +72,14 @@ export function decorateSnapshotUi({
updateSnapshots: boolean;
isCi: boolean;
}) {
let snapshotStatesByFilePath: Record<
string,
{ snapshotState: ISnapshotState; testsInFile: Test[] }
> = {};

registered = true;

let updateSnapshot: SnapshotUpdateState;
globalState.registered = true;

if (isCi) {
// make sure snapshots that have not been committed
// are not written to file on CI, passing the test
updateSnapshot = 'none';
globalState.updateSnapshot = 'none';
} else {
updateSnapshot = updateSnapshots ? 'all' : 'new';
globalState.updateSnapshot = updateSnapshots ? 'all' : 'new';
}

modifyStackTracePrepareOnce();
Expand All @@ -125,21 +96,8 @@ export function decorateSnapshotUi({
// @ts-expect-error
global.expectSnapshot = expectSnapshot;

lifecycle.beforeEachTest.add((currentTest: Test) => {
const { file, snapshotTitle } = getSnapshotMeta(currentTest);

if (!snapshotStatesByFilePath[file]) {
snapshotStatesByFilePath[file] = getSnapshotState(file, currentTest, updateSnapshot);
}

testContext = {
file,
snapshotTitle,
snapshotContext: {
snapshotState: snapshotStatesByFilePath[file].snapshotState,
currentTestName: snapshotTitle,
},
};
lifecycle.beforeEachTest.add((test: Test) => {
globalState.currentTest = test;
});

lifecycle.afterTestSuite.add(function (testSuite) {
Expand All @@ -150,19 +108,18 @@ export function decorateSnapshotUi({

const unused: string[] = [];

Object.keys(snapshotStatesByFilePath).forEach((file) => {
const { snapshotState, testsInFile } = snapshotStatesByFilePath[file];

testsInFile.forEach((test) => {
const snapshotMeta = getSnapshotMeta(test);
globalState.snapshots.forEach((snapshot) => {
const { tests, snapshotState } = snapshot;
tests.forEach((test) => {
const title = test.fullTitle();
// If test is failed or skipped, mark snapshots as used. Otherwise,
// running a test in isolation will generate false positives.
if (!test.isPassed()) {
snapshotState.markSnapshotsAsCheckedForTest(snapshotMeta.snapshotTitle);
snapshotState.markSnapshotsAsCheckedForTest(title);
}
});

if (!updateSnapshots) {
if (!globalState.updateSnapshot) {
unused.push(...snapshotState.getUncheckedKeys());
} else {
snapshotState.removeUncheckedKeys();
Expand All @@ -179,28 +136,14 @@ export function decorateSnapshotUi({
);
}

snapshotStatesByFilePath = {};
globalState.snapshots.length = 0;
});
}

function recursivelyGetTestsFromSuite(suite: Suite): Test[] {
return suite.tests.concat(flatten(suite.suites.map((s) => recursivelyGetTestsFromSuite(s))));
}

function getSnapshotState(file: string, test: Test, updateSnapshot: SnapshotUpdateState) {
function getSnapshotState(file: string, updateSnapshot: SnapshotUpdateState) {
const dirname = path.dirname(file);
const filename = path.basename(file);

let parent: Suite | undefined = test.parent;

while (parent && parent.parent?.file === file) {
parent = parent.parent;
}

if (!parent) {
throw new Error('Top-level suite not found');
}

const snapshotState = new SnapshotState(
path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')),
{
Expand All @@ -211,24 +154,54 @@ function getSnapshotState(file: string, test: Test, updateSnapshot: SnapshotUpda
}
);

return { snapshotState, testsInFile: recursivelyGetTestsFromSuite(parent) };
return snapshotState;
}

export function expectSnapshot(received: any) {
if (!registered) {
if (!globalState.registered) {
throw new Error(
'Mocha hooks were not registered before expectSnapshot was used. Call `registerMochaHooksForSnapshots` in your top-level describe().'
);
}

if (!testContext) {
throw new Error('A current Mocha context is needed to match snapshots');
if (!globalState.currentTest) {
throw new Error('expectSnapshot can only be called inside of an it()');
}

const [, fileOfTest] = callsites().map((site) => site.getFileName());

if (!fileOfTest) {
throw new Error("Couldn't infer a filename for the current test");
}

let snapshot = globalState.snapshots.find(({ file }) => file === fileOfTest);

if (!snapshot) {
snapshot = {
file: fileOfTest,
tests: [],
snapshotState: getSnapshotState(fileOfTest, globalState.updateSnapshot),
};
globalState.snapshots.unshift(snapshot!);
}

if (!snapshot) {
throw new Error('Snapshot is undefined');
}

if (!snapshot.tests.includes(globalState.currentTest)) {
snapshot.tests.push(globalState.currentTest);
}

const context: SnapshotContext = {
snapshotState: snapshot.snapshotState,
currentTestName: globalState.currentTest.fullTitle(),
};

return {
toMatch: expectToMatchSnapshot.bind(null, testContext.snapshotContext, received),
toMatch: expectToMatchSnapshot.bind(null, context, received),
// use bind to support optional 3rd argument (actual)
toMatchInline: expectToMatchInlineSnapshot.bind(null, testContext.snapshotContext, received),
toMatchInline: expectToMatchInlineSnapshot.bind(null, context, received),
};
}

Expand Down
8 changes: 2 additions & 6 deletions x-pack/test/apm_api_integration/basic/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createTestConfig } from '../common/config';
import { configs } from '../configs';

export default createTestConfig({
license: 'basic',
name: 'X-Pack APM API integration tests (basic)',
testFiles: [require.resolve('./tests')],
});
export default configs.basic;
124 changes: 0 additions & 124 deletions x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts

This file was deleted.

Loading