lib/model/unittest/CDetectionRuleTest.cc (788 lines of code) (raw):

/* * 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 and the following additional limitation. Functionality enabled by the * files subject to the Elastic License 2.0 may only be used in production when * invoked by an Elasticsearch process with a license key installed that permits * use of machine learning features. You may not use this file except in * compliance with the Elastic License 2.0 and the foregoing additional * limitation. */ #include <core/CLogger.h> #include <core/CPatternSet.h> #include <core/Constants.h> #include <core/CoreTypes.h> #include <model/CAnomalyDetectorModel.h> #include <model/CDataGatherer.h> #include <model/CDetectionRule.h> #include <model/CResourceMonitor.h> #include <model/CRuleCondition.h> #include <model/CSearchKey.h> #include <model/ModelTypes.h> #include <model/SModelParams.h> #include <maths/common/CNormalMeanPrecConjugate.h> #include <maths/common/MathsTypes.h> #include <maths/time_series/CTimeSeriesDecomposition.h> #include <maths/time_series/CTimeSeriesModel.h> #include <test/CRandomNumbers.h> #include "Mocks.h" #include "ModelTestHelpers.h" #include <boost/test/tools/interface.hpp> #include <boost/test/unit_test.hpp> #include <boost/test/unit_test_suite.hpp> #include <memory> #include <string> #include <vector> BOOST_AUTO_TEST_SUITE(CDetectionRuleTest) using namespace ml; using namespace model; namespace { using TFeatureVec = std::vector<model_t::EFeature>; using TMockModelPtr = std::unique_ptr<CMockModel>; const std::string EMPTY_STRING; TMockModelPtr initializeModel(CResourceMonitor& resourceMonitor) { constexpr core_t::TTime bucketLength{600}; SModelParams const params{bucketLength}; CSearchKey const key; model_t::TFeatureVec features; // Initialize mock model CAnomalyDetectorModel::TDataGathererPtr gatherer; features.assign(1, model_t::E_IndividualSumByBucketAndPerson); gatherer = CDataGathererBuilder(analysisCategory(features[0]), features, params, key, 0) .personFieldName("p") .buildSharedPtr(); std::string const person("p1"); bool addedPerson{false}; gatherer->addPerson(person, resourceMonitor, addedPerson); const CMockModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; //we don't care about influence auto model = std::make_unique<CMockModel>(params, gatherer, influenceCalculators); maths::time_series::CTimeSeriesDecomposition const trend; maths::common::CNormalMeanPrecConjugate const prior{ maths::common::CNormalMeanPrecConjugate::nonInformativePrior(maths_t::E_ContinuousData)}; maths::common::CModelParams const timeSeriesModelParams{ bucketLength, 1.0, 0.001, 0.2, 6 * core::constants::HOUR, 24 * core::constants::HOUR}; auto timeSeriesModel = std::make_unique<maths::time_series::CUnivariateTimeSeriesModel>( timeSeriesModelParams, 0, trend, prior); CMockModel::TMathsModelUPtrVec models; models.emplace_back(std::move(timeSeriesModel)); model->mockTimeSeriesModels(std::move(models)); return model; } } class CTestFixture { protected: CResourceMonitor m_ResourceMonitor; }; BOOST_FIXTURE_TEST_CASE(testApplyGivenScope, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; const SModelParams params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_PopulationMeanByPersonAndAttribute); std::string const partitionFieldName("partition"); std::string const partitionFieldValue("par_1"); std::string const personFieldName("over"); std::string const attributeFieldName("by"); CSearchKey const key(0, function_t::E_PopulationMetricMean, false, model_t::E_XF_None, "", attributeFieldName, personFieldName, partitionFieldName); auto gathererPtr = CDataGathererBuilder(model_t::E_PopulationMetric, features, params, key, startTime) .partitionFieldValue(partitionFieldValue) .personFieldName(personFieldName) .attributeFieldName(attributeFieldName) .buildSharedPtr(); std::string const person1("p1"); bool added = false; gathererPtr->addPerson(person1, m_ResourceMonitor, added); std::string const person2("p2"); gathererPtr->addPerson(person2, m_ResourceMonitor, added); std::string const attr11("a1_1"); std::string const attr12("a1_2"); std::string const attr21("a2_1"); std::string const attr22("a2_2"); gathererPtr->addAttribute(attr11, m_ResourceMonitor, added); gathererPtr->addAttribute(attr12, m_ResourceMonitor, added); gathererPtr->addAttribute(attr21, m_ResourceMonitor, added); gathererPtr->addAttribute(attr22, m_ResourceMonitor, added); CMockModel model(params, gathererPtr, influenceCalculators); model.mockPopulation(true); CAnomalyDetectorModel::TDouble1Vec const actual(1, 4.99); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 0, 0, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 0, 1, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 1, 2, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 1, 3, 100, actual); for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["a1_1","a2_2"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(attributeFieldName, valueFilter); } else { rule.excludeScope(attributeFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) == isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["a1*"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(attributeFieldName, valueFilter); } else { rule.excludeScope(attributeFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) != isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["*2"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(attributeFieldName, valueFilter); } else { rule.excludeScope(attributeFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) == isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["*1*"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(attributeFieldName, valueFilter); } else { rule.excludeScope(attributeFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) != isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["p2"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(personFieldName, valueFilter); } else { rule.excludeScope(personFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) == isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["par_1"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(partitionFieldName, valueFilter); } else { rule.excludeScope(partitionFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) == isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) == isInclude); } for (auto filterType : {CRuleScope::E_Include, CRuleScope::E_Exclude}) { std::string const filterJson(R"(["par_2"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); CDetectionRule rule; if (filterType == CRuleScope::E_Include) { rule.includeScope(partitionFieldName, valueFilter); } else { rule.excludeScope(partitionFieldName, valueFilter); } bool isInclude = filterType == CRuleScope::E_Include; model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) != isInclude); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) != isInclude); } } BOOST_FIXTURE_TEST_CASE(testApplyGivenNumericalActualCondition, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; CSearchKey const key; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec const features{model_t::E_IndividualMeanByPerson}; auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .buildSharedPtr(); std::string const person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); CAnomalyDetectorModel::TDouble1Vec const actual100{4.99}; CAnomalyDetectorModel::TDouble1Vec const actual200{5.00}; CAnomalyDetectorModel::TDouble1Vec const actual300{5.01}; model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100, actual100); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 200, actual200); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 300, actual300); auto testRule = [&](CRuleCondition::ERuleConditionOperator op, double value, bool expected100, bool expected200, bool expected300) { CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Actual); condition.op(op); condition.value(value); CDetectionRule rule; rule.addCondition(condition); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100) == expected100); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 200) == expected200); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 300) == expected300); }; testRule(CRuleCondition::E_LT, 5.0, true, false, false); testRule(CRuleCondition::E_LTE, 5.0, true, true, false); testRule(CRuleCondition::E_GT, 5.0, false, false, true); testRule(CRuleCondition::E_GTE, 5.0, false, true, true); } BOOST_FIXTURE_TEST_CASE(testApplyGivenNumericalTypicalCondition, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; const CSearchKey key; const SModelParams params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec const features{model_t::E_IndividualMeanByPerson}; auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .buildSharedPtr(); const std::string person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); const CAnomalyDetectorModel::TDouble1Vec actual100{4.99}; const CAnomalyDetectorModel::TDouble1Vec actual200{5.00}; const CAnomalyDetectorModel::TDouble1Vec actual300{5.01}; model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100, actual100); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 200, actual200); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 300, actual300); const CAnomalyDetectorModel::TDouble1Vec typical100{44.99}; const CAnomalyDetectorModel::TDouble1Vec typical200{45.00}; const CAnomalyDetectorModel::TDouble1Vec typical300{45.01}; model.mockAddBucketBaselineMean(model_t::E_IndividualMeanByPerson, 0, 0, 100, typical100); model.mockAddBucketBaselineMean(model_t::E_IndividualMeanByPerson, 0, 0, 200, typical200); model.mockAddBucketBaselineMean(model_t::E_IndividualMeanByPerson, 0, 0, 300, typical300); auto testRule = [&](CRuleCondition::ERuleConditionOperator op, double value, bool expected100, bool expected200, bool expected300) { CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Typical); condition.op(op); condition.value(value); CDetectionRule rule; rule.addCondition(condition); const model_t::CResultType resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100) == expected100); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 200) == expected200); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 300) == expected300); }; testRule(CRuleCondition::E_LT, 45.0, true, false, false); testRule(CRuleCondition::E_GT, 45.0, false, false, true); } BOOST_FIXTURE_TEST_CASE(testApplyGivenNumericalDiffAbsCondition, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; const CSearchKey key; const SModelParams params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec const features{model_t::E_IndividualMeanByPerson}; auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .buildSharedPtr(); const std::string person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); const std::vector<CAnomalyDetectorModel::TDouble1Vec> actuals{ {8.9}, {9.0}, {9.1}, {10.9}, {11.0}, {11.1}}; const std::vector<CAnomalyDetectorModel::TDouble1Vec> typicals(6, {10.0}); for (size_t i = 0; i < actuals.size(); ++i) { model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100 * (i + 1), actuals[i]); model.mockAddBucketBaselineMean(model_t::E_IndividualMeanByPerson, 0, 0, 100 * (i + 1), typicals[i]); } auto testRule = [&](CRuleCondition::ERuleConditionOperator op, double value, const std::vector<bool>& expected) { CRuleCondition condition; condition.appliesTo(CRuleCondition::E_DiffFromTypical); condition.op(op); condition.value(value); CDetectionRule rule; rule.addCondition(condition); const model_t::CResultType resultType(model_t::CResultType::E_Final); for (size_t i = 0; i < expected.size(); ++i) { BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100 * (i + 1)) == expected[i]); } }; testRule(CRuleCondition::E_LT, 1.0, {false, false, true, true, false, false}); testRule(CRuleCondition::E_GT, 1.0, {true, false, false, false, false, true}); } BOOST_FIXTURE_TEST_CASE(testApplyGivenNoActualValueAvailable, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; CSearchKey const key; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_IndividualMeanByPerson); CAnomalyDetectorModel::TDataGathererPtr const gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .buildSharedPtr(); std::string const person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); CAnomalyDetectorModel::TDouble1Vec const actual100(1, 4.99); CAnomalyDetectorModel::TDouble1Vec const actual200(1, 5.00); CAnomalyDetectorModel::TDouble1Vec const actual300(1, 5.01); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100, actual100); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 200, actual200); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 300, actual300); CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Actual); condition.op(CRuleCondition::E_LT); condition.value(5.0); CDetectionRule rule; rule.addCondition(condition); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 400) == false); } BOOST_FIXTURE_TEST_CASE(testApplyGivenDifferentSeriesAndIndividualModel, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; CSearchKey const key; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_IndividualMeanByPerson); std::string const personFieldName("series"); CAnomalyDetectorModel::TDataGathererPtr const gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .personFieldName(personFieldName) .buildSharedPtr(); std::string const person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); std::string const person2("p2"); gathererPtr->addPerson(person2, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); CAnomalyDetectorModel::TDouble1Vec const p1Actual(1, 4.99); CAnomalyDetectorModel::TDouble1Vec const p2Actual(1, 4.99); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100, p1Actual); model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 1, 0, 100, p2Actual); CDetectionRule rule; std::string const filterJson(R"(["p1"])"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); rule.includeScope(personFieldName, valueFilter); CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Actual); condition.op(CRuleCondition::E_LT); condition.value(5.0); rule.addCondition(condition); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 1, 0, 100) == false); } BOOST_FIXTURE_TEST_CASE(testApplyGivenDifferentSeriesAndPopulationModel, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; CSearchKey const key; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_PopulationMeanByPersonAndAttribute); std::string const personFieldName("over"); std::string const attributeFieldName("by"); auto gathererPtr = CDataGathererBuilder(model_t::E_PopulationMetric, features, params, key, startTime) .personFieldName(personFieldName) .attributeFieldName(attributeFieldName) .buildSharedPtr(); std::string const person1("p1"); bool added = false; gathererPtr->addPerson(person1, m_ResourceMonitor, added); std::string const person2("p2"); gathererPtr->addPerson(person2, m_ResourceMonitor, added); std::string const attr11("a1_1"); std::string const attr12("a1_2"); std::string const attr21("a2_1"); std::string const attr22("a2_2"); gathererPtr->addAttribute(attr11, m_ResourceMonitor, added); gathererPtr->addAttribute(attr12, m_ResourceMonitor, added); gathererPtr->addAttribute(attr21, m_ResourceMonitor, added); gathererPtr->addAttribute(attr22, m_ResourceMonitor, added); CMockModel model(params, gathererPtr, influenceCalculators); model.mockPopulation(true); CAnomalyDetectorModel::TDouble1Vec const actual(1, 4.99); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 0, 0, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 0, 1, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 1, 2, 100, actual); model.mockAddBucketValue(model_t::E_PopulationMeanByPersonAndAttribute, 1, 3, 100, actual); CDetectionRule rule; std::string const filterJson("[\"" + attr12 + "\"]"); core::CPatternSet valueFilter; valueFilter.initFromJson(filterJson); rule.includeScope(attributeFieldName, valueFilter); CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Actual); condition.op(CRuleCondition::E_LT); condition.value(5.0); rule.addCondition(condition); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 0, 100) == false); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 0, 1, 100)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 2, 100) == false); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_PopulationMeanByPersonAndAttribute, resultType, 1, 3, 100) == false); } BOOST_FIXTURE_TEST_CASE(testApplyGivenMultipleConditions, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; const CSearchKey key; const SModelParams params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec const features{model_t::E_IndividualMeanByPerson}; const std::string personFieldName("series"); auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .personFieldName(personFieldName) .buildSharedPtr(); const std::string person1("p1"); bool addedPerson = false; gathererPtr->addPerson(person1, m_ResourceMonitor, addedPerson); CMockModel model(params, gathererPtr, influenceCalculators); const CAnomalyDetectorModel::TDouble1Vec p1Actual{10.0}; model.mockAddBucketValue(model_t::E_IndividualMeanByPerson, 0, 0, 100, p1Actual); auto testRule = [&](const CRuleCondition& condition1, const CRuleCondition& condition2, bool expected) { CDetectionRule rule; rule.addCondition(condition1); rule.addCondition(condition2); const model_t::CResultType resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100) == expected); }; CRuleCondition condition1; condition1.appliesTo(CRuleCondition::E_Actual); condition1.op(CRuleCondition::E_LT); condition1.value(9.0); CRuleCondition condition2; condition2.appliesTo(CRuleCondition::E_Actual); condition2.op(CRuleCondition::E_LT); condition2.value(9.5); testRule(condition1, condition2, false); condition1.value(11.0); condition2.value(9.5); testRule(condition1, condition2, false); condition1.value(9.0); condition2.value(10.5); testRule(condition1, condition2, false); condition1.value(12.0); condition2.value(10.5); testRule(condition1, condition2, true); } BOOST_FIXTURE_TEST_CASE(testApplyGivenTimeCondition, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_IndividualMeanByPerson); std::string const partitionFieldName("partition"); std::string const personFieldName("series"); CSearchKey const key(0, function_t::E_IndividualMetricMean, false, model_t::E_XF_None, "", personFieldName, EMPTY_STRING, partitionFieldName); auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .personFieldName(personFieldName) .buildSharedPtr(); CMockModel const model(params, gathererPtr, influenceCalculators); CRuleCondition conditionGte; conditionGte.appliesTo(CRuleCondition::E_Time); conditionGte.op(CRuleCondition::E_GTE); conditionGte.value(100); CRuleCondition conditionLt; conditionLt.appliesTo(CRuleCondition::E_Time); conditionLt.op(CRuleCondition::E_LT); conditionLt.value(200); CDetectionRule rule; rule.addCondition(conditionGte); rule.addCondition(conditionLt); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 99) == false); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 150)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 200) == false); } BOOST_FIXTURE_TEST_CASE(testRuleActions, CTestFixture) { constexpr core_t::TTime bucketLength = 100; constexpr core_t::TTime startTime = 100; SModelParams const params(bucketLength); const CAnomalyDetectorModel::TFeatureInfluenceCalculatorCPtrPrVecVec influenceCalculators; TFeatureVec features; features.push_back(model_t::E_IndividualMeanByPerson); std::string const partitionFieldName("partition"); std::string const personFieldName("series"); CSearchKey const key(0, function_t::E_IndividualMetricMean, false, model_t::E_XF_None, "", personFieldName, EMPTY_STRING, partitionFieldName); auto gathererPtr = CDataGathererBuilder(model_t::E_Metric, features, params, key, startTime) .personFieldName(personFieldName) .buildSharedPtr(); CMockModel const model(params, gathererPtr, influenceCalculators); CRuleCondition conditionGte; conditionGte.appliesTo(CRuleCondition::E_Time); conditionGte.op(CRuleCondition::E_GTE); conditionGte.value(100); CDetectionRule rule; rule.addCondition(conditionGte); model_t::CResultType const resultType(model_t::CResultType::E_Final); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipModelUpdate, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100) == false); rule.action(CDetectionRule::E_SkipModelUpdate); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100) == false); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipModelUpdate, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); rule.action(3); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipResult, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); BOOST_TEST_REQUIRE(rule.apply(CDetectionRule::E_SkipModelUpdate, model, model_t::E_IndividualMeanByPerson, resultType, 0, 0, 100)); } BOOST_FIXTURE_TEST_CASE(testRuleTimeShiftShouldShiftTimeSeriesModelState, CTestFixture) { test::CRandomNumbers rng; test::CRandomNumbers::TDoubleVec timeShifts; rng.generateUniformSamples(-3600, 3600, 10, timeShifts); for (const auto timeShift : timeShifts) { core_t::TTime timeShiftInSecs{static_cast<core_t::TTime>(timeShift)}; TMockModelPtr model{initializeModel(m_ResourceMonitor)}; // Capture state before the rule is applied const auto& trendModel = static_cast<const maths::time_series::CTimeSeriesDecomposition&>( static_cast<const maths::time_series::CUnivariateTimeSeriesModel*>( model->model(0)) ->trendModel()); const core_t::TTime lastValueTime = trendModel.lastValueTime(); const auto& annotations = model->annotations(); const std::size_t numAnnotationsBeforeShift = annotations.size(); constexpr core_t::TTime timestamp{100}; CRuleCondition conditionGte; conditionGte.appliesTo(CRuleCondition::E_Time); conditionGte.op(CRuleCondition::E_GTE); conditionGte.value(static_cast<double>(timestamp)); // When time shift rule is applied CDetectionRule rule; rule.addCondition(conditionGte); rule.addTimeShift(timeShiftInSecs); rule.executeCallback(*model, timestamp); // the time series model should have been shifted by specified amount. BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTime + timeShiftInSecs); BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShiftInSecs); // and an annotation should have been added to the model BOOST_TEST_REQUIRE(annotations.size() == numAnnotationsBeforeShift + 1); } } BOOST_FIXTURE_TEST_CASE(testRuleTimeShiftShouldNotApplyTwice, CTestFixture) { // Test that if a rule has already been applied, it should not be applied again. constexpr core_t::TTime timeShift{3600}; const TMockModelPtr model{initializeModel(m_ResourceMonitor)}; const auto& trendModel = static_cast<const maths::time_series::CTimeSeriesDecomposition&>( static_cast<const maths::time_series::CUnivariateTimeSeriesModel*>(model->model(0)) ->trendModel()); core_t::TTime timestamp{100}; CRuleCondition conditionGte; conditionGte.appliesTo(CRuleCondition::E_Time); conditionGte.op(CRuleCondition::E_GTE); conditionGte.value(static_cast<double>(timestamp)); // When time shift rule is applied twice CDetectionRule rule; rule.addCondition(conditionGte); rule.addTimeShift(timeShift); rule.executeCallback(*model, timestamp); core_t::TTime lastValueTimeAfterFirstShift = trendModel.lastValueTime(); core_t::TTime timeShiftAfterFirstShift = trendModel.timeShift(); // the values after the second time should be the same as the values after the first time shift. timestamp += timeShift; // simulate the time has moved forward by the time shift rule.executeCallback(*model, timestamp); BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift); BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShiftAfterFirstShift); } BOOST_FIXTURE_TEST_CASE(testTwoTimeShiftRuleShouldShiftTwice, CTestFixture) { // Test that if two rules are applied, the time series model should be shifted twice. constexpr core_t::TTime timeShift1{3600}; constexpr core_t::TTime timeShift2{7200}; const TMockModelPtr model{initializeModel(m_ResourceMonitor)}; const auto& trendModel = static_cast<const maths::time_series::CTimeSeriesDecomposition&>( static_cast<const maths::time_series::CUnivariateTimeSeriesModel*>(model->model(0)) ->trendModel()); core_t::TTime timestamp{100}; CRuleCondition conditionGte; conditionGte.appliesTo(CRuleCondition::E_Time); conditionGte.op(CRuleCondition::E_GTE); conditionGte.value(static_cast<double>(timestamp)); // When time shift rule is applied twice CDetectionRule rule1; rule1.addCondition(conditionGte); rule1.addTimeShift(timeShift1); rule1.executeCallback(*model, timestamp); const core_t::TTime lastValueTimeAfterFirstShift = trendModel.lastValueTime(); CDetectionRule rule2; rule2.addCondition(conditionGte); rule2.addTimeShift(timeShift2); rule2.executeCallback(*model, timestamp); // the values after the second time should be the sum of two rules. timestamp += timeShift1; // simulate the time has moved forward by the time shift rule2.executeCallback(*model, timestamp); BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift + timeShift2); BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShift1 + timeShift2); } BOOST_FIXTURE_TEST_CASE(testChecksum, CTestFixture) { // Create two identical rules CDetectionRule rule1; CDetectionRule rule2; // Compute checksums std::uint64_t checksum1 = rule1.checksum(); std::uint64_t checksum2 = rule2.checksum(); // Verify that identical rules have the same checksum BOOST_REQUIRE_EQUAL(checksum1, checksum2); // Test actions // Modify the action of rule2 rule2.action(CDetectionRule::E_SkipModelUpdate); // Verify that different actions result in different checksums checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_NE(checksum1, checksum2); // Test conditions // Reset rule2 to be identical to rule1 rule2 = rule1; // Add a condition to rule2 CRuleCondition condition; condition.appliesTo(CRuleCondition::E_Actual); condition.op(CRuleCondition::E_GT); condition.value(100.0); rule2.addCondition(condition); // Verify that adding a condition changes the checksum checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_NE(checksum1, checksum2); // Add the same condition to rule1 rule1.addCondition(condition); // Verify that identical conditions result in the same checksum checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_EQUAL(checksum1, checksum2); // Modify the condition in rule2 condition.value(200.0); rule2.clearConditions(); rule2.addCondition(condition); // Verify that different condition values result in different checksums checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_NE(checksum1, checksum2); // Test Scope rule2 = rule1; // Modify the scope of rule2 const std::string fieldName = "user"; core::CPatternSet valueFilter; valueFilter.initFromPatternList({"admin"}); rule2.includeScope(fieldName, valueFilter); // Verify that different scopes result in different checksums checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_NE(checksum1, checksum2); // Add the same scope to rule1 rule1.includeScope(fieldName, valueFilter); // Verify that identical scopes result in the same checksum checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_EQUAL(checksum1, checksum2); // Test Time Shift // Modify the time shift in rule2 rule2.addTimeShift(3600); // Verify that different time shifts result in different checksums checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_NE(checksum1, checksum2); // Add the same time shift to rule1 rule1.addTimeShift(3600); // Verify that identical time shifts result in the same checksum checksum1 = rule1.checksum(); checksum2 = rule2.checksum(); BOOST_REQUIRE_EQUAL(checksum1, checksum2); } BOOST_AUTO_TEST_SUITE_END()