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

[ML] Add tests for anomaly embeddables migrations #116520

Merged
merged 8 commits into from
Nov 3, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('can open job selection flyout', async () => {
await PageObjects.dashboard.clickCreateDashboardPrompt();
await ml.dashboardEmbeddables.assertDashboardIsEmpty();
await ml.dashboardEmbeddables.openJobSelectionFlyout();
await ml.dashboardEmbeddables.openAnomalyJobSelectionFlyout('ml_anomaly_charts');
await a11y.testAppSnapshot();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,16 @@
*/

import { FtrProviderContext } from '../../../ftr_provider_context';
import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';

// @ts-expect-error not full interface
const JOB_CONFIG: Job = {
job_id: `fq_multi_1_ae`,
description:
'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span',
groups: ['farequote', 'automated', 'multi-metric'],
analysis_config: {
bucket_span: '1h',
influencers: ['airline'],
detectors: [
{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'min', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'max', field_name: 'responsetime', partition_field_name: 'airline' },
],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '20mb' },
model_plot_config: { enabled: true },
};

// @ts-expect-error not full interface
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_multi_1_ae',
indices: ['ft_farequote'],
job_id: 'fq_multi_1_ae',
query: { bool: { must: [{ match_all: {} }] } },
};
import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants';

const testDataList = [
{
type: 'testData',
suiteSuffix: 'with multi metric job',
panelTitle: `ML anomaly charts for ${JOB_CONFIG.job_id}`,
jobConfig: JOB_CONFIG,
datafeedConfig: DATAFEED_CONFIG,
dashboardTitle: `ML anomaly charts for fq_multi_1_ae ${Date.now()}`,
expected: {
influencers: [
{
Expand All @@ -59,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const ml = getService('ml');
const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']);

describe('anomaly charts', function () {
describe('anomaly charts in dashboard', function () {
this.tags(['mlqa']);

before(async () => {
Expand All @@ -69,6 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await ml.securityUI.loginAsMlPowerUser();
});

after(async () => {
await ml.api.cleanMlIndices();
});

for (const testData of testDataList) {
describe(testData.suiteSuffix, function () {
before(async () => {
Expand All @@ -79,14 +57,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('dashboard');
});

after(async () => {
await ml.api.cleanMlIndices();
});

it('can open job selection flyout', async () => {
await PageObjects.dashboard.clickCreateDashboardPrompt();
await PageObjects.dashboard.clickNewDashboard();
await ml.dashboardEmbeddables.assertDashboardIsEmpty();
await ml.dashboardEmbeddables.openJobSelectionFlyout();
await ml.dashboardEmbeddables.openAnomalyJobSelectionFlyout(
ML_EMBEDDABLE_TYPES.ANOMALY_CHARTS
);
});

it('can select jobs', async () => {
Expand All @@ -109,6 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.pauseAutoRefresh();
await ml.dashboardEmbeddables.assertAnomalyChartsSeverityThresholdControlExists();
await ml.dashboardEmbeddables.assertAnomalyChartsExists();
await PageObjects.dashboard.saveDashboard(testData.dashboardTitle);
});
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { FtrProviderContext } from '../../../ftr_provider_context';
import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants';

const testDataList = [
{
type: ML_EMBEDDABLE_TYPES.ANOMALY_SWIMLANE,
panelTitle: 'ML anomaly swim lane',
dashboardSavedObject: {
attributes: {
title: `7.15.2 ML anomaly swimlane dashboard ${Date.now()}`,
description: '',
panelsJSON: `[{"version":"7.15.2","type":"ml_anomaly_swimlane","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"c177ed0a-dea0-40f8-8980-cfb0c6bc13a8"},"panelIndex":"c177ed0a-dea0-40f8-8980-cfb0c6bc13a8","embeddableConfig":{"jobIds":["fq_multi_1_ae"],"swimlaneType":"viewBy","viewBy":"airline","enhancements":{}},"title":"ML anomaly swim lane"}]`,
optionsJSON: '{"useMargins":true,"syncColors":false,"hidePanelTitles":false}',
timeRestore: true,
timeTo: '2016-02-11T00:00:00.000Z',
timeFrom: '2016-02-07T00:00:00.000Z',
refreshInterval: {
pause: true,
value: 0,
},
kibanaSavedObjectMeta: {
searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}',
},
},
coreMigrationVersion: '7.15.2',
},
},
{
type: ML_EMBEDDABLE_TYPES.ANOMALY_CHARTS,
panelTitle: 'ML anomaly charts',
dashboardSavedObject: {
attributes: {
title: `7.15.2 ML anomaly charts dashboard ${Date.now()}`,
description: '',
panelsJSON:
'[{"version":"7.15.2","type":"ml_anomaly_charts","gridData":{"x":0,"y":0,"w":38,"h":21,"i":"1155890b-c19c-4d98-8153-50e6434612f1"},"panelIndex":"1155890b-c19c-4d98-8153-50e6434612f1","embeddableConfig":{"jobIds":["fq_multi_1_ae"],"maxSeriesToPlot":6,"severityThreshold":0,"enhancements":{}},"title":"ML anomaly charts"}]',
optionsJSON: '{"useMargins":true,"syncColors":false,"hidePanelTitles":false}',
timeRestore: true,
timeTo: '2016-02-11T00:00:00.000Z',
timeFrom: '2016-02-07T00:00:00.000Z',
refreshInterval: {
pause: true,
value: 0,
},
kibanaSavedObjectMeta: {
searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}',
},
},
coreMigrationVersion: '7.15.2',
},
},
];

export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']);

describe('anomaly embeddables migration in Dashboard', function () {
this.tags(['mlqa']);

before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();

await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
await PageObjects.common.navigateToApp('dashboard');
});

after(async () => {
await ml.api.cleanMlIndices();
});

for (const testData of testDataList) {
const { dashboardSavedObject, panelTitle, type } = testData;
describe(`loads saved dashboard from version ${dashboardSavedObject.coreMigrationVersion}`, function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same description is used for the test below. I think it would be good to include the panelTitle in the description here too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - please let's not repeat this long title and have a shorter one for either the suite or the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated here 7e51b01

before(async () => {
await ml.testResources.createDashboardSavedObject(
dashboardSavedObject.attributes.title,
dashboardSavedObject,
true
);
await PageObjects.common.navigateToApp('dashboard');
});

after(async () => {
await ml.testResources.deleteDashboardByTitle(dashboardSavedObject.attributes.title);
});

it(`loads saved dashboard from version ${dashboardSavedObject.coreMigrationVersion}`, async () => {
await PageObjects.dashboard.loadSavedDashboard(dashboardSavedObject.attributes.title);

await ml.dashboardEmbeddables.assertDashboardPanelExists(panelTitle);
await PageObjects.timePicker.setAbsoluteRange(
'Feb 7, 2015 @ 00:00:00.000',
'Feb 11, 2016 @ 00:00:00.000'
);
await PageObjects.timePicker.pauseAutoRefresh();
pheyos marked this conversation as resolved.
Show resolved Hide resolved

if (type === ML_EMBEDDABLE_TYPES.ANOMALY_CHARTS) {
await ml.dashboardEmbeddables.assertAnomalyChartsSeverityThresholdControlExists();
await ml.dashboardEmbeddables.assertAnomalyChartsExists();
}

if (type === ML_EMBEDDABLE_TYPES.ANOMALY_SWIMLANE) {
await ml.dashboardEmbeddables.assertAnomalySwimlaneExists();
}
});
});
}
});
}
41 changes: 41 additions & 0 deletions x-pack/test/functional/apps/ml/embeddables/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Datafeed, Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';

// @ts-expect-error not full interface
export const JOB_CONFIG: Job = {
job_id: `fq_multi_1_ae`,
description:
'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span',
groups: ['farequote', 'automated', 'multi-metric'],
analysis_config: {
bucket_span: '1h',
influencers: ['airline'],
detectors: [
{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'min', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'max', field_name: 'responsetime', partition_field_name: 'airline' },
],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '20mb' },
model_plot_config: { enabled: true },
};

// @ts-expect-error not full interface
export const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_multi_1_ae',
indices: ['ft_farequote'],
job_id: 'fq_multi_1_ae',
query: { bool: { must: [{ match_all: {} }] } },
};

export const ML_EMBEDDABLE_TYPES = {
ANOMALY_SWIMLANE: 'ml_anomaly_swimlane',
ANOMALY_CHARTS: 'ml_anomaly_charts',
} as const;
1 change: 1 addition & 0 deletions x-pack/test/functional/apps/ml/embeddables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('embeddables', function () {
this.tags(['skipFirefox']);
loadTestFile(require.resolve('./anomaly_charts_dashboard_embeddables'));
loadTestFile(require.resolve('./anomaly_embeddables_migration'));
});
}
12 changes: 10 additions & 2 deletions x-pack/test/functional/services/ml/dashboard_embeddables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,21 @@ export function MachineLearningDashboardEmbeddablesProvider(
});
},

async openJobSelectionFlyout() {
async assertAnomalySwimlaneExists() {
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail(`mlAnomalySwimlaneEmbeddableWrapper`);
});
},

async openAnomalyJobSelectionFlyout(
mlEmbeddableType: 'ml_anomaly_swimlane' | 'ml_anomaly_charts'
) {
await retry.tryForTime(60 * 1000, async () => {
await dashboardAddPanel.clickEditorMenuButton();
await testSubjects.existOrFail('dashboardEditorContextMenu', { timeout: 2000 });

await dashboardAddPanel.clickEmbeddableFactoryGroupButton('ml');
await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts');
await dashboardAddPanel.clickAddNewEmbeddableLink(mlEmbeddableType);

await mlDashboardJobSelectionTable.assertJobSelectionTableExists();
});
Expand Down
17 changes: 17 additions & 0 deletions x-pack/test/functional/services/ml/test_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider
return createResponse.id;
},

async createDashboardSavedObject(
pheyos marked this conversation as resolved.
Show resolved Hide resolved
title: string,
body: object,
override = false
): Promise<string> {
log.debug(`Creating dashboard with title '${title}'`);

const createResponse = await supertest
.post(`/api/saved_objects/${SavedObjectType.DASHBOARD}?overwrite=${override}`)
.set(COMMON_REQUEST_HEADERS)
.send(body)
.then((res: any) => res.body);

log.debug(` > Created with id '${createResponse.id}'`);
return createResponse.id;
},

async createIndexPatternIfNeeded(title: string, timeFieldName?: string): Promise<string> {
const indexPatternId = await this.getIndexPatternId(title);
if (indexPatternId !== undefined) {
Expand Down