| // Copyright 2017 The Fuchsia Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #ifndef COBALT_ANALYZER_REPORT_MASTER_REPORT_GENERATOR_ABSTRACT_TEST_H_ |
| #define COBALT_ANALYZER_REPORT_MASTER_REPORT_GENERATOR_ABSTRACT_TEST_H_ |
| |
| #include "analyzer/report_master/report_generator.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "./observation.pb.h" |
| #include "encoder/client_secret.h" |
| #include "encoder/encoder.h" |
| #include "encoder/project_context.h" |
| #include "glog/logging.h" |
| #include "third_party/googletest/googletest/include/gtest/gtest.h" |
| |
| // This file contains type-parameterized tests of ReportGenerator. |
| // |
| // We use C++ templates along with the macros TYPED_TEST_CASE_P and |
| // TYPED_TEST_P in order to define test templates that may be instantiated to |
| // to produce concrete tests that use various implementations of Datastore. |
| // |
| // See report_generator_test.cc and report_generator_emulator_test.cc for the |
| // concrete instantiations. |
| // |
| // NOTE: If you add a new test to this file you must add its name to the |
| // invocation REGISTER_TYPED_TEST_CASE_P macro at the bottom of this file. |
| |
| namespace cobalt { |
| namespace analyzer { |
| |
| namespace testing { |
| |
| const uint32_t kCustomerId = 1; |
| const uint32_t kProjectId = 1; |
| const uint32_t kMetricId = 1; |
| const uint32_t kReportConfigId = 1; |
| const uint32_t kForculusEncodingConfigId = 1; |
| const uint32_t kBasicRapporEncodingConfigId = 2; |
| const char kPartName1[] = "Part1"; |
| const char kPartName2[] = "Part2"; |
| const size_t kForculusThreshold = 20; |
| |
| // This unix timestamp corresponds to Friday Dec 2, 2016 in UTC |
| const time_t kSomeTimestamp = 1480647356; |
| // This is the day index for Friday Dec 2, 2016 |
| const uint32_t kDayIndex = 17137; |
| |
| const char* kMetricConfigText = R"( |
| # Metric 1 has two string parts. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 1 |
| time_zone_policy: UTC |
| parts { |
| key: "Part1" |
| value { |
| } |
| } |
| parts { |
| key: "Part2" |
| value { |
| } |
| } |
| } |
| |
| )"; |
| |
| const char* kEncodingConfigText = R"( |
| # EncodingConfig 1 is Forculus. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 1 |
| forculus { |
| threshold: 20 |
| } |
| } |
| |
| # EncodingConfig 2 is Basic RAPPOR. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 2 |
| basic_rappor { |
| prob_0_becomes_1: 0.25 |
| prob_1_stays_1: 0.75 |
| string_categories: { |
| category: "Apple" |
| category: "Banana" |
| category: "Cantaloupe" |
| } |
| } |
| } |
| |
| )"; |
| |
| const char* kReportConfigText = R"( |
| # ReportConfig 1 specifies a report of both variables of Metric 1. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 1 |
| metric_id: 1 |
| variable { |
| metric_part: "Part1" |
| } |
| variable { |
| metric_part: "Part2" |
| } |
| } |
| |
| )"; |
| } // namespace testing |
| |
| // ReportGeneratorAbstractTest is templatized on the parameter |
| // |StoreFactoryClass| which must be the name of a class that contains the |
| // following method: static DataStore* NewStore() |
| // See MemoryStoreFactory in sotre/memory_store_test_helper.h and |
| // BigtableStoreEmulatorFactory in store/bigtable_emulator_helper.h. |
| template <class StoreFactoryClass> |
| class ReportGeneratorAbstractTest : public ::testing::Test { |
| protected: |
| ReportGeneratorAbstractTest() |
| : data_store_(StoreFactoryClass::NewStore()), |
| observation_store_(new store::ObservationStore(data_store_)), |
| report_store_(new store::ReportStore(data_store_)) { |
| report_id_.set_customer_id(testing::kCustomerId); |
| report_id_.set_project_id(testing::kProjectId); |
| report_id_.set_report_config_id(testing::kReportConfigId); |
| } |
| |
| void SetUp() { |
| // Clear the DataStore. |
| EXPECT_EQ(store::kOK, |
| data_store_->DeleteAllRows(store::DataStore::kObservations)); |
| EXPECT_EQ(store::kOK, |
| data_store_->DeleteAllRows(store::DataStore::kReportMetadata)); |
| EXPECT_EQ(store::kOK, |
| data_store_->DeleteAllRows(store::DataStore::kReportRows)); |
| |
| // Parse the metric config string |
| auto metric_parse_result = |
| config::MetricRegistry::FromString(testing::kMetricConfigText, nullptr); |
| EXPECT_EQ(config::kOK, metric_parse_result.second); |
| std::shared_ptr<config::MetricRegistry> metric_registry( |
| metric_parse_result.first.release()); |
| |
| // Parse the encoding config string |
| auto encoding_parse_result = config::EncodingRegistry::FromString( |
| testing::kEncodingConfigText, nullptr); |
| EXPECT_EQ(config::kOK, encoding_parse_result.second); |
| std::shared_ptr<config::EncodingRegistry> encoding_config_registry( |
| encoding_parse_result.first.release()); |
| |
| // Parse the report config string |
| auto report_parse_result = |
| config::ReportRegistry::FromString(testing::kReportConfigText, nullptr); |
| EXPECT_EQ(config::kOK, report_parse_result.second); |
| std::shared_ptr<config::ReportRegistry> report_config_registry( |
| report_parse_result.first.release()); |
| |
| // Make a ProjectContext |
| project_.reset( |
| new encoder::ProjectContext(testing::kCustomerId, testing::kProjectId, |
| metric_registry, encoding_config_registry)); |
| |
| std::shared_ptr<config::AnalyzerConfig> analyzer_config( |
| new config::AnalyzerConfig(encoding_config_registry, metric_registry, |
| report_config_registry)); |
| |
| // Make the ReportGenerator |
| report_generator_.reset(new ReportGenerator( |
| analyzer_config, observation_store_, report_store_)); |
| } |
| |
| // Makes an Observation with two string parts, both of which have the |
| // given |string_value|, using the encoding with the given encoding_config_id. |
| std::unique_ptr<Observation> MakeObservation(std::string string_value, |
| uint32_t encoding_config_id) { |
| // Construct a new Encoder with a new client secret. |
| encoder::Encoder encoder(project_, |
| encoder::ClientSecret::GenerateNewSecret()); |
| // Set a static current time so we know we have a static day_index. |
| encoder.set_current_time(testing::kSomeTimestamp); |
| |
| // Construct the two-part value to add. |
| encoder::Encoder::Value value; |
| value.AddStringPart(encoding_config_id, testing::kPartName1, string_value); |
| value.AddStringPart(encoding_config_id, testing::kPartName2, string_value); |
| |
| // Encode an observation. |
| encoder::Encoder::Result result = encoder.Encode(testing::kMetricId, value); |
| EXPECT_EQ(encoder::Encoder::kOK, result.status); |
| EXPECT_TRUE(result.observation.get() != nullptr); |
| EXPECT_EQ(2, result.observation->parts_size()); |
| return std::move(result.observation); |
| } |
| |
| // Adds to the ObservationStore |num_clients| observations of our test metric |
| // that each encode the given string |value| using the given |
| // |encoding_config_id|. Each Observation is generated as if from a different |
| // client. |
| void AddObservations(std::string value, uint32_t encoding_config_id, |
| int num_clients) { |
| std::vector<Observation> observations; |
| for (int i = 0; i < num_clients; i++) { |
| observations.emplace_back(*MakeObservation(value, encoding_config_id)); |
| } |
| ObservationMetadata metadata; |
| metadata.set_customer_id(testing::kCustomerId); |
| metadata.set_project_id(testing::kProjectId); |
| metadata.set_metric_id(testing::kMetricId); |
| metadata.set_day_index(testing::kDayIndex); |
| EXPECT_EQ(store::kOK, |
| observation_store_->AddObservationBatch(metadata, observations)); |
| } |
| |
| struct GeneratedReport { |
| ReportMetadataLite metadata; |
| ReportRows rows; |
| }; |
| |
| // Uses the ReportGenerator to generate a HISTOGRAM report that analyzes the |
| // specified variable of our two-variable test metric. |variable_index| must |
| // be either 0 or 1. It will also be used for the sequence_num. |
| GeneratedReport GenerateHistogramReport(int variable_index) { |
| // Complete the report_id by specifying the sequence_num. |
| report_id_.set_sequence_num(variable_index); |
| |
| // Start a report for the specified variable, for the interval of days |
| // [kDayIndex, kDayIndex]. |
| EXPECT_EQ(store::kOK, |
| report_store_->StartNewReport( |
| testing::kDayIndex, testing::kDayIndex, true, HISTOGRAM, |
| {(uint32_t)variable_index}, &report_id_)); |
| |
| // Generate the report |
| EXPECT_TRUE(report_generator_->GenerateReport(report_id_).ok()); |
| |
| // Fetch the report from the ReportStore. |
| GeneratedReport report; |
| EXPECT_EQ(store::kOK, report_store_->GetReport(report_id_, &report.metadata, |
| &report.rows)); |
| |
| return report; |
| } |
| |
| // Adds to the ObservationStore a bunch of Observations of our test metric |
| // that use our test Forculus encoding config in which the Forculus threshold |
| // is 20. Each Observation is generated as if from a different client. |
| // We simulate 20 clients adding "hello", 19 clients adding "goodbye", and |
| // 21 clients adding "peace". Thus we expect "hello" and "peace" to appear |
| // in the generated report but not "goodybe". |
| void AddForculusObservations() { |
| // Add 20 copies of the Observation "hello" |
| AddObservations("hello", testing::kForculusEncodingConfigId, |
| testing::kForculusThreshold); |
| |
| // Add 19 copies of the Observation "goodbye" |
| AddObservations("goodbye", testing::kForculusEncodingConfigId, |
| testing::kForculusThreshold - 1); |
| |
| // Add 21 copies of the Observation "peace" |
| AddObservations("peace", testing::kForculusEncodingConfigId, |
| testing::kForculusThreshold + 1); |
| } |
| |
| // This method should be invoked after invoking AddForculusObservations() |
| // and then GenerateReport. It checks the generated Report to make sure |
| // it is correct given the Observations that were added and the Forculus |
| // config. |
| void CheckForculusReport(const GeneratedReport& report, uint variable_index) { |
| EXPECT_EQ(HISTOGRAM, report.metadata.report_type()); |
| EXPECT_EQ(1, report.metadata.variable_indices_size()); |
| EXPECT_EQ(variable_index, report.metadata.variable_indices(0)); |
| EXPECT_EQ(2, report.rows.rows_size()); |
| for (const auto& report_row : report.rows.rows()) { |
| EXPECT_EQ(0, report_row.histogram().std_error()); |
| ValuePart recovered_value; |
| EXPECT_TRUE(report_row.histogram().has_value()); |
| recovered_value = report_row.histogram().value(); |
| |
| EXPECT_EQ(ValuePart::kStringValue, recovered_value.data_case()); |
| std::string string_value = recovered_value.string_value(); |
| int count_estimate = report_row.histogram().count_estimate(); |
| switch (count_estimate) { |
| case 20: |
| EXPECT_EQ("hello", string_value); |
| break; |
| case 21: |
| EXPECT_EQ("peace", string_value); |
| break; |
| default: |
| FAIL(); |
| } |
| } |
| } |
| |
| // Adds to the ObservationStore a bunch of Observations of our test metric |
| // that use our test BasicRappor encoding config. We add 100 observations of |
| // "Apple", 200 observations of "Banana", and 300 observations of |
| // "Cantaloupe". |
| void AddBasicRapporObservations() { |
| AddObservations("Apple", testing::kBasicRapporEncodingConfigId, 100); |
| AddObservations("Banana", testing::kBasicRapporEncodingConfigId, 200); |
| AddObservations("Cantaloupe", testing::kBasicRapporEncodingConfigId, 300); |
| } |
| |
| // This method should be invoked after invoking AddBasicRapporObservations() |
| // and then GenerateReport. It checks the generated Report to make sure |
| // it is correct given the Observations that were added. We are not attempting |
| // to validate the Basic RAPPOR algorithm here so we simply test that the |
| // all three strings appear with a non-zero count and under the correct |
| // variable index. |
| void CheckBasicRapporReport(const GeneratedReport& report, |
| uint variable_index) { |
| EXPECT_EQ(HISTOGRAM, report.metadata.report_type()); |
| EXPECT_EQ(1, report.metadata.variable_indices_size()); |
| EXPECT_EQ(variable_index, report.metadata.variable_indices(0)); |
| EXPECT_EQ(3, report.rows.rows_size()); |
| for (const auto& report_row : report.rows.rows()) { |
| EXPECT_NE(0, report_row.histogram().std_error()); |
| ValuePart recovered_value; |
| EXPECT_TRUE(report_row.histogram().has_value()); |
| recovered_value = report_row.histogram().value(); |
| break; |
| |
| EXPECT_EQ(ValuePart::kStringValue, recovered_value.data_case()); |
| std::string string_value = recovered_value.string_value(); |
| EXPECT_TRUE(string_value == "Apple" || string_value == "Banana" || |
| string_value == "Cantaloupe"); |
| |
| EXPECT_GT(report_row.histogram().count_estimate(), 0); |
| } |
| } |
| |
| ReportId report_id_; |
| std::shared_ptr<encoder::ProjectContext> project_; |
| std::shared_ptr<store::DataStore> data_store_; |
| std::shared_ptr<store::ObservationStore> observation_store_; |
| std::shared_ptr<store::ReportStore> report_store_; |
| std::unique_ptr<ReportGenerator> report_generator_; |
| }; |
| |
| TYPED_TEST_CASE_P(ReportGeneratorAbstractTest); |
| |
| // Tests that the ReportGenerator correctly generates a report for both |
| // variables of our two-variable metric when the ObservationStroe has been |
| // filled with Observations of that metric that use our Forculus encoding. |
| // Note that *joint* reports have not yet been implemented. |
| TYPED_TEST_P(ReportGeneratorAbstractTest, Forculus) { |
| this->AddForculusObservations(); |
| { |
| SCOPED_TRACE("variable_index = 0"); |
| int variable_index = 0; |
| auto report = this->GenerateHistogramReport(variable_index); |
| this->CheckForculusReport(report, variable_index); |
| } |
| { |
| SCOPED_TRACE("variable_index = 1"); |
| int variable_index = 1; |
| auto report = this->GenerateHistogramReport(variable_index); |
| this->CheckForculusReport(report, variable_index); |
| } |
| } |
| |
| // Tests that the ReportGenerator correctly generates a report for both |
| // variables of our two-variable metric when the ObservationStroe has been |
| // filled with Observations of that metric that use our Basic RAPPOR encoding. |
| // Note that *joint* reports have not yet been implemented. |
| TYPED_TEST_P(ReportGeneratorAbstractTest, BasicRappor) { |
| this->AddBasicRapporObservations(); |
| { |
| SCOPED_TRACE("variable_index = 0"); |
| int variable_index = 0; |
| auto report = this->GenerateHistogramReport(variable_index); |
| this->CheckBasicRapporReport(report, variable_index); |
| } |
| { |
| SCOPED_TRACE("variable_index = 1"); |
| int variable_index = 1; |
| auto report = this->GenerateHistogramReport(variable_index); |
| this->CheckBasicRapporReport(report, variable_index); |
| } |
| } |
| |
| REGISTER_TYPED_TEST_CASE_P(ReportGeneratorAbstractTest, Forculus, BasicRappor); |
| |
| } // namespace analyzer |
| } // namespace cobalt |
| |
| #endif // COBALT_ANALYZER_REPORT_MASTER_REPORT_GENERATOR_ABSTRACT_TEST_H_ |