| // 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. |
| |
| #include "analyzer/report_master/histogram_analysis_engine.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "./observation.pb.h" |
| #include "config/config_text_parser.h" |
| #include "encoder/client_secret.h" |
| #include "encoder/encoder.h" |
| #include "encoder/project_context.h" |
| #include "gflags/gflags.h" |
| #include "glog/logging.h" |
| #include "third_party/googletest/googletest/include/gtest/gtest.h" |
| |
| namespace cobalt { |
| namespace analyzer { |
| |
| using config::AnalyzerConfig; |
| using config::EncodingRegistry; |
| using config::MetricRegistry; |
| using config::ReportRegistry; |
| using encoder::ClientSecret; |
| using encoder::Encoder; |
| using encoder::ProjectContext; |
| |
| namespace { |
| |
| const uint32_t kCustomerId = 1; |
| const uint32_t kProjectId = 1; |
| const uint32_t kStringMetricId = 1; |
| const uint32_t kIndexMetricId = 2; |
| const uint32_t kIntBucketsMetricId = 3; |
| const uint32_t kForculusEncodingConfigId = 1; |
| const uint32_t kBasicRapporStringEncodingConfigId = 2; |
| const uint32_t kBasicRapporIndexEncodingConfigId = 3; |
| const uint32_t kStringRapporEncodingConfigId = 4; |
| const uint32_t kNoOpEncodingConfigId = 5; |
| const uint32_t kStringReportConfigId = 1; |
| const uint32_t kIndexReportConfigId = 2; |
| const uint32_t kIntBucketsReportConfigId = 3; |
| const uint32_t kGroupedStringReportConfigId = 4; |
| const char kPartName[] = "Part1"; |
| 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 one string part. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 1 |
| time_zone_policy: UTC |
| parts { |
| key: "Part1" |
| value { |
| } |
| } |
| } |
| |
| # Metric 2 has one INDEX part. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 2 |
| time_zone_policy: UTC |
| parts { |
| key: "Part1" |
| value { |
| data_type: INDEX |
| } |
| } |
| } |
| |
| # Metric 3 has one INT part with linear buckets. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 3 |
| time_zone_policy: UTC |
| parts { |
| key: "Part1" |
| value { |
| data_type: INT |
| int_buckets: { |
| linear: { |
| floor: 0 |
| num_buckets: 5 |
| step_size: 10 |
| } |
| } |
| } |
| } |
| } |
| |
| )"; |
| |
| 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 with string categories. |
| 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" |
| } |
| } |
| } |
| |
| # EncodingConfig 3 is Basic RAPPOR with indexed categories. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 3 |
| basic_rappor { |
| prob_0_becomes_1: 0.0 |
| prob_1_stays_1: 1.0 |
| indexed_categories: { |
| num_categories: 100 |
| } |
| } |
| } |
| |
| # EncodingConfig 4 is String RAPPOR with no randomness. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 4 |
| rappor { |
| prob_0_becomes_1: 0.0 |
| prob_1_stays_1: 1.0 |
| num_bloom_bits: 8 |
| num_hashes: 2 |
| num_cohorts: 2 |
| } |
| } |
| |
| # EncodingConfig 5 is the No-Op encoding. |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 5 |
| no_op_encoding { |
| } |
| } |
| |
| )"; |
| |
| const char* kReportConfigText = R"( |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 1 |
| metric_id: 1 |
| variable { |
| metric_part: "Part1" |
| rappor_candidates { |
| candidates: "Apple" |
| candidates: "Banana" |
| candidates: "Cantaloupe" |
| } |
| } |
| } |
| |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 2 |
| metric_id: 2 |
| variable { |
| metric_part: "Part1" |
| index_labels { |
| labels { |
| key: 0 |
| value: "Event A" |
| } |
| labels { |
| key: 1 |
| value: "Event B" |
| } |
| labels { |
| key: 5 |
| value: "Event F" |
| } |
| labels { |
| key: 25 |
| value: "Event Z" |
| } |
| } |
| } |
| } |
| |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 3 |
| metric_id: 3 |
| variable { |
| metric_part: "Part1" |
| } |
| } |
| |
| element { |
| customer_id: 1 |
| project_id: 1 |
| id: 4 |
| metric_id: 1 |
| variable { |
| metric_part: "Part1" |
| rappor_candidates { |
| candidates: "Apple" |
| candidates: "Banana" |
| candidates: "Cantaloupe" |
| } |
| } |
| system_profile_field: [BOARD_NAME] |
| } |
| |
| )"; |
| |
| } // namespace |
| |
| class HistogramAnalysisEngineTest : public ::testing::Test { |
| protected: |
| void Init(uint32_t report_config_id) { |
| report_id_.set_customer_id(kCustomerId); |
| report_id_.set_project_id(kProjectId); |
| report_id_.set_report_config_id(report_config_id); |
| |
| // Parse the metric config string |
| auto metric_parse_result = |
| config::FromString<RegisteredMetrics>(kMetricConfigText, nullptr); |
| EXPECT_EQ(config::kOK, metric_parse_result.second); |
| std::shared_ptr<MetricRegistry> metric_registry( |
| metric_parse_result.first.release()); |
| |
| // Parse the encoding config string |
| auto encoding_parse_result = |
| config::FromString<RegisteredEncodings>(kEncodingConfigText, nullptr); |
| EXPECT_EQ(config::kOK, encoding_parse_result.second); |
| std::shared_ptr<EncodingRegistry> encoding_registry( |
| (encoding_parse_result.first.release())); |
| |
| // Parse the report config string |
| auto report_parse_result = |
| config::FromString<RegisteredReports>(kReportConfigText, nullptr); |
| EXPECT_EQ(config::kOK, report_parse_result.second); |
| report_registry_.reset((report_parse_result.first.release())); |
| |
| project_.reset(new ProjectContext(kCustomerId, kProjectId, metric_registry, |
| encoding_registry)); |
| |
| std::shared_ptr<AnalyzerConfig> analyzer_config(new AnalyzerConfig( |
| encoding_registry, metric_registry, report_registry_)); |
| |
| // Extract the ReportVariable from the ReportConfig. |
| const auto* report_config = |
| report_registry_->Get(kCustomerId, kProjectId, report_config_id); |
| EXPECT_NE(nullptr, report_config); |
| const ReportVariable* report_variable = &(report_config->variable(0)); |
| EXPECT_NE(nullptr, report_variable); |
| |
| const Metric* metric = analyzer_config->Metric(report_config->customer_id(), |
| report_config->project_id(), |
| report_config->metric_id()); |
| EXPECT_NE(nullptr, metric); |
| const MetricPart* metric_part = |
| &(metric->parts().at(report_variable->metric_part())); |
| EXPECT_NE(nullptr, metric_part); |
| |
| analysis_engine_.reset(new HistogramAnalysisEngine( |
| report_id_, report_variable, metric_part, analyzer_config)); |
| } |
| |
| // Makes an Observation with one string part which has the given |
| // |string_value|, using the encoding with the given encoding_config_id. |
| std::unique_ptr<Observation> MakeStringObservation( |
| std::string string_value, uint32_t encoding_config_id) { |
| // Construct a new Encoder with a new client secret. |
| Encoder encoder(project_, ClientSecret::GenerateNewSecret()); |
| // Set a static current time so we know we have a static day_index. |
| encoder.set_current_time(kSomeTimestamp); |
| |
| // Encode an observation. |
| Encoder::Result result = |
| encoder.EncodeString(kStringMetricId, encoding_config_id, string_value); |
| EXPECT_EQ(Encoder::kOK, result.status); |
| EXPECT_TRUE(result.observation.get() != nullptr); |
| EXPECT_EQ(1, result.observation->parts_size()); |
| return std::move(result.observation); |
| } |
| |
| // Makes an Observation with one INDEX part which has the given |
| // |index| value, using the encoding with the given encoding_config_id. |
| std::unique_ptr<Observation> MakeIndexObservation( |
| uint32_t index, uint32_t encoding_config_id) { |
| // Construct a new Encoder with a new client secret. |
| Encoder encoder(project_, ClientSecret::GenerateNewSecret()); |
| // Set a static current time so we know we have a static day_index. |
| encoder.set_current_time(kSomeTimestamp); |
| |
| // Encode an observation. |
| Encoder::Result result = |
| encoder.EncodeIndex(kIndexMetricId, encoding_config_id, index); |
| EXPECT_EQ(Encoder::kOK, result.status); |
| EXPECT_TRUE(result.observation.get() != nullptr); |
| EXPECT_EQ(1, result.observation->parts_size()); |
| return std::move(result.observation); |
| } |
| |
| // Makes an Observation with one INT part which has the given |value|, using |
| // the encoding with the given encoding_config_id for the bucketed int metric. |
| std::unique_ptr<Observation> MakeBucketedIntObservation( |
| int64_t value, uint32_t encoding_config_id) { |
| // Construct a new Encoder with a new client secret. |
| Encoder encoder(project_, ClientSecret::GenerateNewSecret()); |
| // Set a static current time so we know we have a static day_index. |
| encoder.set_current_time(kSomeTimestamp); |
| |
| // Encode an observation. |
| Encoder::Result result = |
| encoder.EncodeInt(kIntBucketsMetricId, encoding_config_id, value); |
| EXPECT_EQ(Encoder::kOK, result.status); |
| EXPECT_TRUE(result.observation.get() != nullptr); |
| EXPECT_EQ(1, result.observation->parts_size()); |
| return std::move(result.observation); |
| } |
| |
| // Makes an Observation with one IntBucketDistribution part which has the |
| // given |counts| value, using the encoding with the given encoding_config_id |
| // for the bucketed int metric. |
| std::unique_ptr<Observation> MakeIntBucketDistributionObservation( |
| const std::map<uint32_t, uint64_t>& counts, uint32_t encoding_config_id) { |
| // Construct a new Encoder with a new client secret. |
| Encoder encoder(project_, ClientSecret::GenerateNewSecret()); |
| // Set a static current time so we know we have a static day_index. |
| encoder.set_current_time(kSomeTimestamp); |
| |
| // Encode an observation. |
| Encoder::Result result = encoder.EncodeIntBucketDistribution( |
| kIntBucketsMetricId, encoding_config_id, counts); |
| EXPECT_EQ(Encoder::kOK, result.status); |
| EXPECT_TRUE(result.observation.get() != nullptr); |
| EXPECT_EQ(1, result.observation->parts_size()); |
| return std::move(result.observation); |
| } |
| |
| // Makes an Observation with one string part which has the given |
| // |string_value|, using the encoding with the given encoding_config_id. |
| // Then passes the ObservationPart into |
| // HistogramAnalysisEngine::ProcessObservationPart(). |
| bool MakeAndProcessStringObservationPart(std::string string_value, |
| uint32_t encoding_config_id) { |
| return MakeAndProcessStringObservationPart( |
| string_value, encoding_config_id, std::make_unique<SystemProfile>()); |
| } |
| bool MakeAndProcessStringObservationPart( |
| std::string string_value, uint32_t encoding_config_id, |
| std::unique_ptr<SystemProfile> profile) { |
| std::unique_ptr<Observation> observation = |
| MakeStringObservation(string_value, encoding_config_id); |
| return analysis_engine_->ProcessObservationPart( |
| kDayIndex, observation->parts().at(kPartName), std::move(profile)); |
| } |
| |
| // Makes an Observation with one INDEX part which has the given |
| // |index| value, using the encoding with the given encoding_config_id. |
| // Then passes the ObservationPart into |
| // HistogramAnalysisEngine::ProcessObservationPart(). |
| bool MakeAndProcessIndexObservationPart(uint32_t index, |
| uint32_t encoding_config_id) { |
| return MakeAndProcessIndexObservationPart( |
| index, encoding_config_id, std::make_unique<SystemProfile>()); |
| } |
| bool MakeAndProcessIndexObservationPart( |
| uint32_t index, uint32_t encoding_config_id, |
| std::unique_ptr<SystemProfile> profile) { |
| std::unique_ptr<Observation> observation = |
| MakeIndexObservation(index, encoding_config_id); |
| return analysis_engine_->ProcessObservationPart( |
| kDayIndex, observation->parts().at(kPartName), std::move(profile)); |
| } |
| |
| // Makes an Observation with one INT part which has the given |value|, using |
| // the encoding with the given encoding_config_id for the bucketed int metric. |
| // Then passes the ObservationPart into |
| // HistogramAnalysisEngine::ProcessObservationPart(). |
| bool MakeAndProcessBucketedIntObservationPart(int64_t value, |
| uint32_t encoding_config_id) { |
| return MakeAndProcessBucketedIntObservationPart( |
| value, encoding_config_id, std::make_unique<SystemProfile>()); |
| } |
| bool MakeAndProcessBucketedIntObservationPart( |
| int64_t value, uint32_t encoding_config_id, |
| std::unique_ptr<SystemProfile> profile) { |
| std::unique_ptr<Observation> observation = |
| MakeBucketedIntObservation(value, encoding_config_id); |
| return analysis_engine_->ProcessObservationPart( |
| kDayIndex, observation->parts().at(kPartName), std::move(profile)); |
| } |
| |
| // Makes an Observation with one IntBucketDistribution part which has the |
| // given |counts| value, using the encoding with the given encoding_config_id |
| // for the bucketed int metric. |
| // Then passes the ObservationPart into |
| // HistogramAnalysisEngine::ProcessObservationPart(). |
| bool MakeAndProcessIntBucketDistributionObservationPart( |
| const std::map<uint32_t, uint64_t>& counts, uint32_t encoding_config_id) { |
| return MakeAndProcessIntBucketDistributionObservationPart( |
| counts, encoding_config_id, std::make_unique<SystemProfile>()); |
| } |
| bool MakeAndProcessIntBucketDistributionObservationPart( |
| const std::map<uint32_t, uint64_t>& counts, uint32_t encoding_config_id, |
| std::unique_ptr<SystemProfile> profile) { |
| std::unique_ptr<Observation> observation = |
| MakeIntBucketDistributionObservation(counts, encoding_config_id); |
| return analysis_engine_->ProcessObservationPart( |
| kDayIndex, observation->parts().at(kPartName), std::move(profile)); |
| } |
| |
| // Invokes MakeAndProcessStringObservationPart many times using the Forculus |
| // encoding. Three strings are encoded: "hello" 20 times, |
| // "goodbye" 19 times and "peace" 21 times. The first and third will be |
| // decrypted by Forculus and the 2nd will not. |
| void MakeAndProcessForculusObservations() { |
| // Add the word "hello" 20 times. |
| for (size_t i = 0; i < kForculusThreshold; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "hello", kForculusEncodingConfigId)); |
| } |
| // Add the word "goodbye" 19 times. |
| for (size_t i = 0; i < kForculusThreshold - 1; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "goodbye", kForculusEncodingConfigId)); |
| } |
| // Add the word "peace" 21 times. |
| for (size_t i = 0; i < kForculusThreshold + 1; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "peace", kForculusEncodingConfigId)); |
| } |
| } |
| |
| // Tests the HistogramAnalysisEngine when it is used on a homogeneous set of |
| // Observations, all of which were encoded using the same Forculus |
| // EncodingCofig. |
| void DoForculusTest() { |
| Init(kStringReportConfigId); |
| MakeAndProcessForculusObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check the results. |
| EXPECT_EQ(2u, report_rows.size()); |
| for (const auto& report_row : report_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(); |
| } |
| } |
| } |
| |
| // Invokes MakeAndProcessStringObservationPart many times using the |
| // BasicRappor encoding. |
| void MakeAndProcessBasicRapporStringObservations() { |
| for (int i = 0; i < 100; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Apple", kBasicRapporStringEncodingConfigId)); |
| } |
| for (int i = 0; i < 200; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Banana", kBasicRapporStringEncodingConfigId)); |
| } |
| for (int i = 0; i < 300; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Cantaloupe", kBasicRapporStringEncodingConfigId)); |
| } |
| } |
| |
| void DoBasicRapporStringTest() { |
| Init(kStringReportConfigId); |
| MakeAndProcessBasicRapporStringObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check the results. |
| EXPECT_EQ(3u, report_rows.size()); |
| for (const auto& report_row : report_rows) { |
| EXPECT_NE(0, report_row.histogram().std_error()); |
| EXPECT_GT(report_row.histogram().count_estimate(), 0); |
| 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(); |
| EXPECT_TRUE(string_value == "Apple" || string_value == "Banana" || |
| string_value == "Cantaloupe"); |
| } |
| } |
| |
| // Invokes MakeAndProcessIndexObservationPart several times: Once for |
| // index 0, twice for index 1, ... 10 times for index 9. |
| void MakeAndProcessBasicRapporIndexObservations() { |
| for (int index = 0; index < 10; index++) { |
| for (int count = 1; count <= index + 1; count++) |
| EXPECT_TRUE(MakeAndProcessIndexObservationPart( |
| index, kBasicRapporIndexEncodingConfigId)); |
| } |
| } |
| |
| // Tests HistogramAnalysisEngine in the case where it performs a Basic RAPPROR |
| // report using Observations of type INDEX. We test that the correct |
| // human-readable labels from the ReportConfig are applied to the correct |
| // report rows. |
| void DoBasicRapporIndexTest() { |
| Init(kIndexReportConfigId); |
| MakeAndProcessBasicRapporIndexObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check the results. |
| EXPECT_EQ(100u, report_rows.size()); |
| for (const auto& report_row : report_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::kIndexValue, recovered_value.data_case()); |
| uint32_t index = recovered_value.index_value(); |
| if (index > 9) { |
| // We did not add any Observations for indices > 9. |
| EXPECT_EQ(report_row.histogram().count_estimate(), 0); |
| if (index == 25) { |
| // Index 25 should be associated with the label 'Event Z'. |
| EXPECT_EQ("Event Z", report_row.histogram().label()); |
| } else { |
| EXPECT_EQ("", report_row.histogram().label()); |
| } |
| } else { |
| // For indices i=0..9 we added i+1 Observations. |
| EXPECT_EQ(report_row.histogram().count_estimate(), index + 1); |
| // We added labels for indices 0, 1 and 5 but not the others. |
| switch (index) { |
| case 0: { |
| EXPECT_EQ("Event A", report_row.histogram().label()); |
| break; |
| } |
| case 1: { |
| EXPECT_EQ("Event B", report_row.histogram().label()); |
| break; |
| } |
| case 5: { |
| EXPECT_EQ("Event F", report_row.histogram().label()); |
| break; |
| } |
| default: { EXPECT_EQ("", report_row.histogram().label()); } |
| } |
| } |
| } |
| } |
| |
| // Invokes MakeAndProcessStringObservationPart many times using the |
| // StringRappor encoding. |
| void MakeAndProcessStringRapporObservations() { |
| for (int i = 0; i < 100; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Apple", kStringRapporEncodingConfigId)); |
| } |
| for (int i = 0; i < 200; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Banana", kStringRapporEncodingConfigId)); |
| } |
| for (int i = 0; i < 300; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Cantaloupe", kStringRapporEncodingConfigId)); |
| } |
| } |
| |
| void DoStringRapporTest() { |
| Init(kStringReportConfigId); |
| MakeAndProcessStringRapporObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| auto status = analysis_engine_->PerformAnalysis(&report_rows).error_code(); |
| if (grpc::OK != status) { |
| EXPECT_EQ(grpc::OK, status); |
| return; |
| } |
| |
| // Check the results. |
| if (3u != report_rows.size()) { |
| EXPECT_EQ(3u, report_rows.size()); |
| return; |
| } |
| for (const auto& report_row : report_rows) { |
| // In String RAPPOR we currently do not implement std_error. |
| EXPECT_EQ(0, report_row.histogram().std_error()); |
| EXPECT_GT(report_row.histogram().count_estimate(), 0); |
| 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(); |
| EXPECT_TRUE(string_value == "Apple" || string_value == "Banana" || |
| string_value == "Cantaloupe"); |
| } |
| } |
| |
| std::unique_ptr<SystemProfile> MakeProfile(std::string board_name) { |
| auto profile = std::make_unique<SystemProfile>(); |
| *profile->mutable_board_name() = board_name; |
| return profile; |
| } |
| |
| void MakeAndProcessGroupedStringRapporObservations() { |
| for (int i = 0; i < 50; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Apple", kStringRapporEncodingConfigId, MakeProfile("foo"))); |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Apple", kStringRapporEncodingConfigId, MakeProfile("bar"))); |
| } |
| |
| for (int i = 0; i < 100; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Banana", kStringRapporEncodingConfigId, MakeProfile("foo"))); |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Banana", kStringRapporEncodingConfigId, MakeProfile("bar"))); |
| } |
| |
| for (int i = 0; i < 150; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Cantaloupe", kStringRapporEncodingConfigId, MakeProfile("foo"))); |
| EXPECT_TRUE(MakeAndProcessStringObservationPart( |
| "Cantaloupe", kStringRapporEncodingConfigId, MakeProfile("bar"))); |
| } |
| } |
| |
| void DoGroupedStringRapporTest() { |
| Init(kGroupedStringReportConfigId); |
| MakeAndProcessGroupedStringRapporObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| auto status = analysis_engine_->PerformAnalysis(&report_rows).error_code(); |
| if (grpc::OK != status) { |
| EXPECT_EQ(grpc::OK, status); |
| return; |
| } |
| |
| if (6u != report_rows.size()) { |
| EXPECT_EQ(6u, report_rows.size()); |
| return; |
| } |
| |
| int foo_count = 0; |
| int bar_count = 0; |
| for (const auto& report_row : report_rows) { |
| // In String RAPPOR we currently do not implement std_error. |
| EXPECT_EQ(0, report_row.histogram().std_error()); |
| EXPECT_GT(report_row.histogram().count_estimate(), 0); |
| 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(); |
| EXPECT_TRUE(string_value == "Apple" || string_value == "Banana" || |
| string_value == "Cantaloupe") |
| << string_value; |
| std::string board_name = |
| report_row.histogram().system_profile().board_name(); |
| if (board_name == "foo") foo_count += 1; |
| if (board_name == "bar") bar_count += 1; |
| } |
| EXPECT_EQ(3, foo_count); |
| EXPECT_EQ(3, bar_count); |
| } |
| |
| // Invokes MakeAndProcessStringObservationPart many times using the NoOp |
| // encoding. Three strings are encoded: "hello" 20 times, |
| // "goodbye" 19 times and "peace" 21 times. |
| void MakeAndProcessUnencodedStringObservations() { |
| // Add the word "hello" 20 times. |
| for (size_t i = 0; i < 20; i++) { |
| EXPECT_TRUE( |
| MakeAndProcessStringObservationPart("hello", kNoOpEncodingConfigId)); |
| } |
| // Add the word "goodbye" 19 times. |
| for (size_t i = 0; i < 19; i++) { |
| EXPECT_TRUE(MakeAndProcessStringObservationPart("goodbye", |
| kNoOpEncodingConfigId)); |
| } |
| // Add the word "peace" 21 times. |
| for (size_t i = 0; i < 21; i++) { |
| EXPECT_TRUE( |
| MakeAndProcessStringObservationPart("peace", kNoOpEncodingConfigId)); |
| } |
| } |
| |
| // Tests the HistogramAnalysisEngine when it is used on a homogeneous set of |
| // Observations, all of which were encoded using the same NoOp |
| // EncodingConfig. |
| void DoUnencodedStringTest() { |
| Init(kStringReportConfigId); |
| MakeAndProcessUnencodedStringObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check the results. |
| EXPECT_EQ(3u, report_rows.size()); |
| for (const auto& report_row : report_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 19: |
| EXPECT_EQ("goodbye", string_value); |
| break; |
| case 20: |
| EXPECT_EQ("hello", string_value); |
| break; |
| case 21: |
| EXPECT_EQ("peace", string_value); |
| break; |
| default: |
| FAIL() << "Unexpected count for value " << string_value << ": " |
| << count_estimate; |
| } |
| } |
| } |
| |
| // Invokes MakeAndProcessIndexObservationPart several times: Once for |
| // index 0, twice for index 1, ... 10 times for index 9, all using the |
| // ID of the NoOp encoding. |
| void MakeAndProcessUnencodedIndexObservations() { |
| for (int index = 0; index < 10; index++) { |
| for (int count = 1; count <= index + 1; count++) |
| EXPECT_TRUE( |
| MakeAndProcessIndexObservationPart(index, kNoOpEncodingConfigId)); |
| } |
| } |
| |
| // Tests HistogramAnalysisEngine in the case where it performs a NoOp |
| // report using Observations of type INT and with bucketing enabled. |
| void DoUnencodedIntBucketsTest() { |
| Init(kIntBucketsReportConfigId); |
| |
| // We add a mix of distributions and individual integer observations. |
| std::map<uint32_t, uint64_t> distribution1 = { |
| {0, 6}, {1, 9}, {2, 2}, {3, 7}, {4, 3}, {5, 1}, {6, 7}}; |
| MakeAndProcessIntBucketDistributionObservationPart(distribution1, |
| kNoOpEncodingConfigId); |
| MakeAndProcessBucketedIntObservationPart(-10, kNoOpEncodingConfigId); |
| MakeAndProcessBucketedIntObservationPart(0, kNoOpEncodingConfigId); |
| MakeAndProcessBucketedIntObservationPart(10, kNoOpEncodingConfigId); |
| MakeAndProcessBucketedIntObservationPart(6000, kNoOpEncodingConfigId); |
| std::map<uint32_t, uint64_t> distribution2 = { |
| {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}}; |
| MakeAndProcessIntBucketDistributionObservationPart(distribution2, |
| kNoOpEncodingConfigId); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check that the results are as expected. |
| std::map<uint32_t, uint64_t> expected = {{0, 8}, {1, 12}, {2, 6}, {3, 11}, |
| {4, 8}, {5, 7}, {6, 15}}; |
| |
| EXPECT_EQ(7u, report_rows.size()); |
| for (const auto& report_row : report_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::kIndexValue, recovered_value.data_case()); |
| uint32_t index = recovered_value.index_value(); |
| EXPECT_EQ(expected[index], report_row.histogram().count_estimate()) |
| << index; |
| // TODO(azani): Check the labels. |
| } |
| } |
| |
| // Tests HistogramAnalysisEngine in the case where it performs a NoOp |
| // report using Observations of type INDEX. We test that the correct |
| // human-readable labels from the ReportConfig are applied to the correct |
| // report rows. |
| void DoUnencodedIndexTest() { |
| Init(kIndexReportConfigId); |
| MakeAndProcessUnencodedIndexObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_TRUE(analysis_engine_->PerformAnalysis(&report_rows).ok()); |
| |
| // Check the results. |
| EXPECT_EQ(10u, report_rows.size()); |
| for (const auto& report_row : report_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::kIndexValue, recovered_value.data_case()); |
| uint32_t index = recovered_value.index_value(); |
| EXPECT_TRUE(index >= 0 && index <= 9) << index; |
| // For indices i=0..9 we added i+1 Observations. |
| EXPECT_EQ(report_row.histogram().count_estimate(), index + 1); |
| // We added labels for indices 0, 1 and 5 but not the others. |
| switch (index) { |
| case 0: { |
| EXPECT_EQ("Event A", report_row.histogram().label()); |
| break; |
| } |
| case 1: { |
| EXPECT_EQ("Event B", report_row.histogram().label()); |
| break; |
| } |
| case 5: { |
| EXPECT_EQ("Event F", report_row.histogram().label()); |
| break; |
| } |
| default: { EXPECT_EQ("", report_row.histogram().label()); } |
| } |
| } |
| } |
| |
| void DoMixedEncodingTest() { |
| Init(kStringReportConfigId); |
| MakeAndProcessForculusObservations(); |
| MakeAndProcessBasicRapporStringObservations(); |
| |
| // Perform the analysis. |
| std::vector<ReportRow> report_rows; |
| EXPECT_EQ(grpc::UNIMPLEMENTED, |
| analysis_engine_->PerformAnalysis(&report_rows).error_code()); |
| } |
| |
| ReportId report_id_; |
| std::shared_ptr<ProjectContext> project_; |
| std::shared_ptr<ReportRegistry> report_registry_; |
| std::unique_ptr<HistogramAnalysisEngine> analysis_engine_; |
| }; |
| |
| TEST_F(HistogramAnalysisEngineTest, Forculus) { DoForculusTest(); } |
| |
| TEST_F(HistogramAnalysisEngineTest, BasicRapporString) { |
| DoBasicRapporStringTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, BasicRapporIndex) { |
| DoBasicRapporIndexTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, StringRappor) { DoStringRapporTest(); } |
| |
| TEST_F(HistogramAnalysisEngineTest, GroupedStringRappor) { |
| DoGroupedStringRapporTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, UnencodedStrings) { |
| DoUnencodedStringTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, UnencodedIndices) { |
| DoUnencodedIndexTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, UnencodedIntBuckets) { |
| DoUnencodedIntBucketsTest(); |
| } |
| |
| TEST_F(HistogramAnalysisEngineTest, MixedEncoding) { DoMixedEncodingTest(); } |
| |
| } // namespace analyzer |
| } // namespace cobalt |
| |
| int main(int argc, char** argv) { |
| ::testing::InitGoogleTest(&argc, argv); |
| google::InitGoogleLogging(argv[0]); |
| google::ParseCommandLineFlags(&argc, &argv, true); |
| return RUN_ALL_TESTS(); |
| } |