[ML] Tests for ML annotations feature. (#27994)

Adds unit/integration tests for various parts of the code base affected by the annotations feature.
This commit is contained in:
Walter Rafelsberger 2019-01-04 14:00:34 +01:00 committed by GitHub
parent 3496dff912
commit 99f99291ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 711 additions and 1 deletions

View file

@ -0,0 +1,135 @@
{
"job_id": "farequote",
"job_type": "anomaly_detector",
"job_version": "7.0.0",
"description": "",
"create_time": 1546418356716,
"finished_time": 1546418359427,
"established_model_memory": 42102,
"analysis_config": {
"bucket_span": "15m",
"summary_count_field_name": "doc_count",
"detectors": [
{
"detector_description": "count",
"function": "count",
"detector_index": 0
}
],
"influencers": []
},
"analysis_limits": {
"model_memory_limit": "10mb",
"categorization_examples_limit": 4
},
"data_description": {
"time_field": "@timestamp",
"time_format": "epoch_ms"
},
"model_plot_config": {
"enabled": true
},
"model_snapshot_retention_days": 1,
"custom_settings": {
"created_by": "single-metric-wizard"
},
"model_snapshot_id": "1546418359",
"model_snapshot_min_version": "6.4.0",
"results_index_name": "shared",
"data_counts": {
"job_id": "farequote",
"processed_record_count": 479,
"processed_field_count": 479,
"input_bytes": 21554,
"input_field_count": 479,
"invalid_date_count": 0,
"missing_field_count": 0,
"out_of_order_timestamp_count": 0,
"empty_bucket_count": 0,
"sparse_bucket_count": 0,
"bucket_count": 478,
"earliest_record_timestamp": 1454804096000,
"latest_record_timestamp": 1455234298000,
"last_data_time": 1546418357578,
"input_record_count": 479
},
"model_size_stats": {
"job_id": "farequote",
"result_type": "model_size_stats",
"model_bytes": 42102,
"total_by_field_count": 3,
"total_over_field_count": 0,
"total_partition_field_count": 2,
"bucket_allocation_failures_count": 0,
"memory_status": "ok",
"log_time": 1546418359000,
"timestamp": 1455232500000
},
"datafeed_config": {
"datafeed_id": "datafeed-farequote",
"job_id": "farequote",
"query_delay": "115823ms",
"indices": [
"farequote"
],
"types": [],
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "*",
"fields": [],
"type": "best_fields",
"default_operator": "or",
"max_determinized_states": 10000,
"enable_position_increments": true,
"fuzziness": "AUTO",
"fuzzy_prefix_length": 0,
"fuzzy_max_expansions": 50,
"phrase_slop": 0,
"analyze_wildcard": true,
"escape": false,
"auto_generate_synonyms_phrase_query": true,
"fuzzy_transpositions": true,
"boost": 1
}
}
],
"adjust_pure_negative": true,
"boost": 1
}
},
"aggregations": {
"buckets": {
"date_histogram": {
"field": "@timestamp",
"interval": 900000,
"offset": 0,
"order": {
"_key": "asc"
},
"keyed": false,
"min_doc_count": 0
},
"aggregations": {
"@timestamp": {
"max": {
"field": "@timestamp"
}
}
}
}
},
"scroll_size": 1000,
"chunking_config": {
"mode": "manual",
"time_span": "900000000ms"
},
"delayed_data_check_config": {
"enabled": true
},
"state": "stopped"
},
"state": "closed"
}

View file

