blob: c1922448b5187c3974eb2753e98e24462a45ee6b [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_MASTER_SERVICE_ABSTRACT_TEST_H_
#define COBALT_ANALYZER_REPORT_MASTER_REPORT_MASTER_SERVICE_ABSTRACT_TEST_H_
#include "analyzer/report_master/report_master_service.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "./observation.pb.h"
#include "analyzer/store/report_store_test_utils.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"
#include "util/datetime_util.h"
// This file contains type-parameterized tests of ReportMasterService.
//
// 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_master_service_test.cc and
// report_master_service_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 kMetricId3 = 3;
static const uint32_t kReportConfigId1 = 1;
static const uint32_t kReportConfigId2 = 2;
static const uint32_t kReportConfigId3 = 3;
static const uint32_t kForculusEncodingConfigId = 1;
static const uint32_t kBasicRapporStringEncodingConfigId = 2;
static const uint32_t kBasicRapporIntEncodingConfigId = 3;
static const uint32_t kBasicRapporIndexEncodingConfigId = 4;
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;
// We will use a StationaryClock with the time fixed to this time in order
// to test that time-related fields are set correctly by ReportMasterService.
static const int64_t kFixedTimeSeconds = 1234567;
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
}
}
}
# Metric 3 has one INDEX part.
element {
customer_id: 1
project_id: 1
id: 3
time_zone_policy: UTC
parts {
key: "Part1"
value {
data_type: INDEX
}
}
}
)";
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
}
}
}
# EncodingConfig 4 is Basic RAPPOR with INDEX categories (non-stochastic).
element {
customer_id: 1
project_id: 1
id: 4
basic_rappor {
prob_0_becomes_1: 0.0
prob_1_stays_1: 1.0
indexed_categories: {
num_categories: 100
}
}
}
)";
static const char* kReportConfigText = R"(
# ReportConfig 1 specifies a report with one variable: part 1 of Metric 1.
element {
customer_id: 1
project_id: 1
id: 1
metric_id: 1
variable {
metric_part: "Part1"
}
}
# ReportConfig 2 specifies a report with 2 variables: Both parts of Metric 2.
element {
customer_id: 1
project_id: 1
id: 2
metric_id: 2
report_type: JOINT
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
}
# ReportConfig 3 is for metric 3 and gives labels for encoding config 4.
element {
customer_id: 1
project_id: 1
id: 3
metric_id: 3
variable {
metric_part: "Part1"
index_labels {
labels {
key: 0
value: "Event A"
}
labels {
key: 1
value: "Event B"
}
labels {
key: 25
value: "Event Z"
}
}
}
}
)";
// An implementation of grpc::Writer that keeps a copy of each object
// written for later checking.
class TestingQueryReportsResponseWriter
: public grpc::WriterInterface<QueryReportsResponse> {
public:
bool Write(const QueryReportsResponse& response,
const grpc::WriteOptions& options) override {
responses.emplace_back(response);
return true;
}
std::vector<QueryReportsResponse> responses;
};
// ReportMasterServiceAbstractTest 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 ReportMasterServiceAbstractTest : public ::testing::Test {
protected:
ReportMasterServiceAbstractTest()
: data_store_(StoreFactoryClass::NewStore()),
observation_store_(new store::ObservationStore(data_store_)),
report_store_(new store::ReportStore(data_store_)),
clock_(new util::StationaryClock()) {
clock_->set_current_time_seconds(kFixedTimeSeconds);
report_store_->set_clock(clock_);
}
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(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(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(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));
// Make an AnalyzerConfig
std::shared_ptr<config::AnalyzerConfig> analyzer_config(
new config::AnalyzerConfig(encoding_config_registry, metric_registry,
report_config_registry));
report_master_service_.reset(new ReportMasterService(
0, observation_store_, report_store_, analyzer_config,
grpc::InsecureServerCredentials()));
report_master_service_->StartWorkerThread();
}
void WaitUntilIdle() { report_master_service_->WaitUntilIdle(); }
// 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);
}
// Makes an Observation with one INDEX value for the given metric and
// encoding.
std::unique_ptr<Observation> MakeIndexObservation(
uint32_t index, uint32_t metric_id, 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(kSomeTimestamp);
auto result = encoder.EncodeIndex(metric_id, encoding_config_id, index);
EXPECT_EQ(encoder::Encoder::kOK, result.status);
CHECK_EQ(encoder::Encoder::kOK, result.status);
EXPECT_TRUE(result.observation.get() != nullptr);
CHECK(result.observation.get() != nullptr);
EXPECT_EQ(1, 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));
}
// Adds to the ObservationStore |num_clients| Observations with one INDEX
// value using the given metric and encoding. Each Observation is generated
// as if from a different client.
void AddIndexObservations(uint32_t index, uint32_t metric_id,
uint32_t encoding_config_id1, int num_clients) {
std::vector<Observation> observations;
for (int i = 0; i < num_clients; i++) {
observations.emplace_back(
*MakeIndexObservation(index, metric_id, encoding_config_id1));
}
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));
}
// Invokes ReportMaster::GetReport() and checks the returned ReportMetadata.
void GetReportAndCheck(const std::string& report_id,
uint32_t expected_report_config_id, bool expect_part1,
bool expect_part2, bool check_completed,
Report* report_out) {
GetReportRequest get_request;
get_request.set_report_id(report_id);
auto status =
report_master_service_->GetReport(nullptr, &get_request, report_out);
EXPECT_TRUE(status.ok()) << "error_code=" << status.error_code()
<< " error_message=" << status.error_message();
// Check report metadata
CheckMetadata(report_id, expected_report_config_id, expect_part1,
expect_part2, check_completed, kFixedTimeSeconds,
report_out->metadata());
}
// Checks a ReportMetadata returned from GetReport or QueryReports.
void CheckMetadata(const std::string& report_id,
uint32_t expected_report_config_id, bool expect_part1,
bool expect_part2, bool check_completed,
int64_t expected_current_time_seconds,
const ReportMetadata& metadata) {
EXPECT_EQ(report_id, metadata.report_id());
EXPECT_EQ(kCustomerId, metadata.customer_id());
EXPECT_EQ(kProjectId, metadata.project_id());
EXPECT_EQ(expected_report_config_id, metadata.report_config_id());
EXPECT_EQ(expected_current_time_seconds,
metadata.creation_time().seconds());
bool expect_joint_report = expect_part1 && expect_part2;
if (check_completed) {
// Currently JOINT reports are not implemented so we expect the report
// to have failed.
ReportState expected_completion_state =
(expect_joint_report ? TERMINATED : COMPLETED_SUCCESSFULLY);
EXPECT_EQ(expected_completion_state, metadata.state());
EXPECT_TRUE(metadata.start_time().seconds() >=
metadata.creation_time().seconds());
EXPECT_TRUE(metadata.finish_time().seconds() >=
metadata.start_time().seconds());
}
EXPECT_EQ(kDayIndex, metadata.first_day_index());
EXPECT_EQ(kDayIndex, metadata.last_day_index());
// Check the metric parts.
int expected_num_parts = (expect_joint_report ? 2 : 1);
ASSERT_EQ(expected_num_parts, metadata.metric_parts_size());
if (expect_part1) {
EXPECT_EQ("Part1", metadata.metric_parts(0));
} else if (!expect_part1 && expect_part2) {
EXPECT_EQ("Part2", metadata.metric_parts(0));
}
if (expect_joint_report) {
EXPECT_EQ("Part2", metadata.metric_parts(1));
}
// Check the associated_report_ids.
if (expect_joint_report) {
EXPECT_EQ(2, metadata.associated_report_ids_size());
} else {
EXPECT_EQ(0, metadata.associated_report_ids_size());
}
EXPECT_TRUE(metadata.one_off());
// Check info_messages.
if (check_completed && expect_joint_report) {
ASSERT_NE(0, metadata.info_messages_size());
EXPECT_NE(std::string::npos,
metadata.info_messages(0).message().find(
"Report type JOINT is not yet implemented"));
EXPECT_EQ(expected_current_time_seconds,
metadata.info_messages(0).timestamp().seconds());
}
}
// Invokes GetReportAndCheck() on the given joint report. Then extracts the
// IDs of the two marginal reports and invokes GetReportAndCheck() on those
// also.
void CheckJointReportAndTwoMarginals(std::string report_id_joint,
uint32_t expected_report_config_id,
bool check_completed,
Report* first_marginal_report_out,
Report* second_marginal_report_out) {
// Get and check the metadata of the joint report.
Report joint_report;
bool expect_part1 = true;
bool expect_part2 = true;
this->GetReportAndCheck(report_id_joint, expected_report_config_id,
expect_part1, expect_part2, check_completed,
&joint_report);
// Currently joint reports are not yet implemented so there should be
// no report rows.
EXPECT_FALSE(joint_report.has_rows());
// Extract the IDs of the two marginal reports.
std::string report_id_11 = joint_report.metadata().associated_report_ids(0);
std::string report_id_12 = joint_report.metadata().associated_report_ids(1);
// Get and check the metadata of the first marginal report.
expect_part1 = true;
expect_part2 = false;
this->GetReportAndCheck(report_id_11, expected_report_config_id,
expect_part1, expect_part2, check_completed,
first_marginal_report_out);
if (check_completed) {
EXPECT_TRUE(first_marginal_report_out->has_rows());
}
// Get and check the metadata of the second marginal report.
expect_part1 = false;
expect_part2 = true;
this->GetReportAndCheck(report_id_12, expected_report_config_id,
expect_part1, expect_part2, check_completed,
second_marginal_report_out);
if (check_completed) {
EXPECT_TRUE(second_marginal_report_out->has_rows());
}
}
// Invokes ReportMaster::QueryReportsInternal() using our fixed customer and
// project and the given report_config_id and time interval. The responses
// will be written to the given |response_writer|. Returns true for success
// or false for failure.
bool QueryReports(uint32_t report_config_id, uint64_t first_time_seconds,
uint64_t limit_time_seconds,
TestingQueryReportsResponseWriter* response_writer) {
QueryReportsRequest request;
request.set_customer_id(kCustomerId);
request.set_project_id(kProjectId);
request.set_report_config_id(report_config_id);
request.mutable_first_timestamp()->set_seconds(first_time_seconds);
request.mutable_limit_timestamp()->set_seconds(limit_time_seconds);
auto status = report_master_service_->QueryReportsInternal(
nullptr, &request, response_writer);
EXPECT_TRUE(status.ok()) << "error_code=" << status.error_code()
<< " error_message=" << status.error_message();
return status.ok();
}
void set_current_time_seconds(int64_t current_time_seconds) {
clock_->set_current_time_seconds(current_time_seconds);
}
// Writes Metadata directly into the ReportStore simulating the case that
// StarReport() was invoked many times to form |num_reports| different
// instances of the report with the given |report_config_id|.
//
// The creation time and start time for report i will be
// kFixedTimeSeconds + i.
//
// The implementation of this function breaks several layers of abstraction
// and writes directly into the underlying ReporStore. This is a convenient
// way to efficiently set up the ReportMetadata table in order test the
// QueryReports function. If we were to use the gRPC API to accomplish this
// it would require many RPC roundtrips which would take a long time.
// There is no reason for the gRPC API to support an efficient implementation
// of this function as it is not useful outside of a test.
//
// The vector of string report IDs from the gRPC API are returned so that
// they may be used to query in the gRPC API.
std::vector<std::string> WriteManyNewReports(uint64_t report_config_id,
size_t num_reports) {
ReportId report_id;
report_id.set_customer_id(kCustomerId);
report_id.set_project_id(kProjectId);
report_id.set_report_config_id(report_config_id);
std::vector<ReportId> report_ids(num_reports, report_id);
ReportMetadataLite metadata;
metadata.set_state(IN_PROGRESS);
metadata.set_first_day_index(kDayIndex);
metadata.set_last_day_index(kDayIndex);
metadata.set_one_off(true);
metadata.add_variable_indices(0);
std::vector<ReportMetadataLite> report_metadata(num_reports, metadata);
std::vector<std::string> string_report_ids(num_reports);
for (size_t i = 0; i < num_reports; i++) {
report_ids[i].set_creation_time_seconds(kFixedTimeSeconds + i);
report_ids[i].set_instance_id(i);
report_metadata[i].set_start_time_seconds(kFixedTimeSeconds + i);
string_report_ids[i] =
report_master_service_->MakeStringReportId(report_ids[i]);
}
// We write all the reports with a single RPC.
store::ReportStoreTestUtils test_utils(report_store_);
EXPECT_EQ(store::kOK,
test_utils.WriteBulkMetadata(report_ids, report_metadata));
return string_report_ids;
}
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<ReportMasterService> report_master_service_;
std::shared_ptr<util::StationaryClock> clock_;
};
TYPED_TEST_CASE_P(ReportMasterServiceAbstractTest);
// Adds observations to the ObservationStore and then uses the ReportMaster
// to run two reports for our two registered ReportConfigs. Checks the
// results. From the ReportMaster API we test the methods StartReport and
// GetReport.
TYPED_TEST_P(ReportMasterServiceAbstractTest, StartAndGetReports) {
// Add some observations for metric 1. We use Basic RAPPOR for
// both parts. We add 20 observations of the pair ("Apple", 10).
// Our report will only analyze part 1; part 2 will be ignored. We have
// set the RAPPOR parameters p and q so there is no randomness. We therefore
// will expect the report to produce the following results:
// ("Apple", 20), ("Banana", 0), ("Cantaloupe", 0).
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", 19 observations of "Banana", and 21
// observations of "Cantaloupe" so we expect to see "Apple" and "Cantaloupe"
// in the report but not "Banana". For the Basic RAPPOR part there will
// be 20 observations of |10|, 19 observations of |9|, and 21 observations
// of |8|. Joint reports are not implemented yet so we will only be checking
// the results of the two marginal reports.
this->AddObservations("Apple", 10, kMetricId2, kForculusEncodingConfigId,
kBasicRapporIntEncodingConfigId, kForculusThreshold);
this->AddObservations("Banana", 9, kMetricId2, kForculusEncodingConfigId,
kBasicRapporIntEncodingConfigId,
kForculusThreshold - 1);
this->AddObservations("Cantaloupe", 8, kMetricId2, kForculusEncodingConfigId,
kBasicRapporIntEncodingConfigId,
kForculusThreshold + 1);
// Start the first report. This is a one-variable report of
// part 1 of metric 1.
StartReportRequest start_request;
start_request.set_customer_id(kCustomerId);
start_request.set_project_id(kProjectId);
start_request.set_report_config_id(kReportConfigId1);
start_request.set_first_day_index(kDayIndex);
start_request.set_last_day_index(kDayIndex);
StartReportResponse start_response;
auto status = this->report_master_service_->StartReport(
nullptr, &start_request, &start_response);
EXPECT_TRUE(status.ok()) << "error_code=" << status.error_code()
<< " error_message=" << status.error_message();
// Capture the ID for report 1.
std::string report_id1 = start_response.report_id();
EXPECT_FALSE(report_id1.empty());
// Start the second report. This is a joint two-variable report of metric 2.
// The two marginal reports will be automatically started also but the
// returned report_id will be for the joint report. Since joint reports are
// not implemented yet we will only be checking the results of the two
// marginal reports but we will be checking the metadata of the joint
// report too.
start_request.set_report_config_id(kReportConfigId2);
status = this->report_master_service_->StartReport(nullptr, &start_request,
&start_response);
EXPECT_TRUE(status.ok()) << "error_code=" << status.error_code()
<< " error_message=" << status.error_message();
// Capture the ID for report 2. This is the ID of the joint report.
std::string report_id2 = start_response.report_id();
EXPECT_FALSE(report_id2.empty());
// Check the meta-data of the first report. It should include part 1 and
// not part 2.
Report report1;
bool expect_part1 = true;
bool expect_part2 = false;
// The report is generated asynchronously and we don't know that it is
// done yet so don't check that it is completed.
bool check_completed = false;
{
SCOPED_TRACE("");
this->GetReportAndCheck(report_id1, kReportConfigId1, expect_part1,
expect_part2, check_completed, &report1);
}
// Check the meta-data of the second report. We should find a joint report
// and two associated marginal reports and we check the metadata of all three.
// The joint report will have meta-data only because joint reports are not
// implemented yet. But the two marginals will be returned to us so we can
// check them. (But not yet because we don't know that the report generation
// is completed yet.)
Report first_marginal_report;
Report second_marginal_report;
{
SCOPED_TRACE("");
this->CheckJointReportAndTwoMarginals(
report_id2, kReportConfigId2, check_completed, &first_marginal_report,
&second_marginal_report);
}
// Wait until the report generation for all reports completes.
this->WaitUntilIdle();
// Check the reports again but this time check that they are completed
// and then check the actual contents of the report rows.
check_completed = true;
{
SCOPED_TRACE("");
this->GetReportAndCheck(report_id1, kReportConfigId1, expect_part1,
expect_part2, check_completed, &report1);
}
// Check the rows of report 1.
// Recall that when adding observations to metric 1 above we used Basic
// RAPPOR with no randomness so we expect to see the results
// ("Apple", 20), ("Banana", 0), ("Cantaloupe", 0).
ASSERT_EQ(3, report1.rows().rows_size());
std::map<std::string, int> report1_results;
for (int i = 0; i < 3; i++) {
report1_results[report1.rows().rows(i).histogram().value().string_value()] =
report1.rows().rows(i).histogram().count_estimate();
}
ASSERT_EQ(3u, report1_results.size());
EXPECT_EQ(20, report1_results["Apple"]);
EXPECT_EQ(0, report1_results["Banana"]);
EXPECT_EQ(0, report1_results["Cantaloupe"]);
// Check report 2 again including its associated marginal reports, this
// time checking that they are complete.
{
SCOPED_TRACE("");
this->CheckJointReportAndTwoMarginals(
report_id2, kReportConfigId2, check_completed, &first_marginal_report,
&second_marginal_report);
}
// Check the rows of the first marginal of report 2. Recall that when adding
// rows to part 1 of metric 2 above we used Forculus and we expect to see the
// results ("Apple", 20), ("Cantaloupe", 21) and not to see "Banana" because
// it should not have been decrypted.
ASSERT_EQ(2, first_marginal_report.rows().rows_size());
std::map<std::string, int> first_marginal_results;
for (int i = 0; i < 2; i++) {
first_marginal_results[first_marginal_report.rows()
.rows(i)
.histogram()
.value()
.string_value()] =
first_marginal_report.rows().rows(i).histogram().count_estimate();
}
ASSERT_EQ(2u, first_marginal_results.size());
EXPECT_EQ(20, first_marginal_results["Apple"]);
EXPECT_EQ(21, first_marginal_results["Cantaloupe"]);
// Check the rows of the second marginal of report 2. Recall that when adding
// rows to part 2 of metric 2 above we used Basic RAPPOR with no randomness
// so we expect to see the following results:
// (a) A count of 0 for the numbers 1, 2, 3, 4, 5, 6, 7
// (b) (8, 21), (9, 19), (10, 20)
ASSERT_EQ(10, second_marginal_report.rows().rows_size());
std::map<int, int> second_marginal_results;
for (int i = 0; i < 10; i++) {
second_marginal_results[second_marginal_report.rows()
.rows(i)
.histogram()
.value()
.int_value()] =
second_marginal_report.rows().rows(i).histogram().count_estimate();
}
ASSERT_EQ(10u, second_marginal_results.size());
for (int i = 1; i <= 7; i++) {
EXPECT_EQ(0, second_marginal_results[i]);
}
EXPECT_EQ(21, second_marginal_results[8]);
EXPECT_EQ(19, second_marginal_results[9]);
EXPECT_EQ(20, second_marginal_results[10]);
}
// Tests Cobalt analyzer end-to-end using a (metric, encoding, report) trio
// in which Observations use the INDEX data type and the report config
// specifies human-readable labels for some of the indices.
TYPED_TEST_P(ReportMasterServiceAbstractTest, E2EWithIndexLabels) {
for (int index = 0; index < 50; index++) {
// Add |index| + 1 observations of |index|.
this->AddIndexObservations(index, kMetricId3,
kBasicRapporIndexEncodingConfigId, index + 1);
}
// Start the report.
StartReportRequest start_request;
start_request.set_customer_id(kCustomerId);
start_request.set_project_id(kProjectId);
start_request.set_report_config_id(kReportConfigId3);
start_request.set_first_day_index(kDayIndex);
start_request.set_last_day_index(kDayIndex);
StartReportResponse start_response;
auto status = this->report_master_service_->StartReport(
nullptr, &start_request, &start_response);
EXPECT_TRUE(status.ok()) << "error_code=" << status.error_code()
<< " error_message=" << status.error_message();
// Capture the report ID.
std::string report_id = start_response.report_id();
EXPECT_FALSE(report_id.empty());
// Wait until the report generation completes.
this->WaitUntilIdle();
Report report;
{
// Fetch the report and check the metadata.
SCOPED_TRACE("");
bool check_completed = true;
bool expect_part1 = true;
bool expect_part2 = false;
this->GetReportAndCheck(report_id, kReportConfigId3, expect_part1,
expect_part2, check_completed, &report);
}
// Check the rows of the report including the labels.
ASSERT_EQ(100, report.rows().rows_size());
for (size_t i = 0; i < 100; i++) {
auto index = report.rows().rows(i).histogram().value().index_value();
auto count = report.rows().rows(i).histogram().count_estimate();
auto label = report.rows().rows(i).histogram().label();
auto expected_count = (index < 50 ? index + 1 : 0);
EXPECT_EQ(expected_count, count) << "i=" << i << ", index=" << index
<< ", count=" << count;
switch (index) {
case 0:
EXPECT_EQ("Event A", label);
break;
case 1:
EXPECT_EQ("Event B", label);
break;
case 25:
EXPECT_EQ("Event Z", label);
break;
default:
EXPECT_EQ("", label);
}
}
}
// Tests the method ReportMaster::QueryReports. We write into the ReportStore
// many instance of ReportConfig 1 and then invoke QueryReports() and check the
// results.
TYPED_TEST_P(ReportMasterServiceAbstractTest, QueryReportsTest) {
// Write Metadata into the ReportStore for 210 reports associated with
// ReporcConfig 1 with creation_times that start at kFixedTimeSeconds and
// increment by 1 second for each report.
std::vector<std::string> report_ids =
this->WriteManyNewReports(kReportConfigId1, 210);
// Now invoke QueryReports. We specify a time window that will omit the
// first three and the last 3 reports. So there should be 204 reports
// returned.
TestingQueryReportsResponseWriter response_writer;
ASSERT_TRUE(this->QueryReports(kReportConfigId1, kFixedTimeSeconds + 3,
kFixedTimeSeconds + 207, &response_writer));
// Since we know that reports are returned in batches of 100 we expect there
// to be 3 batches: Two batches of size 100 and one batch of size 4.
EXPECT_EQ(3u, response_writer.responses.size());
// Check the first batch.
bool expect_part1 = true;
bool expect_part2 = false;
// We don't know that the reports are completed yet.
bool check_completed = false;
EXPECT_EQ(100, response_writer.responses[0].reports_size());
for (int i = 0; i < 100; i++) {
this->CheckMetadata(report_ids[i + 3], kReportConfigId1, expect_part1,
expect_part2, check_completed,
kFixedTimeSeconds + 3 + i,
response_writer.responses[0].reports(i));
}
// Check the second batch.
EXPECT_EQ(100, response_writer.responses[1].reports_size());
for (int i = 0; i < 100; i++) {
this->CheckMetadata(report_ids[i + 103], kReportConfigId1, expect_part1,
expect_part2, check_completed,
kFixedTimeSeconds + 103 + i,
response_writer.responses[1].reports(i));
}
// Check the third batch.
EXPECT_EQ(4, response_writer.responses[2].reports_size());
for (int i = 0; i < 4; i++) {
this->CheckMetadata(report_ids[i + 203], kReportConfigId1, expect_part1,
expect_part2, check_completed,
kFixedTimeSeconds + 203 + i,
response_writer.responses[2].reports(i));
}
}
REGISTER_TYPED_TEST_CASE_P(ReportMasterServiceAbstractTest, StartAndGetReports,
E2EWithIndexLabels, QueryReportsTest);
} // namespace analyzer
} // namespace cobalt
#endif // COBALT_ANALYZER_REPORT_MASTER_REPORT_MASTER_SERVICE_ABSTRACT_TEST_H_