text: // Copyright 2019 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 "src/developer/forensics/last_reboot/reporter.h"
  
  #include <lib/fit/result.h>
  #include <lib/zx/time.h>
  #include <zircon/errors.h>
  
  #include <cstdint>
  #include <memory>
  #include <optional>
  #include <vector>
  
  #include <gmock/gmock.h>
  #include <gtest/gtest.h>
  
  #include "src/developer/forensics/last_reboot/reboot_log.h"
  #include "src/developer/forensics/testing/gpretty_printers.h"
  #include "src/developer/forensics/testing/stubs/cobalt_logger.h"
  #include "src/developer/forensics/testing/stubs/cobalt_logger_factory.h"
  #include "src/developer/forensics/testing/stubs/crash_reporter.h"
  #include "src/developer/forensics/testing/unit_test_fixture.h"
  #include "src/developer/forensics/utils/cobalt/event.h"
  #include "src/developer/forensics/utils/cobalt/logger.h"
  #include "src/developer/forensics/utils/cobalt/metrics.h"
  #include "src/lib/files/file.h"
  #include "src/lib/files/path.h"
  #include "src/lib/files/scoped_temp_dir.h"
  #include "src/lib/fxl/strings/string_printf.h"
  
  namespace forensics {
  namespace last_reboot {
  namespace {
  
  using testing::IsEmpty;
  using testing::UnorderedElementsAreArray;
  
  constexpr char kHasReportedOnPath[] = "/tmp/has_reported_on_reboot_log.txt";
  constexpr char kNoGracefulReason[] = "GRACEFUL REBOOT REASON (NONE)";
  
  struct UngracefulRebootTestParam {
    std::string test_name;
    std::string zircon_reboot_log;
    std::string reboot_reason;
  
    std::string output_crash_signature;
    std::optional<zx::duration> output_uptime;
    cobalt::LastRebootReason output_last_reboot_reason;
  };
  
  struct GracefulRebootTestParam {
    std::string test_name;
    std::optional<std::string> graceful_reboot_log;
  
    cobalt::LastRebootReason output_last_reboot_reason;
  };
  
  struct GracefulRebootWithCrashTestParam {
    std::string test_name;
    std::string graceful_reboot_log;
    std::string reboot_reason;
  
    std::string output_crash_signature;
    zx::duration output_uptime;
    cobalt::LastRebootReason output_last_reboot_reason;
    bool output_is_fatal;
  };
  
  template <typename TestParam>
  class ReporterTest : public UnitTestFixture, public testing::WithParamInterface<TestParam> {
   public:
    ReporterTest() : cobalt_(dispatcher(), services()) {}
  
    void SetUp() override { FX_CHECK(tmp_dir_.NewTempFileWithData("", &not_a_fdr_path_)); }
    void TearDown() override { files::DeletePath(kHasReportedOnPath, /*recursive=*/false); }
  
   protected:
    void SetUpCrashReporterServer(std::unique_ptr<stubs::CrashReporterBase> server) {
      crash_reporter_server_ = std::move(server);
      if (crash_reporter_server_) {
        InjectServiceProvider(crash_reporter_server_.get());
      }
    }
  
    void WriteZirconRebootLogContents(const std::string& contents) {
      FX_CHECK(tmp_dir_.NewTempFileWithData(contents, &zircon_reboot_log_path_));
    }
  
    void WriteGracefulRebootLogContents(const std::string& contents) {
      FX_CHECK(tmp_dir_.NewTempFileWithData(contents, &graceful_reboot_log_path_));
    }
  
    void SetAsFdr() { FX_CHECK(files::DeletePath(not_a_fdr_path_, /*recursive=*/true)); }
  
    void ReportOnRebootLog() {
      const auto reboot_log = RebootLog::ParseRebootLog(zircon_reboot_log_path_,
                                                        graceful_reboot_log_path_, not_a_fdr_path_);
      ReportOn(reboot_log);
    }
  
    void ReportOn(const RebootLog& reboot_log) {
      Reporter reporter(dispatcher(), services(), &cobalt_);
      reporter.ReportOn(reboot_log, /*delay=*/zx::sec(0));
      RunLoopUntilIdle();
    }
  
    std::string zircon_reboot_log_path_;
    std::string graceful_reboot_log_path_;
    std::string not_a_fdr_path_;
    std::unique_ptr<stubs::CrashReporterBase> crash_reporter_server_;
  
   private:
    cobalt::Logger cobalt_;
    files::ScopedTempDir tmp_dir_;
  };
  
  using GenericReporterTest = ReporterTest<UngracefulRebootTestParam /*does not matter*/>;
  
  TEST_F(GenericReporterTest, Succeed_WellFormedRebootLog) {
    const zx::duration uptime = zx::msec(74715002);
    const RebootLog reboot_log(RebootReason::kKernelPanic,
                               "ZIRCON REBOOT REASON (KERNEL PANIC)\n\nUPTIME (ms)\n74715002",
                               uptime);
  
    SetUpCrashReporterServer(
        std::make_unique<stubs::CrashReporter>(stubs::CrashReporter::Expectations{
            .crash_signature = ToCrashSignature(reboot_log.RebootReason()),
            .reboot_log = reboot_log.RebootLogStr(),
            .uptime = reboot_log.Uptime(),
            .is_fatal = true,
        }));
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(cobalt::LastRebootReason::kKernelPanic, uptime.to_usecs()),
                }));
    EXPECT_TRUE(files::IsFile(kHasReportedOnPath));
  }
  
  TEST_F(GenericReporterTest, Succeed_NoUptime) {
    const RebootLog reboot_log(RebootReason::kKernelPanic, "ZIRCON REBOOT REASON (KERNEL PANIC)\n",
                               std::nullopt);
  
    SetUpCrashReporterServer(
        std::make_unique<stubs::CrashReporter>(stubs::CrashReporter::Expectations{
            .crash_signature = ToCrashSignature(reboot_log.RebootReason()),
            .reboot_log = reboot_log.RebootLogStr(),
            .uptime = std::nullopt,
            .is_fatal = true,
        }));
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(cobalt::LastRebootReason::kKernelPanic, /*duration=*/0u),
                }));
  }
  
  TEST_F(GenericReporterTest, Succeed_NoCrashReportFiledCleanReboot) {
    const zx::duration uptime = zx::msec(74715002);
    const RebootLog reboot_log(RebootReason::kGenericGraceful,
                               "ZIRCON REBOOT REASON (NO CRASH)\n\nUPTIME (ms)\n74715002", uptime);
  
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterNoFileExpected>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(cobalt::LastRebootReason::kGenericGraceful, uptime.to_usecs()),
                }));
  }
  
  TEST_F(GenericReporterTest, Succeed_NoCrashReportFiledColdReboot) {
    const RebootLog reboot_log(RebootReason::kCold, "", std::nullopt);
  
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterNoFileExpected>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(cobalt::LastRebootReason::kCold, /*duration=*/0u),
                }));
  }
  
  TEST_F(GenericReporterTest, Fail_CrashReporterFailsToFile) {
    const zx::duration uptime = zx::msec(74715002);
    const RebootLog reboot_log(RebootReason::kKernelPanic,
                               "ZIRCON REBOOT REASON (KERNEL PANIC)\n\nUPTIME (ms)\n74715002",
                               uptime);
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterAlwaysReturnsError>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(cobalt::LastRebootReason::kKernelPanic, uptime.to_usecs()),
                }));
  }
  
  TEST_F(GenericReporterTest, Succeed_DoesNothingIfAlreadyReportedOn) {
    ASSERT_TRUE(files::WriteFile(kHasReportedOnPath, /*data=*/"", /*size=*/0));
  
    const RebootLog reboot_log(RebootReason::kKernelPanic,
                               "ZIRCON REBOOT REASON (KERNEL PANIC)\n\nUPTIME (ms)\n74715002",
                               zx::msec(74715002));
  
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterNoFileExpected>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOn(reboot_log);
  
    EXPECT_THAT(ReceivedCobaltEvents(), IsEmpty());
  }
  
  using UngracefulReporterTest = ReporterTest<UngracefulRebootTestParam>;
  
  INSTANTIATE_TEST_SUITE_P(WithVariousRebootLogs, UngracefulReporterTest,
                           ::testing::ValuesIn(std::vector<UngracefulRebootTestParam>({
                               {
                                   "KernelPanic",
                                   "ZIRCON REBOOT REASON (KERNEL PANIC)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (KERNEL PANIC)",
                                   "fuchsia-kernel-panic",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kKernelPanic,
                               },
                               {
                                   "OOM",
                                   "ZIRCON REBOOT REASON (OOM)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (OOM)",
                                   "fuchsia-oom",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kSystemOutOfMemory,
                               },
                               {
                                   "Spontaneous",
                                   "ZIRCON REBOOT REASON (UNKNOWN)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (SPONTANEOUS)",
                                   "fuchsia-brief-power-loss",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kBriefPowerLoss,
                               },
                               {
                                   "SoftwareWatchdogTimeout",
                                   "ZIRCON REBOOT REASON (SW WATCHDOG)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (SOFTWARE WATCHDOG TIMEOUT)",
                                   "fuchsia-sw-watchdog-timeout",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kSoftwareWatchdogTimeout,
                               },
                               {
                                   "HardwareWatchdogTimeout",
                                   "ZIRCON REBOOT REASON (HW WATCHDOG)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (HARDWARE WATCHDOG TIMEOUT)",
                                   "fuchsia-hw-watchdog-timeout",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kHardwareWatchdogTimeout,
                               },
                               {
                                   "BrownoutPower",
                                   "ZIRCON REBOOT REASON (BROWNOUT)\n\nUPTIME (ms)\n65487494",
                                   "FINAL REBOOT REASON (BROWNOUT)",
                                   "fuchsia-brownout",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kBrownout,
                               },
                               {
                                   "NotParseable",
                                   "NOT PARSEABLE",
                                   "FINAL REBOOT REASON (NOT PARSEABLE)",
                                   "fuchsia-reboot-log-not-parseable",
                                   std::nullopt,
                                   cobalt::LastRebootReason::kUnknown,
                               },
                           })),
                           [](const testing::TestParamInfo<UngracefulRebootTestParam>& info) {
                             return info.param.test_name;
                           });
  
  TEST_P(UngracefulReporterTest, Succeed) {
    const auto param = GetParam();
  
    WriteZirconRebootLogContents(param.zircon_reboot_log);
    SetUpCrashReporterServer(
        std::make_unique<stubs::CrashReporter>(stubs::CrashReporter::Expectations{
            .crash_signature = param.output_crash_signature,
            .reboot_log = fxl::StringPrintf("%s\n%s\n\n%s", param.zircon_reboot_log.c_str(),
                                            kNoGracefulReason, param.reboot_reason.c_str()),
            .uptime = param.output_uptime,
            .is_fatal = true,
        }));
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOnRebootLog();
  
    const zx::duration expected_uptime =
        (param.output_uptime.has_value()) ? param.output_uptime.value() : zx::usec(0);
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(param.output_last_reboot_reason, expected_uptime.to_usecs()),
                }));
  }
  
  using GracefulReporterTest = ReporterTest<GracefulRebootTestParam>;
  
  INSTANTIATE_TEST_SUITE_P(WithVariousRebootLogs, GracefulReporterTest,
                           ::testing::ValuesIn(std::vector<GracefulRebootTestParam>({
                               {
                                   "UserRequest",
                                   "USER REQUEST",
                                   cobalt::LastRebootReason::kUserRequest,
                               },
                               {
                                   "SystemUpdate",
                                   "SYSTEM UPDATE",
                                   cobalt::LastRebootReason::kSystemUpdate,
                               },
                               {
                                   "ZbiSwap",
                                   "ZBI SWAP",
                                   cobalt::LastRebootReason::kZbiSwap,
                               },
                               {
                                   "HighTemperature",
                                   "HIGH TEMPERATURE",
                                   cobalt::LastRebootReason::kHighTemperature,
                               },
                               {
                                   "NotSupported",
                                   "NOT SUPPORTED",
                                   cobalt::LastRebootReason::kGenericGraceful,
                               },
                               {
                                   "kNotParseable",
                                   "NOT PARSEABLE",
                                   cobalt::LastRebootReason::kGenericGraceful,
                               },
                               {
                                   "None",
                                   std::nullopt,
                                   cobalt::LastRebootReason::kGenericGraceful,
                               },
                           })),
                           [](const testing::TestParamInfo<GracefulRebootTestParam>& info) {
                             return info.param.test_name;
                           });
  
  TEST_P(GracefulReporterTest, Succeed) {
    const auto param = GetParam();
  
    WriteZirconRebootLogContents("ZIRCON REBOOT REASON (NO CRASH)\n\nUPTIME (ms)\n65487494");
    if (param.graceful_reboot_log.has_value()) {
      WriteGracefulRebootLogContents(param.graceful_reboot_log.value());
    }
  
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterNoFileExpected>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOnRebootLog();
  
    const zx::duration expected_uptime = zx::msec(65487494);
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(param.output_last_reboot_reason, expected_uptime.to_usecs()),
                }));
  }
  
  TEST_P(GracefulReporterTest, Succeed_FDR) {
    WriteZirconRebootLogContents("ZIRCON REBOOT REASON (NO CRASH)\n\nUPTIME (ms)\n65487494");
    SetAsFdr();
  
    SetUpCrashReporterServer(std::make_unique<stubs::CrashReporterNoFileExpected>());
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOnRebootLog();
  
    const zx::duration expected_uptime = zx::msec(65487494);
    EXPECT_THAT(
        ReceivedCobaltEvents(),
        UnorderedElementsAreArray({
            cobalt::Event(cobalt::LastRebootReason::kFactoryDataReset, expected_uptime.to_usecs()),
        }));
  }
  
  using GracefulWithCrashReporterTest = ReporterTest<GracefulRebootWithCrashTestParam>;
  
  INSTANTIATE_TEST_SUITE_P(WithVariousRebootLogs, GracefulWithCrashReporterTest,
                           ::testing::ValuesIn(std::vector<GracefulRebootWithCrashTestParam>({
                               {
                                   "SessionFailure",
                                   "SESSION FAILURE",
                                   "FINAL REBOOT REASON (SESSION FAILURE)",
                                   "fuchsia-session-failure",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kSessionFailure,
                                   false,
                               },
                               {
                                   "SysmgrFailure",
                                   "SYSMGR FAILURE",
                                   "FINAL REBOOT REASON (SYSMGR FAILURE)",
                                   "fuchsia-sysmgr-failure",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kSysmgrFailure,
                                   true,
                               },
                               {
                                   "CriticalComponentFailure",
                                   "CRITICAL COMPONENT FAILURE",
                                   "FINAL REBOOT REASON (CRITICAL COMPONENT FAILURE)",
                                   "fuchsia-critical-component-failure",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kCriticalComponentFailure,
                                   true,
                               },
                               {
                                   "RetrySystemUpdate",
                                   "RETRY SYSTEM UPDATE",
                                   "FINAL REBOOT REASON (RETRY SYSTEM UPDATE)",
                                   "fuchsia-retry-system-update",
                                   zx::msec(65487494),
                                   cobalt::LastRebootReason::kRetrySystemUpdate,
                                   true,
                               },
                           })),
                           [](const testing::TestParamInfo<GracefulRebootWithCrashTestParam>& info) {
                             return info.param.test_name;
                           });
  TEST_P(GracefulWithCrashReporterTest, Succeed) {
    const auto param = GetParam();
  
    const std::string zircon_reboot_log = fxl::StringPrintf(
        "ZIRCON REBOOT REASON (NO CRASH)\n\nUPTIME (ms)\n%lu", param.output_uptime.to_msecs());
    WriteZirconRebootLogContents(zircon_reboot_log);
  
    WriteGracefulRebootLogContents(param.graceful_reboot_log);
  
    SetUpCrashReporterServer(
        std::make_unique<stubs::CrashReporter>(stubs::CrashReporter::Expectations{
            .crash_signature = param.output_crash_signature,
            .reboot_log =
                fxl::StringPrintf("%s\nGRACEFUL REBOOT REASON (%s)\n\n%s", zircon_reboot_log.c_str(),
                                  param.graceful_reboot_log.c_str(), param.reboot_reason.c_str()),
            .uptime = param.output_uptime,
            .is_fatal = param.output_is_fatal,
        }));
    SetUpCobaltServer(std::make_unique<stubs::CobaltLoggerFactory>());
  
    ReportOnRebootLog();
  
    EXPECT_THAT(ReceivedCobaltEvents(),
                UnorderedElementsAreArray({
                    cobalt::Event(param.output_last_reboot_reason, param.output_uptime.to_usecs()),
                }));
  }
  
  }  // namespace
  }  // namespace last_reboot
  }  // namespace forensics
  