blob: a0eb3b2f3613cfe0dc0eed193c0219142b66e788 [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_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 "analyzer/report_master/report_exporter.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 {
namespace testing {
const uint32_t kCustomerId = 1;
const uint32_t kProjectId = 1;
const uint32_t kMetricId = 1;
const uint32_t kJointReportConfigId = 1;
const uint32_t kRawDumpReportConfigId = 1;
const uint32_t kGroupedReportConfigId = 3;
const uint32_t kGroupedRawDumpReportConfigId = 4;
const uint32_t kForculusEncodingConfigId = 1;
const uint32_t kBasicRapporEncodingConfigId = 2;
const uint32_t kNoOpEncodingConfigId = 3;
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"
}
}
}
# EncodingConfig 3 is NoOp.
element {
customer_id: 1
project_id: 1
id: 3
no_op_encoding {
}
}
)";
const char* kReportConfigText = R"(
# ReportConfig 1 specifies a JOINT report of both variables of Metric 1.
# We use this config only in order to run HISTOGRAM reports on the
# two variables separately since JOINT reports are not currently
# implemented.
element {
customer_id: 1
project_id: 1
id: 1
metric_id: 1
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
report_type: JOINT
export_configs {
csv {}
gcs {
bucket: "BUCKET-NAME"
}
}
}
# ReportConfig 2 specifies a RAW_DUMP report of both variables of Metric 1.
element {
customer_id: 1
project_id: 1
id: 2
metric_id: 1
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
report_type: RAW_DUMP
export_configs {
csv {}
gcs {
bucket: "BUCKET-NAME"
}
}
}
element {
customer_id: 1
project_id: 1
id: 3
metric_id: 1
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
system_profile_field: [BOARD_NAME]
report_type: JOINT
export_configs {
csv {}
gcs {
bucket: "BUCKET-NAME"
}
}
}
element {
customer_id: 1
project_id: 1
id: 4
metric_id: 1
variable {
metric_part: "Part1"
}
variable {
metric_part: "Part2"
}
system_profile_field: [BOARD_NAME]
report_type: RAW_DUMP
export_configs {
csv {}
gcs {
bucket: "BUCKET-NAME"
}
}
}
)";
// An implementation of GcsUploadInterface that saves its parameters and
// returns OK.
struct FakeGcsUploader : public GcsUploadInterface {
grpc::Status UploadToGCS(const std::string& bucket, const std::string& path,
const std::string& mime_type,
ReportStream* report_stream) override {
this->upload_was_invoked = true;
this->bucket = bucket;
this->path = path;
this->mime_type = mime_type;
this->serialized_report =
std::string(std::istreambuf_iterator<char>(*report_stream), {});
return grpc::Status::OK;
}
bool upload_was_invoked = false;
std::string bucket;
std::string path;
std::string mime_type;
std::string serialized_report;
};
} // 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_)),
fake_uploader_(new testing::FakeGcsUploader()) {
report_id_.set_customer_id(testing::kCustomerId);
report_id_.set_project_id(testing::kProjectId);
report_id_.set_report_config_id(testing::kJointReportConfigId);
}
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>(
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::FromString<RegisteredEncodings>(
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::FromString<RegisteredReports>(
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));
std::shared_ptr<config::AnalyzerConfigManager> analyzer_config_manager(
new config::AnalyzerConfigManager(analyzer_config));
// Make the ReportGenerator
std::unique_ptr<ReportExporter> report_exporter(
new ReportExporter(fake_uploader_));
report_generator_.reset(
new ReportGenerator(analyzer_config_manager, observation_store_,
report_store_, std::move(report_exporter)));
}
// 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) {
AddObservations(value, encoding_config_id, num_clients,
std::make_unique<SystemProfile>());
}
void AddObservations(std::string value, uint32_t encoding_config_id,
int num_clients,
std::unique_ptr<SystemProfile> profile) {
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);
metadata.set_allocated_system_profile(profile.release());
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.
// If |export_report| is true then the report will be exported using
// our FakeGcsUploader. If |in_store| is true the report will be saved
// to the ReportStore.
GeneratedReport GenerateHistogramReport(int variable_index,
bool export_report, bool in_store) {
// 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].
std::string export_name = export_report ? "export_name" : "";
EXPECT_EQ(store::kOK, report_store_->StartNewReport(
testing::kDayIndex, testing::kDayIndex, true,
export_name, in_store, 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;
}
GeneratedReport GenerateGroupedHistogramReport(int variable_index,
bool export_report,
bool in_store) {
report_id_.set_report_config_id(testing::kGroupedReportConfigId);
report_id_.set_sequence_num(variable_index);
std::string export_name = export_report ? "export_name" : "";
EXPECT_EQ(store::kOK, report_store_->StartNewReport(
testing::kDayIndex, testing::kDayIndex, true,
export_name, in_store, HISTOGRAM,
{(uint32_t)variable_index}, &report_id_));
EXPECT_TRUE(report_generator_->GenerateReport(report_id_).ok());
GeneratedReport report;
EXPECT_EQ(store::kOK, report_store_->GetReport(report_id_, &report.metadata,
&report.rows));
return report;
}
GeneratedReport GenerateRawDumpReport(bool export_report, bool in_store) {
report_id_.set_sequence_num(0);
// Start a report for the specified variable, for the interval of days
// [kDayIndex, kDayIndex].
std::string export_name = export_report ? "export_name" : "";
EXPECT_EQ(store::kOK,
report_store_->StartNewReport(
testing::kDayIndex, testing::kDayIndex, true, export_name,
in_store, RAW_DUMP, {0, 1}, &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;
}
GeneratedReport GenerateGroupedRawDumpReport(bool export_report,
bool in_store) {
report_id_.set_report_config_id(testing::kGroupedRawDumpReportConfigId);
report_id_.set_sequence_num(0);
// Start a report for the specified variable, for the interval of days
// [kDayIndex, kDayIndex].
std::string export_name = export_report ? "export_name" : "";
EXPECT_EQ(store::kOK,
report_store_->StartNewReport(
testing::kDayIndex, testing::kDayIndex, true, export_name,
in_store, RAW_DUMP, {0, 1}, &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 is the CSV that should be generated when the report for metric part 2
// is exported, when Forculus Observations are added, based on the
// Observations that are added in AddForculusObservations() above.
const char* const kExpectedPart2ForculusCSV = R"(date,Part2,count,err
2016-12-2,"hello",20.000,0
2016-12-2,"peace",21.000,0
)";
// 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,
const std::string& expected_export_csv) {
EXPECT_EQ(HISTOGRAM, report.metadata.report_type());
EXPECT_EQ(1, report.metadata.variable_indices_size());
EXPECT_EQ(variable_index, report.metadata.variable_indices(0));
if (report.metadata.in_store()) {
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();
}
}
} else {
EXPECT_EQ(0, report.rows.rows_size());
}
if (report.metadata.export_name() == "") {
EXPECT_FALSE(this->fake_uploader_->upload_was_invoked);
} else {
EXPECT_TRUE(this->fake_uploader_->upload_was_invoked);
// Reset for next time
this->fake_uploader_->upload_was_invoked = false;
EXPECT_EQ("BUCKET-NAME", fake_uploader_->bucket);
EXPECT_EQ("1_1_1/export_name.csv", fake_uploader_->path);
EXPECT_EQ("text/csv", fake_uploader_->mime_type);
EXPECT_EQ(expected_export_csv, fake_uploader_->serialized_report);
}
}
// 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);
}
std::unique_ptr<SystemProfile> MakeProfile(std::string board_name) {
auto profile = std::make_unique<SystemProfile>();
*profile->mutable_board_name() = board_name;
return profile;
}
void AddGroupedBasicRapporObservations() {
AddObservations("Apple", testing::kBasicRapporEncodingConfigId, 50,
MakeProfile("foo"));
AddObservations("Apple", testing::kBasicRapporEncodingConfigId, 50,
MakeProfile("bar"));
AddObservations("Banana", testing::kBasicRapporEncodingConfigId, 100,
MakeProfile("foo"));
AddObservations("Banana", testing::kBasicRapporEncodingConfigId, 100,
MakeProfile("bar"));
AddObservations("Cantaloupe", testing::kBasicRapporEncodingConfigId, 150,
MakeProfile("foo"));
AddObservations("Cantaloupe", testing::kBasicRapporEncodingConfigId, 150,
MakeProfile("bar"));
}
void AddUnencodedObservations() {
AddObservations("Apple", testing::kNoOpEncodingConfigId, 1);
AddObservations("Banana", testing::kNoOpEncodingConfigId, 2);
AddObservations("Cantaloupe", testing::kNoOpEncodingConfigId, 3);
}
void AddGroupedUnencodedObservations() {
AddObservations("Apple", testing::kNoOpEncodingConfigId, 1,
MakeProfile("foo"));
AddObservations("Apple", testing::kNoOpEncodingConfigId, 1,
MakeProfile("bar"));
AddObservations("Banana", testing::kNoOpEncodingConfigId, 2,
MakeProfile("foo"));
AddObservations("Banana", testing::kNoOpEncodingConfigId, 2,
MakeProfile("bar"));
AddObservations("Cantaloupe", testing::kNoOpEncodingConfigId, 3,
MakeProfile("foo"));
AddObservations("Cantaloupe", testing::kNoOpEncodingConfigId, 3,
MakeProfile("bar"));
}
// 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));
if (report.metadata.in_store()) {
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);
}
} else {
EXPECT_EQ(0, report.rows.rows_size());
}
if (report.metadata.export_name() == "") {
EXPECT_FALSE(this->fake_uploader_->upload_was_invoked);
} else {
EXPECT_TRUE(this->fake_uploader_->upload_was_invoked);
// Reset for next time
this->fake_uploader_->upload_was_invoked = false;
EXPECT_EQ("BUCKET-NAME", fake_uploader_->bucket);
EXPECT_EQ("1_1_1/export_name.csv", fake_uploader_->path);
EXPECT_EQ("text/csv", fake_uploader_->mime_type);
EXPECT_FALSE(fake_uploader_->serialized_report.empty());
}
}
void CheckGroupedRapporReport(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));
int foo_count = 0;
int bar_count = 0;
if (report.metadata.in_store()) {
EXPECT_EQ(6, report.rows.rows_size());
for (const auto& report_row : report.rows.rows()) {
EXPECT_NE(0, report_row.histogram().std_error());
EXPECT_TRUE(report_row.histogram().has_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);
} else {
EXPECT_EQ(0, report.rows.rows_size());
}
if (report.metadata.export_name() == "") {
EXPECT_FALSE(this->fake_uploader_->upload_was_invoked);
} else {
EXPECT_TRUE(this->fake_uploader_->upload_was_invoked);
this->fake_uploader_->upload_was_invoked = false;
EXPECT_EQ("BUCKET-NAME", fake_uploader_->bucket);
EXPECT_EQ("1_1_3/export_name.csv", fake_uploader_->path);
EXPECT_EQ("text/csv", fake_uploader_->mime_type);
EXPECT_FALSE(fake_uploader_->serialized_report.empty());
}
}
void CheckRawDumpReport(const GeneratedReport& report) {
EXPECT_EQ(RAW_DUMP, report.metadata.report_type());
EXPECT_EQ(2, report.metadata.variable_indices_size());
EXPECT_EQ(0u, report.metadata.variable_indices(0));
EXPECT_EQ(1u, report.metadata.variable_indices(1));
if (report.metadata.in_store()) {
EXPECT_EQ(6, report.rows.rows_size());
} else {
EXPECT_EQ(0, report.rows.rows_size());
}
if (report.metadata.export_name() == "") {
EXPECT_FALSE(this->fake_uploader_->upload_was_invoked);
} else {
EXPECT_TRUE(this->fake_uploader_->upload_was_invoked);
// Reset for next time
this->fake_uploader_->upload_was_invoked = false;
EXPECT_EQ("BUCKET-NAME", fake_uploader_->bucket);
EXPECT_EQ("1_1_1/export_name.csv", fake_uploader_->path);
EXPECT_EQ("text/csv", fake_uploader_->mime_type);
// Take the export CSV file and split it into lines.
std::stringstream csv_stream(fake_uploader_->serialized_report);
std::vector<std::string> csv_lines;
std::string line;
while (std::getline(csv_stream, line)) {
csv_lines.push_back(line);
}
EXPECT_EQ(7u, csv_lines.size());
// Check the header line.
EXPECT_EQ("date,Part1,Part2", csv_lines[0]);
// Check the body of the report. They are in random order so we
// need to count them and check the totals.
size_t apple_lines = 0;
size_t banana_lines = 0;
size_t cantaloupe_lines = 0;
for (auto i = 1ul; i < csv_lines.size(); i++) {
if (csv_lines[i] == "2016-12-2,\"Apple\",\"Apple\"") {
apple_lines++;
} else if (csv_lines[i] == "2016-12-2,\"Banana\",\"Banana\"") {
banana_lines++;
} else if (csv_lines[i] == "2016-12-2,\"Cantaloupe\",\"Cantaloupe\"") {
cantaloupe_lines++;
}
}
EXPECT_EQ(1u, apple_lines);
EXPECT_EQ(2u, banana_lines);
EXPECT_EQ(3u, cantaloupe_lines);
}
}
void CheckGroupedRawDumpReport(const GeneratedReport& report) {
EXPECT_EQ(RAW_DUMP, report.metadata.report_type());
EXPECT_EQ(2, report.metadata.variable_indices_size());
EXPECT_EQ(0u, report.metadata.variable_indices(0));
EXPECT_EQ(1u, report.metadata.variable_indices(1));
if (report.metadata.in_store()) {
EXPECT_EQ(6, report.rows.rows_size());
} else {
EXPECT_EQ(0, report.rows.rows_size());
}
if (report.metadata.export_name() == "") {
EXPECT_FALSE(this->fake_uploader_->upload_was_invoked);
} else {
EXPECT_TRUE(this->fake_uploader_->upload_was_invoked);
// Reset for next time
this->fake_uploader_->upload_was_invoked = false;
EXPECT_EQ("BUCKET-NAME", fake_uploader_->bucket);
EXPECT_EQ("1_1_4/export_name.csv", fake_uploader_->path);
EXPECT_EQ("text/csv", fake_uploader_->mime_type);
LOG(INFO) << "COUNT OF ROWS: " << report.rows.rows_size();
for (const auto& row : report.rows.rows()) {
LOG(INFO) << row.raw_dump().system_profile().board_name();
}
// Take the export CSV file and split it into lines.
std::stringstream csv_stream(fake_uploader_->serialized_report);
std::vector<std::string> csv_lines;
std::string line;
while (std::getline(csv_stream, line)) {
csv_lines.push_back(line);
}
EXPECT_EQ(13u, csv_lines.size());
// Check the header line.
EXPECT_EQ("date,Part1,Part2,Board_Name", csv_lines[0]);
// Check the body of the report. They are in random order so we
// need to count them and check the totals.
size_t apple_foo_lines = 0;
size_t banana_foo_lines = 0;
size_t cantaloupe_foo_lines = 0;
size_t apple_bar_lines = 0;
size_t banana_bar_lines = 0;
size_t cantaloupe_bar_lines = 0;
for (auto i = 1ul; i < csv_lines.size(); i++) {
LOG(INFO) << csv_lines[i];
if (csv_lines[i] == "2016-12-2,\"Apple\",\"Apple\",\"foo\"") {
apple_foo_lines++;
} else if (csv_lines[i] == "2016-12-2,\"Banana\",\"Banana\",\"foo\"") {
banana_foo_lines++;
} else if (csv_lines[i] ==
"2016-12-2,\"Cantaloupe\",\"Cantaloupe\",\"foo\"") {
cantaloupe_foo_lines++;
} else if (csv_lines[i] == "2016-12-2,\"Apple\",\"Apple\",\"bar\"") {
apple_bar_lines++;
} else if (csv_lines[i] == "2016-12-2,\"Banana\",\"Banana\",\"bar\"") {
banana_bar_lines++;
} else if (csv_lines[i] ==
"2016-12-2,\"Cantaloupe\",\"Cantaloupe\",\"bar\"") {
cantaloupe_bar_lines++;
}
}
EXPECT_EQ(1u, apple_foo_lines);
EXPECT_EQ(2u, banana_foo_lines);
EXPECT_EQ(3u, cantaloupe_foo_lines);
EXPECT_EQ(1u, apple_bar_lines);
EXPECT_EQ(2u, banana_bar_lines);
EXPECT_EQ(3u, cantaloupe_bar_lines);
}
}
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_;
std::shared_ptr<testing::FakeGcsUploader> fake_uploader_;
};
TYPED_TEST_CASE_P(ReportGeneratorAbstractTest);
// Tests that the ReportGenerator correctly generates a report for both
// variables of our two-variable metric when the ObservationStore 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;
// Don't export the report. Do store it to the store.
bool in_store = true;
auto report =
this->GenerateHistogramReport(variable_index, false, in_store);
this->CheckForculusReport(report, variable_index, "");
}
{
SCOPED_TRACE("variable_index = 1");
int variable_index = 1;
// Do export the report. Do store it to the store.
bool in_store = true;
auto report = this->GenerateHistogramReport(variable_index, true, in_store);
this->CheckForculusReport(report, variable_index,
this->kExpectedPart2ForculusCSV);
}
{
SCOPED_TRACE("variable_index = 0");
int variable_index = 0;
// Don't export the report. Don't store it to the store.
bool in_store = false;
auto report =
this->GenerateHistogramReport(variable_index, false, in_store);
this->CheckForculusReport(report, variable_index, "");
}
{
SCOPED_TRACE("variable_index = 1");
int variable_index = 1;
// Do export the report. Don't store it to the store.
bool in_store = false;
auto report = this->GenerateHistogramReport(variable_index, true, in_store);
this->CheckForculusReport(report, variable_index,
this->kExpectedPart2ForculusCSV);
}
}
// Tests that the ReportGenerator correctly generates a report for both
// variables of our two-variable metric when the ObservationStore 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;
// Do exort the report. Do store it to the store.
bool in_store = true;
auto report = this->GenerateHistogramReport(variable_index, true, in_store);
this->CheckBasicRapporReport(report, variable_index);
}
{
SCOPED_TRACE("variable_index = 1");
int variable_index = 1;
// Don't export the report. Do store it to the store.
bool in_store = true;
auto report =
this->GenerateHistogramReport(variable_index, false, in_store);
this->CheckBasicRapporReport(report, variable_index);
}
}
TYPED_TEST_P(ReportGeneratorAbstractTest, GroupedBasicRappor) {
this->AddGroupedBasicRapporObservations();
{
SCOPED_TRACE("variable_index = 0");
int variable_index = 0;
// Don't export the report. Do store it to the store.
bool in_store = true;
auto report =
this->GenerateGroupedHistogramReport(variable_index, true, in_store);
this->CheckGroupedRapporReport(report, variable_index);
}
{
SCOPED_TRACE("variable_index = 1");
int variable_index = 1;
// Don't export the report. Do store it to the store.
bool in_store = true;
auto report =
this->GenerateGroupedHistogramReport(variable_index, true, in_store);
this->CheckGroupedRapporReport(report, variable_index);
}
}
TYPED_TEST_P(ReportGeneratorAbstractTest, RawDump) {
this->AddUnencodedObservations();
// Do exort the report. Don't store it to the store.
bool in_store = false;
auto report = this->GenerateRawDumpReport(true, in_store);
this->CheckRawDumpReport(report);
}
TYPED_TEST_P(ReportGeneratorAbstractTest, GroupedRawDump) {
this->AddGroupedUnencodedObservations();
// Do exort the report. Don't store it to the store.
bool in_store = false;
auto report = this->GenerateGroupedRawDumpReport(true, in_store);
this->CheckGroupedRawDumpReport(report);
}
REGISTER_TYPED_TEST_CASE_P(ReportGeneratorAbstractTest, Forculus, BasicRappor,
RawDump, GroupedBasicRappor, GroupedRawDump);
} // namespace analyzer
} // namespace cobalt
#endif // COBALT_ANALYZER_REPORT_MASTER_REPORT_GENERATOR_ABSTRACT_TEST_H_