blob: 3bcdfe6bbfaa9e9d5667ad5587e43ea425b2c367 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "analyzer/report_master/raw_dump_reports.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "analyzer/store/data_store.h"
#include "analyzer/store/memory_store.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"
namespace cobalt {
namespace analyzer {
using config::AnalyzerConfig;
using store::DataStore;
using store::Status;
using store::test::FaultInjectableMemoryStore;
namespace {
const uint32_t kCustomerId = 1;
const uint32_t kProjectId = 1;
const uint32_t kMetricId = 1;
const uint32_t kMissingPartsMetricID = 2;
const uint32_t kNosuchMetricId = 3;
const uint32_t kDayIndex = 123456;
const char kPart1Name[] = "Part1";
const char kPart2Name[] = "Part2";
const char kPart3Name[] = "Part3";
const char kPart4Name[] = "Part4";
const char* kPartNames[] = {kPart1Name, kPart2Name, kPart3Name, kPart4Name};
std::string PartName(int part_num) {
CHECK(1 <= part_num && part_num <= 4) << part_num;
return kPartNames[part_num - 1];
}
const char kCobaltRegistryText[] = R"(
encoding_configs {
customer_id: 1
project_id: 1
id: 1
no_op_encoding {
}
}
# Metric 1 is the valid metric we will use.
metric_configs {
customer_id: 1
project_id: 1
id: 1
time_zone_policy: UTC
parts {
key: "Part1"
value {
data_type: STRING
}
}
parts {
key: "Part2"
value {
data_type: INT
}
}
parts {
key: "Part3"
value {
data_type: DOUBLE
}
}
parts {
key: "Part4"
value {
data_type: INDEX
}
}
}
# Metric 2 is missing all the parts we will look for.
metric_configs {
customer_id: 1
project_id: 1
id: 2
time_zone_policy: UTC
parts {
key: "Frog"
value {
data_type: STRING
}
}
}
)";
// Adds an ObservationPart to |observation| with the given |part_name|,
// using the NoOp encoding to directly embed a ValuePart with the given
// |data_type| and a value derived from |index|.
void AddUnencodedValuePart(std::string part_name, size_t index,
MetricPart::DataType data_type,
Observation* observation) {
auto value = (*observation->mutable_parts())[part_name]
.mutable_unencoded()
->mutable_unencoded_value();
switch (data_type) {
case MetricPart::STRING: {
value->mutable_string_value()->assign(std::to_string(index));
break;
}
case MetricPart::INT: {
value->set_int_value(index);
break;
}
case MetricPart::DOUBLE: {
value->set_double_value(index);
break;
}
case MetricPart::INDEX: {
value->set_index_value(index);
break;
}
default: { CHECK(false); }
}
}
enum ObservationFault {
kNoFault = 0,
kMissingPart2 = 1,
kWrongTypeForPart2 = 2
};
// Builds an unencoded observation for our test metric. If index is even
// then the value of |fault| is ignored and the Observation will be valid. But
// if index is odd then the Observation will have the fault specified by |fault|
// in its Part2.
Observation MakeObservationWithFault(size_t index, ObservationFault fault) {
Observation observation;
AddUnencodedValuePart(kPart1Name, index, MetricPart::STRING, &observation);
if (index % 2 == 0) {
fault = kNoFault;
}
switch (fault) {
case kNoFault:
AddUnencodedValuePart(kPart2Name, index, MetricPart::INT, &observation);
break;
case kMissingPart2:
break;
case kWrongTypeForPart2:
AddUnencodedValuePart(kPart2Name, index, MetricPart::STRING,
&observation);
break;
}
AddUnencodedValuePart(kPart3Name, index, MetricPart::DOUBLE, &observation);
AddUnencodedValuePart(kPart4Name, index, MetricPart::INDEX, &observation);
return observation;
}
// Returns the string representation of |value|, given that it was extracted
// from part |part_num| of one of the valid Observations that we made.
std::string AsString(int part_num, const ValuePart& value) {
std::ostringstream stream;
switch (part_num) {
case 1:
EXPECT_EQ(ValuePart::kStringValue, value.data_case());
return value.string_value();
case 2:
EXPECT_EQ(ValuePart::kIntValue, value.data_case());
stream << value.int_value();
break;
case 3:
EXPECT_EQ(ValuePart::kDoubleValue, value.data_case());
stream << value.double_value();
break;
case 4:
EXPECT_EQ(ValuePart::kIndexValue, value.data_case());
stream << value.index_value();
break;
default:
CHECK(false) << part_num;
}
return stream.str();
}
// Checks a ReportRow returned from the iterator, given that |part_nums|
// was used for the current experiment.
void CheckRow(const ReportRow* row, std::vector<int> part_nums) {
ASSERT_NE(nullptr, row);
ASSERT_EQ(ReportRow::kRawDump, row->row_type_case());
int expected_num_parts = static_cast<int>(part_nums.size());
ASSERT_EQ(expected_num_parts, row->raw_dump().values_size());
std::string value_as_string =
AsString(part_nums[0], row->raw_dump().values(0));
for (int i = 1; i < expected_num_parts; i++) {
EXPECT_EQ(value_as_string,
AsString(part_nums[i], row->raw_dump().values(i)));
}
}
} // namespace
// Tests of the RawDumpReportRowIterator.
class RawDumpReportRowIteratorTest : public testing::Test {
protected:
void SetUp() {
data_store_.reset(new FaultInjectableMemoryStore());
observation_store_.reset(new store::ObservationStore(data_store_));
ASSERT_EQ(store::kOK, data_store_->DeleteAllRows(DataStore::kObservations));
analyzer_config_ =
AnalyzerConfig::CreateFromCobaltRegistryProtoText(kCobaltRegistryText);
}
// Initializes |iterator_| using our fixed customer_id, project_id, day
// indices, and the given Metric ID and Observation parts.
void NewIterator(uint32_t metric_id, std::vector<int> part_nums) {
std::vector<std::string> parts(part_nums.size());
for (size_t i = 0; i < part_nums.size(); i++) {
parts[i] = PartName(part_nums[i]);
}
iterator_.reset(new RawDumpReportRowIterator(
kCustomerId, kProjectId, metric_id, kDayIndex, kDayIndex, parts, {},
"report_id_string", observation_store_, analyzer_config_));
}
// Adds Observations to the ObservationStore. Every other Observation added
// will have the part specified by |fault| in its Part2.
void AddObservationsWithFault(size_t num_observations,
ObservationFault fault) {
std::vector<Observation> observations;
for (size_t i = 0; i < num_observations; i++) {
observations.emplace_back(MakeObservationWithFault(i, fault));
}
ObservationMetadata metadata;
metadata.set_customer_id(kCustomerId);
metadata.set_project_id(kProjectId);
metadata.set_metric_id(kMetricId);
metadata.set_day_index(kDayIndex);
EXPECT_EQ(store::kOK,
observation_store_->AddObservationBatch(metadata, observations));
}
// This method contains our iterator checking logic. We invoke the methods of
// RawDumpIterator causing it to yield its rows and we check the results.
//
// |expect_init_failure| Do we expect that an error occurred when the
// Iterator was constructed?
//
// |num_observations| How many Observations were added to the
// ObservationStore.
//
// |expect_error_after_this_many_rows| After this may rows returned from the
// ObservationStore, expect the ObservationStore to return an error.
//
// |part_nums| The indices of the ObservationParts that were used.
//
// |fault| Every other Observation added to the ObservationStore will have
// this fault in its Part2.
void IterateOnceAndCheck(bool expect_init_failure, size_t num_observations,
size_t expect_error_after_this_many_rows,
std::vector<int> part_nums, ObservationFault fault) {
const ReportRow* next_row;
size_t row_num = 0;
bool has_more_rows = false;
do {
auto status = iterator_->HasMoreRows(&has_more_rows);
if (expect_init_failure) {
ASSERT_FALSE(status.ok());
return;
}
if (row_num > expect_error_after_this_many_rows) {
ASSERT_FALSE(status.ok()) << "row_num=" << row_num;
return;
}
ASSERT_TRUE(status.ok())
<< status.error_message() << "(" << status.error_code()
<< ") row_num=" << row_num;
if (has_more_rows) {
status = iterator_->NextRow(&next_row);
ASSERT_TRUE(status.ok())
<< status.error_message() << "(" << status.error_code()
<< ") row_num=" << row_num;
CheckRow(next_row, part_nums);
row_num++;
}
} while (has_more_rows);
size_t expected_num_rows = num_observations;
if (fault != kNoFault &&
std::find(part_nums.begin(), part_nums.end(), 2) != part_nums.end()) {
expected_num_rows = num_observations / 2;
}
EXPECT_EQ(expected_num_rows, row_num);
}
// This is our main test method. It adds |num_observations| Observations
// to the ObservationStore in which every other Observation has the fault
// specified by |fault| in its Part2. Then it constructs a new
// RawDumpReportRowIterator, invokes IterateOnceAndCheck(), then
// invokes Reset() and then invokes IterateOnceAndCheck() a second time.
void DoIteratorTest(uint32_t metric_id, bool expect_init_failure,
size_t num_observations, std::vector<int> part_nums,
ObservationFault fault) {
AddObservationsWithFault(num_observations, fault);
NewIterator(metric_id, part_nums);
IterateOnceAndCheck(expect_init_failure, num_observations, -1, part_nums,
fault);
auto status = iterator_->Reset();
ASSERT_TRUE(status.ok())
<< status.error_message() << "(" << status.error_code() << ")";
IterateOnceAndCheck(expect_init_failure, num_observations, -1, part_nums,
fault);
}
// |metric_id| should be something other than kMetricId. This will
// cause initialization of the iterator to fail.
void DoIteratorTestWithInitialFailure(uint32_t metric_id) {
DoIteratorTest(metric_id, true, 10, {3}, kNoFault);
}
// Invokes DoIteratorTest() with our standard metric ID so that no
// initial failure will occur.
void DoIteratorTestWithFault(size_t num_observations,
std::vector<int> part_nums,
ObservationFault fault) {
DoIteratorTest(kMetricId, false, num_observations, part_nums, fault);
}
// Invokes DoIteratorTest() with our standard metric ID and fault = kNoFault
// so that no failures will occur.
void DoIteratorTestNoFault(size_t num_observations,
std::vector<int> part_nums) {
DoIteratorTestWithFault(num_observations, part_nums, kNoFault);
}
std::shared_ptr<FaultInjectableMemoryStore> data_store_;
std::shared_ptr<store::ObservationStore> observation_store_;
std::shared_ptr<config::AnalyzerConfig> analyzer_config_;
std::unique_ptr<RawDumpReportRowIterator> iterator_;
}; // namespace analyzer
// Tests in the case of no errors, zero rows, and parts 3,1.
TEST_F(RawDumpReportRowIteratorTest, NoFault0Rows) {
DoIteratorTestNoFault(0, {3, 1});
}
// Tests in the case of no errors, 1 row, and parts 3,1.
TEST_F(RawDumpReportRowIteratorTest, NoFault1Row) {
DoIteratorTestNoFault(1, {3, 1});
}
// Tests in the case of no errors, 999 rows, and parts 3.
TEST_F(RawDumpReportRowIteratorTest, NoFault999Rows) {
DoIteratorTestNoFault(999, {3});
}
// Tests in the case of no errors, 1000 rows, and parts 2,4.
TEST_F(RawDumpReportRowIteratorTest, NoFault1000Rows) {
DoIteratorTestNoFault(1000, {2, 4});
}
// Tests in the case of no errors, 1001 rows, and parts 1,4,2,3
TEST_F(RawDumpReportRowIteratorTest, NoFault1001Rows) {
DoIteratorTestNoFault(1001, {1, 4, 2, 3});
}
// Tests in the case of no errors, 2001 rows, and parts 2,1,3,
TEST_F(RawDumpReportRowIteratorTest, NoFault2001Rows) {
DoIteratorTestNoFault(2001, {2, 1, 3});
}
// Tests in the case of faulty Observations with a missing part that is not
// used.
TEST_F(RawDumpReportRowIteratorTest, MissingUnusedPart10Rows) {
DoIteratorTestWithFault(10, {3, 1}, kMissingPart2);
}
// Tests in the case of faulty Observations with a missing part that is used.
TEST_F(RawDumpReportRowIteratorTest, MissingUsedPart10Rows) {
DoIteratorTestWithFault(10, {2, 1}, kMissingPart2);
}
// Tests in the case of faulty Observations with a corrupt part that is not
// used.
TEST_F(RawDumpReportRowIteratorTest, WrongTypeUnusedPart10Rows) {
DoIteratorTestWithFault(10, {3, 1}, kWrongTypeForPart2);
}
// Tests in the case of faulty Observations with a corrupt part that is used.
TEST_F(RawDumpReportRowIteratorTest, WrongTypeUsedPart10Rows) {
DoIteratorTestWithFault(10, {2, 1}, kWrongTypeForPart2);
}
// Tests in the case of faulty config with a missing metric part.
TEST_F(RawDumpReportRowIteratorTest, InitFailurePartNotFoundInConfig) {
DoIteratorTestWithInitialFailure(kMissingPartsMetricID);
}
// Tests in the case of faulty config with a missing metric.
TEST_F(RawDumpReportRowIteratorTest, InitFailureMetricNotFoundInConfig) {
DoIteratorTestWithInitialFailure(kNosuchMetricId);
}
// Tests in the case that the ObservationStore returns an error part of
// the way through the query.
TEST_F(RawDumpReportRowIteratorTest, ObservationStoreReturnsError) {
// We tell the DataStore to return an error after 2 queries. We know
// that the RawDumpReportRowIterator happens to be coded so that each
// query returns 1000 rows. So we expect a problem to occur after on
// the row with index 2001.
data_store_->reset_num_to_succeed(2);
// Add more than 2001 valid Observations.
AddObservationsWithFault(2010, kNoFault);
// Construct the iterator.
NewIterator(kMetricId, {3});
// - We don't expect any initial failure because the config was good.
// - We added 2010 Observations
// - We expect to see an error returned *after* the row with the
// (zero-based) index of 1999
// - Expect to see ObservationPart 3.
// - Expect no faulty Observations.
IterateOnceAndCheck(false, 2010, 1999, {3}, kNoFault);
}
} // namespace analyzer
} // namespace cobalt