text: // 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 kCobaltConfigText[] = 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::CreateFromCobaltConfigProtoText(kCobaltConfigText);
    }
  
    // 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
  