From 99f99291ef0eec96b37ed19faeab68e633daeffb Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 4 Jan 2019 14:00:34 +0100 Subject: [PATCH] [ML] Tests for ML annotations feature. (#27994) Adds unit/integration tests for various parts of the code base affected by the annotations feature. --- .../types/__mocks__/job_config_farequote.json | 135 ++++++++++++++++++ .../ml/common/types/annotations.test.ts | 23 +++ x-pack/plugins/ml/common/types/jobs.test.js | 15 ++ .../__mocks__/mock_annotations.json | 14 ++ .../annotations_table.test.js.snap | 126 ++++++++++++++++ .../__tests__/annotations_table_directive.js | 55 +++++++ .../annotations_table/annotations_table.js | 6 +- .../annotations_table.test.js | 50 +++++++ .../explorer/__tests__/explorer_controller.js | 1 + .../__snapshots__/index.test.tsx.snap | 43 ++++++ .../index.test.tsx | 27 ++++ .../__snapshots__/index.test.tsx.snap | 120 ++++++++++++++++ .../annotation_flyout/index.test.tsx | 27 ++++ .../__mocks__/mock_annotations_overlap.json | 54 +++++++ .../timeseries_chart_annotations.test.ts | 16 +++ 15 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json create mode 100644 x-pack/plugins/ml/common/types/annotations.test.ts create mode 100644 x-pack/plugins/ml/common/types/jobs.test.js create mode 100644 x-pack/plugins/ml/public/components/annotations_table/__mocks__/mock_annotations.json create mode 100644 x-pack/plugins/ml/public/components/annotations_table/__snapshots__/annotations_table.test.js.snap create mode 100644 x-pack/plugins/ml/public/components/annotations_table/__tests__/annotations_table_directive.js create mode 100644 x-pack/plugins/ml/public/components/annotations_table/annotations_table.test.js create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/index.test.tsx create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/index.test.tsx create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json create mode 100644 x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts diff --git a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json new file mode 100644 index 0000000000000..59ba8a6bffb03 --- /dev/null +++ b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json @@ -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" +} diff --git a/x-pack/plugins/ml/common/types/annotations.test.ts b/x-pack/plugins/ml/common/types/annotations.test.ts new file mode 100644 index 0000000000000..3f2bb29c921ed --- /dev/null +++ b/x-pack/plugins/ml/common/types/annotations.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/ml/common/types/jobs.test.js b/x-pack/plugins/ml/common/types/jobs.test.js new file mode 100644 index 0000000000000..02a6500403cf3 --- /dev/null +++ b/x-pack/plugins/ml/common/types/jobs.test.js @@ -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); + }); +}); diff --git a/x-pack/plugins/ml/public/components/annotations_table/__mocks__/mock_annotations.json b/x-pack/plugins/ml/public/components/annotations_table/__mocks__/mock_annotations.json new file mode 100644 index 0000000000000..2964989316da0 --- /dev/null +++ b/x-pack/plugins/ml/public/components/annotations_table/__mocks__/mock_annotations.json @@ -0,0 +1,14 @@ +[ + { + "timestamp": 1455026177994, + "end_timestamp": 1455041968976, + "annotation": "Major spike.", + "job_id": "farequote", + "type": "annotation", + "create_time": 1546417097181, + "create_username": "", + "modified_time": 1546417097181, + "modified_username": "", + "_id": "KCCkDWgB_ZdQ1MFDSYPi" + } +] diff --git a/x-pack/plugins/ml/public/components/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/components/annotations_table/__snapshots__/annotations_table.test.js.snap new file mode 100644 index 0000000000000..61870e434c065 --- /dev/null +++ b/x-pack/plugins/ml/public/components/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` +", + "end_timestamp": 1455041968976, + "job_id": "farequote", + "modified_time": 1546417097181, + "modified_username": "", + "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`] = ` + + + + + +`; + +exports[`AnnotationsTable Minimal initialization without props. 1`] = ` + +`; diff --git a/x-pack/plugins/ml/public/components/annotations_table/__tests__/annotations_table_directive.js b/x-pack/plugins/ml/public/components/annotations_table/__tests__/annotations_table_directive.js new file mode 100644 index 0000000000000..768a5a5ce4a0e --- /dev/null +++ b/x-pack/plugins/ml/public/components/annotations_table/__tests__/annotations_table_directive.js @@ -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 - ', () => { + 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('')($scope); + }).to.not.throwError(); + }); + + it('Initialization with empty annotations array doesn\'t throw an error', () => { + expect(() => { + $compile('')($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('')($scope); + }).to.not.throwError(); + + getAnnotationsStub.restore(); + }); + +}); diff --git a/x-pack/plugins/ml/public/components/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/components/annotations_table/annotations_table.js index db74e06ac2607..74b8474503e93 100644 --- a/x-pack/plugins/ml/public/components/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/components/annotations_table/annotations_table.js @@ -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(); diff --git a/x-pack/plugins/ml/public/components/annotations_table/annotations_table.test.js b/x-pack/plugins/ml/public/components/annotations_table/annotations_table.test.js new file mode 100644 index 0000000000000..b8fd4cbd94f19 --- /dev/null +++ b/x-pack/plugins/ml/public/components/annotations_table/annotations_table.test.js @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); + + test('Initialization with job config prop.', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + test('Initialization with annotations prop.', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + +}); diff --git a/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js b/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js index 671cf0fea1b7b..f220b06c6f4ce 100644 --- a/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js @@ -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); }); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..6d6fbfdae6e54 --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/__snapshots__/index.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnnotationDescriptionList Initialization with annotation. 1`] = ` +", + "title": "Created by", + }, + Object { + "description": "January 2nd 2019, 08:18:17", + "title": "Last modified", + }, + Object { + "description": "", + "title": "Modified by", + }, + ] + } + textStyle="normal" + type="column" +/> +`; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/index.test.tsx b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/index.test.tsx new file mode 100644 index 0000000000000..65520b18b11ae --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_description_list/index.test.tsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..0847c212825c4 --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/__snapshots__/index.test.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnnotationFlyout Initialization. 1`] = ` + + + +

+ Edit + annotation +

+
+
+ + ", + "end_timestamp": 1455041968976, + "job_id": "farequote", + "modified_time": 1546417097181, + "modified_username": "", + "timestamp": 1455026177994, + "type": "annotation", + } + } + /> + + + + + + + + + + Cancel + + + + + Delete + + + + + Update + + + + +
+`; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/index.test.tsx new file mode 100644 index 0000000000000..c9f8990e2c79e --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/annotation_flyout/index.test.tsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json b/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json new file mode 100644 index 0000000000000..03085ca39ad23 --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json @@ -0,0 +1,54 @@ +[ + { + "timestamp": 1454810815950, + "end_timestamp": 1455060155890, + "annotation": "Still learning the model.", + "job_id": "farequote", + "type": "annotation", + "create_time": 1546530633618, + "create_username": "", + "modified_time": 1546530633618, + "modified_username": "", + "_id": "6bpoFGgBrdsAtoAOt0WT", + "key": "A" + }, + { + "timestamp": 1455020901845, + "end_timestamp": 1455075725930, + "annotation": "Overlapping annotation.", + "job_id": "farequote", + "type": "annotation", + "create_time": 1546530659999, + "create_username": "", + "modified_time": 1546530659999, + "modified_username": "", + "_id": "6rppFGgBrdsAtoAOHkWg", + "key": "B" + }, + { + "timestamp": 1455027864892, + "end_timestamp": 1455042003613, + "annotation": "Massive spike.", + "job_id": "farequote", + "type": "annotation", + "create_time": 1546512778313, + "create_username": "", + "modified_time": 1546512778313, + "modified_username": "", + "_id": "Gl5YE2gBBwRksc9URChP", + "key": "C" + }, + { + "timestamp": 1455063006742, + "end_timestamp": 1455230768443, + "annotation": "Learned the model.", + "job_id": "farequote", + "type": "annotation", + "create_time": 1546530615480, + "create_username": "", + "modified_time": 1546530615480, + "modified_username": "", + "_id": "6LpoFGgBrdsAtoAOcEW7", + "key": "D" + } +] diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts b/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts new file mode 100644 index 0000000000000..240e840363a1f --- /dev/null +++ b/x-pack/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts @@ -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 }); + }); +});