blob: 503a58aac96a0a10ff705b4592488d515b907bb7 [file] [log] [blame]
// 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_EXECUTOR_ABSTRACT_TEST_H_
#define COBALT_ANALYZER_REPORT_MASTER_REPORT_EXECUTOR_ABSTRACT_TEST_H_
#include "analyzer/report_master/report_executor.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#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 "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 {
static const uint32_t kCustomerId = 1;
static const uint32_t kProjectId = 1;
static const uint32_t kMetricId1 = 1;
static const uint32_t kMetricId2 = 2;
static const uint32_t kReportConfigId1 = 1;
static const uint32_t kReportConfigId2 = 2;
static const uint32_t kForculusEncodingConfigId = 1;
static const uint32_t kBasicRapporStringEncodingConfigId = 2;
static const uint32_t kBasicRapporIntEncodingConfigId = 3;
static const char kPartName1[] = "Part1";
static const char kPartName2[] = "Part2";
static const size_t kForculusThreshold = 20;
// This unix timestamp corresponds to Friday Dec 2, 2016 in UTC
static const time_t kSomeTimestamp = 1480647356;
// This is the day index for Friday Dec 2, 2016
static const uint32_t kDayIndex = 17137;
static const char* kMetricConfigText = R"(
# Metric 1 has one string part and one integer part.
element {
customer_id: 1
project_id: 1
id: 1
time_zone_policy: UTC
parts {
key: "Part1"
value {
}
}
parts {
key: "Part2"
value {
data_type: INT
}
}
}
# Metric 2 has one string part and one integer part.
element {
customer_id: 1
project_id: 1
id: 2
time_zone_policy: UTC
parts {
key: "Part1"
value {
}
}
parts {
key: "Part2"
value {
data_type: INT
}
}
}
)";
static 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 candidates (non-stochastic)
element {
customer_id: 1
project_id: 1
id: 2
basic_rappor {
prob_0_becomes_1: 0.0
prob_1_stays_1: 1.0
string_categories: {
category: "Apple"
category: "Banana"
category: "Cantaloupe"
}
}
}
# EncodingConfig 3 is Basic RAPPOR with integer candidates (non-stochastic).
element {
customer_id: 1
project_id: 1
id: 3
basic_rappor {
prob_0_becomes_1: 0.0
prob_1_stays_1: 1.0
int_range_categories: {
first: 1
last: 10
}
}
}
)";
static 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"
}
}
# ReportConfig 2 specifies a report of both variables of Metric 2.
element {
customer_id: 1
project_id: 1
id: 2
metric_id: 2
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
}
)";
// ReportExecutorAbstractTest 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 store/memory_store_test_helper.h and
// BigtableStoreEmulatorFactory in store/bigtable_emulator_helper.h.
template <class StoreFactoryClass>
class ReportExecutorAbstractTest : public ::testing::Test {
protected:
ReportExecutorAbstractTest()
: data_store_(StoreFactoryClass::NewStore()),
observation_store_(new store::ObservationStore(data_store_)),
report_store_(new store::ReportStore(data_store_)) {
report_id1_.set_customer_id(kCustomerId);
report_id1_.set_project_id(kProjectId);
report_id1_.set_report_config_id(kReportConfigId1);
report_id2_.set_customer_id(kCustomerId);
report_id2_.set_project_id(kProjectId);
report_id2_.set_report_config_id(kReportConfigId2);
}
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::FromString<RegisteredMetrics>(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::FromString<RegisteredEncodings>(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::FromString<RegisteredReports>(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(
kCustomerId, kProjectId, metric_registry, encoding_config_registry));
std::shared_ptr<config::AnalyzerConfig> analyzer_config(
new config::AnalyzerConfig(encoding_config_registry, metric_registry,
report_config_registry));
std::shared_ptr<config::AnalyzerConfigManager> analyzer_config_manager(
new config::AnalyzerConfigManager(analyzer_config));
// Make a ReportGenerator
std::unique_ptr<ReportGenerator> report_generator(new ReportGenerator(
analyzer_config_manager, observation_store_, report_store_, nullptr));
// Make a ReportExecutor
report_executor_.reset(
new ReportExecutor(report_store_, std::move(report_generator)));
}
// Makes an Observation with one string part and one int part, using the
// two given values and the two given encodings for the given metric.
std::unique_ptr<Observation> MakeObservation(std::string part1_value,
int part2_value,
uint32_t metric_id,
uint32_t encoding_config_id1,
uint32_t encoding_config_id2) {
// 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(kSomeTimestamp);
// Construct the two-part value to add.
encoder::Encoder::Value value;
value.AddStringPart(encoding_config_id1, kPartName1, part1_value);
value.AddIntPart(encoding_config_id2, kPartName2, part2_value);
// Encode an observation.
encoder::Encoder::Result result = encoder.Encode(metric_id, 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| two-part observations that each
// encode the given two values using the given metric and the
// given two encodings. Each Observation is generated as if from a different
// client.
void AddObservations(std::string part1_value, int part2_value,
uint32_t metric_id, uint32_t encoding_config_id1,
uint32_t encoding_config_id2, int num_clients) {
std::vector<Observation> observations;
for (int i = 0; i < num_clients; i++) {
observations.emplace_back(*MakeObservation(part1_value, part2_value,
metric_id, encoding_config_id1,
encoding_config_id2));
}
ObservationMetadata metadata;
metadata.set_customer_id(kCustomerId);
metadata.set_project_id(kProjectId);
metadata.set_metric_id(metric_id);
metadata.set_day_index(kDayIndex);
EXPECT_EQ(store::kOK,
observation_store_->AddObservationBatch(metadata, observations));
}
// Checks that the report with the given ID completed successfully and has
// the expected number of rows.
void CheckReport(const ReportId report_id, int expected_num_rows) {
ReportMetadataLite metadata;
ReportRows rows;
ASSERT_EQ(store::kOK, report_store_->GetReport(report_id, &metadata, &rows))
<< "report_id=" << store::ReportStore::ToString(report_id);
ASSERT_EQ(COMPLETED_SUCCESSFULLY, metadata.state())
<< "report_id=" << store::ReportStore::ToString(report_id);
EXPECT_EQ(expected_num_rows, rows.rows_size())
<< "report_id=" << store::ReportStore::ToString(report_id);
}
ReportId report_id1_, report_id2_;
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<ReportExecutor> report_executor_;
};
TYPED_TEST_CASE_P(ReportExecutorAbstractTest);
// We load up the ObservationStore with observations for our two metrics.
// Then we start a ReportExecutor and invoke EnqueueReportGeneration() on
// two dependency chains of reports-- one for parts 1 and 2 of ReportConfig 1
// and one for parts 1 and 2 of ReportConfig 2. We expect 4 reports to
// complete successfully and contain the expected number of rows.
TYPED_TEST_P(ReportExecutorAbstractTest, EnqueueReportGeneration) {
// Add some observations for metric 1. We use Basic RAPPOR for
// both parts.
this->AddObservations("Apple", 10, kMetricId1,
kBasicRapporStringEncodingConfigId,
kBasicRapporIntEncodingConfigId, 20);
// Add some observations for metric 2. We use Forculus for part 1
// and BasicRappor for part 2. For the Forculus part there will be
// 20 observations of "Apple" but only 19 observations of "Banana" so
// we expect to see only Apple in the report.
this->AddObservations("Apple", 10, kMetricId2, kForculusEncodingConfigId,
kBasicRapporIntEncodingConfigId, kForculusThreshold);
this->AddObservations("Banana", 10, kMetricId2, kForculusEncodingConfigId,
kBasicRapporIntEncodingConfigId,
kForculusThreshold - 1);
// Register the start of report 1, sequence_num 0, variable 0.
ReportId report_id11 = this->report_id1_;
this->report_store_->StartNewReport(kDayIndex, kDayIndex, true, "", true,
HISTOGRAM, {0}, &report_id11);
// Register the creation of report1, sequence_num 1, variable 1.
ReportId report_id12 = report_id11;
this->report_store_->CreateDependentReport(1, "", true, HISTOGRAM, {1},
&report_id12);
// Register the start of report2, sequence_num 0, variable 0.
ReportId report_id21 = this->report_id2_;
this->report_store_->StartNewReport(kDayIndex, kDayIndex, true, "", true,
HISTOGRAM, {0}, &report_id21);
// Register the creation of report2, sequence_num 1, variable 1.
ReportId report_id22 = report_id21;
this->report_store_->CreateDependentReport(1, "", true, HISTOGRAM, {1},
&report_id22);
// Create two dependency chains of reports. We have the variable 1 report
// depend on the variable 0 report for both report IDs.
std::vector<ReportId> chain1;
chain1.push_back(report_id11);
chain1.push_back(report_id12);
std::vector<ReportId> chain2;
chain2.push_back(report_id21);
chain2.push_back(report_id22);
// Start the ReportExecutor
this->report_executor_->Start();
// Enqueue chain 1.
auto status =
this->report_executor_->EnqueueReportGeneration(std::move(chain1));
EXPECT_TRUE(status.ok()) << status.error_code() << " "
<< status.error_message();
// Enqueue chain 2.
status = this->report_executor_->EnqueueReportGeneration(std::move(chain2));
EXPECT_TRUE(status.ok()) << status.error_code() << " "
<< status.error_message();
// Wait for the processing to stop.
this->report_executor_->WaitUntilIdle();
// report_id11 analyzed Part 1 of metric 1 which received
// Basic RAPPOR string observations with 3 categories.
this->CheckReport(report_id11, 3);
// report_id12 analyzed Part 2 of metric 1 which received
// Basic RAPPOR int observations with 10 categories.
this->CheckReport(report_id12, 10);
// report_id21 analyzed Part 1 of metric 1 which received
// Forculus observations in which there were 20 observations of Apple
// but only 19 observations of Banana. So there should only be 1 row in
// the report.
this->CheckReport(report_id21, 1);
// report_id22 of report 2 analyzes Part 2 of metric 2 which received
// Basic RAPPOR int observations with 10 categories.
this->CheckReport(report_id22, 10);
}
REGISTER_TYPED_TEST_CASE_P(ReportExecutorAbstractTest, EnqueueReportGeneration);
} // namespace analyzer
} // namespace cobalt
#endif // COBALT_ANALYZER_REPORT_MASTER_REPORT_EXECUTOR_ABSTRACT_TEST_H_