Skip to content

Commit

Permalink
[ML] Add multi metric job wizard test (#45279) (#45394)
Browse files Browse the repository at this point in the history
This PR adds functional UI tests to create a machine learning job using the multi metric wizard.
  • Loading branch information
pheyos committed Sep 11, 2019
1 parent 62ca0c8 commit 395cf6d
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const AnomalyChart: FC<Props> = ({
const data = chartType === CHART_TYPE.SCATTER ? flattenData(chartData) : chartData;
const xDomain = getXRange(data);
return (
<div style={{ width, height }} data-test-subj="mlAnomalyChart">
<div style={{ width, height }} data-test-subj={`mlAnomalyChart ${CHART_TYPE[chartType]}`}>
<LoadingWrapper height={height} hasData={data.length > 0} loading={loading}>
<Chart>
<Settings xDomain={xDomain} tooltip={TooltipType.None} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const DetectorTitle: FC<DetectorTitleProps> = ({
return (
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween">
<EuiFlexItem>
<span style={{ fontSize: 'small' }}>{getTitle(agg, field, splitField)}</span>
<span style={{ fontSize: 'small' }} data-test-subj="detectorTitle">
{getTitle(agg, field, splitField)}
</span>
</EuiFlexItem>

{children !== false && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const ChartGrid: FC<ChartGridProps> = ({
>
<EuiFlexGrid columns={chartSettings.cols}>
{aggFieldPairList.map((af, i) => (
<EuiFlexItem key={i}>
<EuiFlexItem key={i} data-test-subj={`detector ${i}`}>
<Fragment>
<DetectorTitle
index={i}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export const MultiMetricSettings: FC<Props> = ({ setIsValid }) => {
return (
<Fragment>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
<EuiFlexItem data-test-subj="mlJobWizardSplitFieldSelection">
<SplitFieldSelector />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem data-test-subj="mlJobWizardInfluencerSelection">
<Influencers />
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,13 @@ export const SplitCards: FC<Props> = memo(
};
return (
<div key={fieldName} ref={ref => storePanels(ref, marginBottom)} style={style}>
<EuiPanel paddingSize="m" style={{ paddingTop: '4px' }}>
<div style={{ fontWeight: 'bold', fontSize: 'small' }}>{fieldName}</div>
<EuiPanel paddingSize="m" style={{ paddingTop: '4px' }} data-test-subj="splitCard back">
<div
style={{ fontWeight: 'bold', fontSize: 'small' }}
data-test-subj="splitCardTitle"
>
{fieldName}
</div>
</EuiPanel>
</div>
);
Expand All @@ -80,13 +85,16 @@ export const SplitCards: FC<Props> = memo(

return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexItem data-test-subj="dataSplit">
{(fieldValues.length === 0 || numberOfDetectors === 0) && <Fragment>{children}</Fragment>}
{fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && (
<Fragment>
{jobType === JOB_TYPE.MULTI_METRIC && (
<Fragment>
<div style={{ fontSize: 'small' }}>
<div
style={{ fontSize: 'small' }}
data-test-subj={`dataSplitTitle ${splitField.name}`}
>
<FormattedMessage
id="xpack.ml.newJob.wizard.pickFieldsStep.splitCards.dataSplitBy"
defaultMessage="Data split by {field}"
Expand All @@ -98,8 +106,17 @@ export const SplitCards: FC<Props> = memo(
)}

{getBackPanels()}
<EuiPanel paddingSize="m" style={{ paddingTop: '4px' }}>
<div style={{ fontWeight: 'bold', fontSize: 'small' }}>{fieldValues[0]}</div>
<EuiPanel
paddingSize="m"
style={{ paddingTop: '4px' }}
data-test-subj="splitCard front"
>
<div
style={{ fontWeight: 'bold', fontSize: 'small' }}
data-test-subj="splitCardTitle"
>
{fieldValues[0]}
</div>
<EuiHorizontalRule margin="s" />
{children}
</EuiPanel>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';

import { FtrProviderContext } from '../../ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');

const jobId = `fq_multi_1_${Date.now()}`;
const jobDescription =
'Create multi metric job based on the farequote dataset with 15m bucketspan and min/max/mean(responsetime) split by airline';
const jobGroups = ['automated', 'farequote', 'multi-metric'];
const aggAndFieldIdentifiers = ['Min(responsetime)', 'Max(responsetime)', 'Mean(responsetime)'];
const splitField = 'airline';
const bucketSpan = '15m';
const memoryLimit = '20MB';

describe('multi metric job creation', function() {
this.tags(['smoke', 'mlqa']);
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
});

after(async () => {
await esArchiver.unload('ml/farequote');
await ml.api.cleanMlIndices();
await ml.api.cleanDataframeIndices();
});

it('loads the job management page', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
});

it('loads the new job source selection page', async () => {
await ml.jobManagement.navigateToNewJobSourceSelection();
});

it('loads the job type selection page', async () => {
await ml.jobSourceSelection.selectSourceIndexPattern('farequote');
});

it('loads the single metric job wizard page', async () => {
await ml.jobTypeSelection.selectMultiMetricJob();
});

it('displays the time range step', async () => {
await ml.jobWizardCommon.assertTimeRangeSectionExists();
});

it('displays the event rate chart', async () => {
await ml.jobWizardCommon.clickUseFullDataButton();
await ml.jobWizardCommon.assertEventRateChartExists();
});

it('displays the pick fields step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertPickFieldsSectionExists();
});

it('selects detectors and displays detector previews', async () => {
for (const [index, aggAndFieldIdentifier] of aggAndFieldIdentifiers.entries()) {
await ml.jobWizardCommon.assertAggAndFieldInputExists();
await ml.jobWizardCommon.selectAggAndField(aggAndFieldIdentifier);
await ml.jobWizardCommon.assertDetectorPreviewExists(aggAndFieldIdentifier, index, 'LINE');
}
});

it('inputs the split field and displays split cards', async () => {
await ml.jobWizardCommon.assertMultiMetricSplitFieldInputExists();
await ml.jobWizardCommon.selectMultiMetricSplitField(splitField);
await ml.jobWizardCommon.assertMultiMetricSplitFieldSelection(splitField);

await ml.jobWizardCommon.assertDetectorSplitExists(splitField);
await ml.jobWizardCommon.assertDetectorSplitFrontCardTitle('AAL');
await ml.jobWizardCommon.assertDetectorSplitNumberOfBackCards(9);

await ml.jobWizardCommon.assertInfluencerSelection([splitField]);
});

it('displays the influencer field', async () => {
await ml.jobWizardCommon.assertInfluencerInputExists();
await ml.jobWizardCommon.assertInfluencerSelection([splitField]);
});

it('inputs the bucket span', async () => {
await ml.jobWizardCommon.assertBucketSpanInputExists();
await ml.jobWizardCommon.setBucketSpan(bucketSpan);
await ml.jobWizardCommon.assertBucketSpanValue(bucketSpan);
});

it('displays the job details step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertJobDetailsSectionExists();
});

it('inputs the job id', async () => {
await ml.jobWizardCommon.assertJobIdInputExists();
await ml.jobWizardCommon.setJobId(jobId);
await ml.jobWizardCommon.assertJobIdValue(jobId);
});

it('inputs the job description', async () => {
await ml.jobWizardCommon.assertJobDescriptionInputExists();
await ml.jobWizardCommon.setJobDescription(jobDescription);
await ml.jobWizardCommon.assertJobDescriptionValue(jobDescription);
});

it('inputs job groups', async () => {
await ml.jobWizardCommon.assertJobGroupInputExists();
for (const jobGroup of jobGroups) {
await ml.jobWizardCommon.addJobGroup(jobGroup);
}
await ml.jobWizardCommon.assertJobGroupSelection(jobGroups);
});

it('opens the advanced section', async () => {
await ml.jobWizardCommon.ensureAdvancedSectionOpen();
});

it('displays the model plot switch', async () => {
await ml.jobWizardCommon.assertModelPlotSwitchExists();
});

it('enables the dedicated index switch', async () => {
await ml.jobWizardCommon.assertDedicatedIndexSwitchExists();
await ml.jobWizardCommon.activateDedicatedIndexSwitch();
await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true);
});

it('inputs the model memory limit', async () => {
await ml.jobWizardCommon.assertModelMemoryLimitInputExists();
await ml.jobWizardCommon.setModelMemoryLimit(memoryLimit);
await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit);
});

it('displays the validation step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertValidationSectionExists();
});

it('displays the summary step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertSummarySectionExists();
});

it('creates the job and finishes processing', async () => {
await ml.jobWizardCommon.assertCreateJobButtonExists();
await ml.jobWizardCommon.createJobAndWaitForCompletion();
});

it('displays the created job in the job list', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();

await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId);
const rows = await ml.jobTable.parseJobTable();
expect(rows.filter(row => row.id === jobId)).to.have.length(1);
});

it('displays details for the created job in the job list', async () => {
const expectedRow = {
id: jobId,
description: jobDescription,
jobGroups,
recordCount: '86,274',
memoryStatus: 'ok',
jobState: 'closed',
datafeedState: 'stopped',
latestTimestamp: '2016-02-11 23:59:54',
};
await ml.jobTable.assertJobRowFields(jobId, expectedRow);

const expectedCounts = {
job_id: jobId,
processed_record_count: '86,274',
processed_field_count: '172,548',
input_bytes: '6.4 MB',
input_field_count: '172,548',
invalid_date_count: '0',
missing_field_count: '0',
out_of_order_timestamp_count: '0',
empty_bucket_count: '0',
sparse_bucket_count: '0',
bucket_count: '479',
earliest_record_timestamp: '2016-02-07 00:00:00',
latest_record_timestamp: '2016-02-11 23:59:54',
input_record_count: '86,274',
latest_bucket_timestamp: '2016-02-11 23:45:00',
};
const expectedModelSizeStats = {
job_id: jobId,
result_type: 'model_size_stats',
model_bytes: '1.8 MB',
model_bytes_exceeded: '0',
model_bytes_memory_limit: '20971520',
total_by_field_count: '59',
total_over_field_count: '0',
total_partition_field_count: '58',
bucket_allocation_failures_count: '0',
memory_status: 'ok',
timestamp: '2016-02-11 23:30:00',
};
await ml.jobTable.assertJobRowDetailsCounts(jobId, expectedCounts, expectedModelSizeStats);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ export default function({ getService }: FtrProviderContext) {
});

it('displays details for the created job in the job list', async () => {
const rows = await ml.jobTable.parseJobTable();
const job = rows.filter(row => row.id === jobId)[0];
expect(job).to.eql({
const expectedRow = {
id: jobId,
description: jobDescription,
jobGroups,
Expand All @@ -157,18 +155,10 @@ export default function({ getService }: FtrProviderContext) {
jobState: 'closed',
datafeedState: 'stopped',
latestTimestamp: '2016-02-11 23:56:59',
});

const countDetails = await ml.jobTable.parseJobCounts(jobId);
const counts = countDetails.counts;

// last_data_time holds a runtime timestamp and is hard to predict
// the property is only validated to be present and then removed
// so it doesn't make the counts object validation fail
expect(counts).to.have.property('last_data_time');
delete counts.last_data_time;
};
await ml.jobTable.assertJobRowFields(jobId, expectedRow);

expect(counts).to.eql({
const expectedCounts = {
job_id: jobId,
processed_record_count: '2,399',
processed_field_count: '4,798',
Expand All @@ -184,17 +174,8 @@ export default function({ getService }: FtrProviderContext) {
latest_record_timestamp: '2016-02-11 23:56:59',
input_record_count: '2,399',
latest_bucket_timestamp: '2016-02-11 23:30:00',
});

const modelSizeStats = countDetails.modelSizeStats;

// log_time holds a runtime timestamp and is hard to predict
// the property is only validated to be present and then removed
// so it doesn't make the modelSizeStats object validation fail
expect(modelSizeStats).to.have.property('log_time');
delete modelSizeStats.log_time;

expect(modelSizeStats).to.eql({
};
const expectedModelSizeStats = {
job_id: jobId,
result_type: 'model_size_stats',
model_bytes: '47.6 KB',
Expand All @@ -206,7 +187,8 @@ export default function({ getService }: FtrProviderContext) {
bucket_allocation_failures_count: '0',
memory_status: 'ok',
timestamp: '2016-02-11 23:00:00',
});
};
await ml.jobTable.assertJobRowDetailsCounts(jobId, expectedCounts, expectedModelSizeStats);
});
});
}
1 change: 1 addition & 0 deletions x-pack/test/functional/apps/machine_learning/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./pages'));
loadTestFile(require.resolve('./create_single_metric_job'));
loadTestFile(require.resolve('./create_multi_metric_job'));
});
}
Loading

0 comments on commit 395cf6d

Please sign in to comment.