@ -0,0 +1,23 @@
/*
* 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 { ANNOTATION_TYPE } from '../constants/annotations';
import { isAnnotation, isAnnotations } from './annotations';
describe('Types: Annotations', () => {
test('Minimal integrity check.', () => {
const annotation = {
job_id: 'id',
annotation: 'Annotation text',
timestamp: 0,
type: ANNOTATION_TYPE.ANNOTATION,
};
expect(isAnnotation(annotation)).toBe(true);
expect(isAnnotations([annotation])).toBe(true);
});
});

View file

@ -0,0 +1,15 @@
/*
* 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 jobConfigFarequote from './__mocks__/job_config_farequote';
import { isMlJob, isMlJobs } from './jobs';
describe('Types: Jobs', () => {
test('Minimal integrity check.', () => {
expect(isMlJob(jobConfigFarequote)).toBe(true);
expect(isMlJobs([jobConfigFarequote])).toBe(true);
});
});

View file

@ -0,0 +1,14 @@
[
{
"timestamp": 1455026177994,
"end_timestamp": 1455041968976,
"annotation": "Major spike.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1546417097181,
"create_username": "<user unknown>",
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"_id": "KCCkDWgB_ZdQ1MFDSYPi"
}
]

View file

@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
<EuiInMemoryTable
className="eui-textOverflowWrap"
columns={
Array [
Object {
"field": "annotation",
"name": "Annotation",
"sortable": true,
},
Object {
"dataType": "date",
"field": "timestamp",
"name": "From",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "date",
"field": "end_timestamp",
"name": "To",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "date",
"field": "create_time",
"name": "Creation date",
"render": [Function],
"sortable": true,
},
Object {
"field": "create_username",
"name": "Created by",
"sortable": true,
},
Object {
"dataType": "date",
"field": "modified_time",
"name": "Last modified date",
"render": [Function],
"sortable": true,
},
Object {
"field": "modified_username",
"name": "Last modified by",
"sortable": true,
},
Object {
"align": "right",
"name": "View",
"render": [Function],
"width": "60px",
},
]
}
compressed={true}
items={
Array [
Object {
"_id": "KCCkDWgB_ZdQ1MFDSYPi",
"annotation": "Major spike.",
"create_time": 1546417097181,
"create_username": "<user unknown>",
"end_timestamp": 1455041968976,
"job_id": "farequote",
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"timestamp": 1455026177994,
"type": "annotation",
},
]
}
pagination={
Object {
"pageSizeOptions": Array [
5,
10,
25,
],
}
}
responsive={true}
rowProps={[Function]}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "timestamp",
},
}
}
/>
`;
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="spaceAround"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiLoadingSpinner
size="l"
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
<EuiCallOut
color="primary"
iconType="iInCircle"
size="m"
title="No annotations created for this job"
/>
`;

View file

@ -0,0 +1,55 @@
/*
* 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 jobConfig from '../../../../common/types/__mocks__/job_config_farequote';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import { ml } from '../../../services/ml_api_service';
describe('ML - <ml-annotation-table>', () => {
let $scope;
let $compile;
beforeEach(ngMock.module('kibana'));
beforeEach(() => {
ngMock.inject(function ($injector) {
$compile = $injector.get('$compile');
const $rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
});
});
afterEach(() => {
$scope.$destroy();
});
it('Plain initialization doesn\'t throw an error', () => {
expect(() => {
$compile('<ml-annotation-table />')($scope);
}).to.not.throwError();
});
it('Initialization with empty annotations array doesn\'t throw an error', () => {
expect(() => {
$compile('<ml-annotation-table annotations="[]" />')($scope);
}).to.not.throwError();
});
it('Initialization with job config doesn\'t throw an error', () => {
const getAnnotationsStub = sinon.stub(ml.annotations, 'getAnnotations').resolves({ annotations: [] });
expect(() => {
$scope.jobs = [jobConfig];
$compile('<ml-annotation-table jobs="jobs" />')($scope);
}).to.not.throwError();
getAnnotationsStub.restore();
});
});

View file

@ -109,7 +109,10 @@ class AnnotationsTable extends Component {
}
componentDidMount() {
if (this.props.annotations === undefined) {
if (
this.props.annotations === undefined &&
Array.isArray(this.props.jobs) && this.props.jobs.length > 0
) {
this.getAnnotations();
}
}
@ -118,6 +121,7 @@ class AnnotationsTable extends Component {
if (
this.props.annotations === undefined &&
this.state.isLoading === false &&
Array.isArray(this.props.jobs) && this.props.jobs.length > 0 &&
this.state.jobId !== this.props.jobs[0].job_id
) {
this.getAnnotations();

View file

@ -0,0 +1,50 @@
/*
* 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 jobConfig from '../../../common/types/__mocks__/job_config_farequote';
import mockAnnotations from './__mocks__/mock_annotations.json';
import { shallow } from 'enzyme';
import React from 'react';
import { AnnotationsTable } from './annotations_table';
jest.mock('ui/chrome', () => ({
getBasePath: (path) => path,
addBasePath: () => {}
}));
jest.mock('../../services/job_service', () => ({
mlJobService: {
getJob: jest.fn()
}
}));
jest.mock('../../services/ml_api_service', () => ({
ml: {
annotations: {
getAnnotations: jest.fn().mockResolvedValue({ annotations: [] })
}
}
}));
describe('AnnotationsTable', () => {
test('Minimal initialization without props.', () => {
const wrapper = shallow(<AnnotationsTable />);
expect(wrapper).toMatchSnapshot();
});
test('Initialization with job config prop.', () => {
const wrapper = shallow(<AnnotationsTable jobs={[jobConfig]} />);
expect(wrapper).toMatchSnapshot();
});
test('Initialization with annotations prop.', () => {
const wrapper = shallow(<AnnotationsTable annotations={mockAnnotations} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -19,6 +19,7 @@ describe('ML - Explorer Controller', () => {
const scope = $rootScope.$new();
$controller('MlExplorerController', { $scope: scope });
expect(Array.isArray(scope.annotationsData)).to.be(true);
expect(Array.isArray(scope.anomalyChartRecords)).to.be(true);
expect(scope.loading).to.be(true);
});

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationDescriptionList Initialization with annotation. 1`] = `
<EuiDescriptionList
align="left"
className="ml-annotation-description-list"
compressed={false}
listItems={
Array [
Object {
"description": "farequote",
"title": "Job ID",
},
Object {
"description": "February 9th 2016, 13:56:17",
"title": "Start",
},
Object {
"description": "February 9th 2016, 18:19:28",
"title": "End",
},
Object {
"description": "January 2nd 2019, 08:18:17",
"title": "Created",
},
Object {
"description": "<user unknown>",
"title": "Created by",
},
Object {
"description": "January 2nd 2019, 08:18:17",
"title": "Last modified",
},
Object {
"description": "<user unknown>",
"title": "Modified by",
},
]
}
textStyle="normal"
type="column"
/>
`;

View file

@ -0,0 +1,27 @@
/*
* 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 mockAnnotations from '../../../components/annotations_table/__mocks__/mock_annotations.json';
import { shallow } from 'enzyme';
import moment from 'moment-timezone';
import React from 'react';
import { AnnotationDescriptionList } from './index';
describe('AnnotationDescriptionList', () => {
beforeEach(() => {
moment.tz.setDefault('UTC');
});
afterEach(() => {
moment.tz.setDefault('Browser');
});
test('Initialization with annotation.', () => {
const wrapper = shallow(<AnnotationDescriptionList annotation={mockAnnotations[0]} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,120 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationFlyout Initialization. 1`] = `
<EuiFlyout
aria-labelledby="Add annotation"
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[MockFunction]}
ownFocus={false}
size="s"
>
<EuiFlyoutHeader
hasBorder={true}
>
<EuiTitle
size="s"
textTransform="none"
>
<h2
id="mlAnnotationFlyoutTitle"
>
Edit
annotation
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<Component
annotation={
Object {
"_id": "KCCkDWgB_ZdQ1MFDSYPi",
"annotation": "Major spike.",
"create_time": 1546417097181,
"create_username": "<user unknown>",
"end_timestamp": 1455041968976,
"job_id": "farequote",
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"timestamp": 1455026177994,
"type": "annotation",
}
}
/>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={true}
hasEmptyLabelSpace={false}
label="Annotation text"
>
<EuiTextArea
fullWidth={true}
isInvalid={false}
onChange={[MockFunction]}
placeholder="..."
resize="vertical"
value="Major spike."
/>
</EuiFormRow>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
color="primary"
flush="left"
iconSide="left"
iconType="cross"
onClick={[MockFunction]}
type="button"
>
Cancel
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
color="danger"
iconSide="left"
onClick={[Function]}
type="button"
>
Delete
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
fill={true}
iconSide="left"
isDisabled={false}
onClick={[Function]}
type="button"
>
Update
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
`;

View file

@ -0,0 +1,27 @@
/*
* 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 mockAnnotations from '../../../components/annotations_table/__mocks__/mock_annotations.json';
import { shallow } from 'enzyme';
import React from 'react';
import { AnnotationFlyout } from './index';
describe('AnnotationFlyout', () => {
test('Initialization.', () => {
const props = {
annotation: mockAnnotations[0],
cancelAction: jest.fn(),
controlFunc: jest.fn(),
deleteAction: jest.fn(),
saveAction: jest.fn(),
};
const wrapper = shallow(<AnnotationFlyout {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,54 @@
[
{
"timestamp": 1454810815950,
"end_timestamp": 1455060155890,
"annotation": "Still learning the model.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1546530633618,
"create_username": "<user unknown>",
"modified_time": 1546530633618,
"modified_username": "<user unknown>",
"_id": "6bpoFGgBrdsAtoAOt0WT",
"key": "A"
},
{
"timestamp": 1455020901845,
"end_timestamp": 1455075725930,
"annotation": "Overlapping annotation.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1546530659999,
"create_username": "<user unknown>",
"modified_time": 1546530659999,
"modified_username": "<user unknown>",
"_id": "6rppFGgBrdsAtoAOHkWg",
"key": "B"
},
{
"timestamp": 1455027864892,
"end_timestamp": 1455042003613,
"annotation": "Massive spike.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1546512778313,
"create_username": "<user unknown>",
"modified_time": 1546512778313,
"modified_username": "<user unknown>",
"_id": "Gl5YE2gBBwRksc9URChP",
"key": "C"
},
{
"timestamp": 1455063006742,
"end_timestamp": 1455230768443,
"annotation": "Learned the model.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1546530615480,
"create_username": "<user unknown>",
"modified_time": 1546530615480,
"modified_username": "<user unknown>",
"_id": "6LpoFGgBrdsAtoAOcEW7",
"key": "D"
}
]

View file

@ -0,0 +1,16 @@
/*
* 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 mockAnnotationsOverlap from './__mocks__/mock_annotations_overlap.json';
import { getAnnotationLevels } from './timeseries_chart_annotations';
describe('Timeseries Chart Annotations: getAnnotationLevels()', () => {
test('getAnnotationLevels()', () => {
const levels = getAnnotationLevels(mockAnnotationsOverlap);
expect(levels).toEqual({ A: 0, B: 1, C: 2, D: 2 });
});
});