// Copyright 2021 The Chromium 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 "components/metrics/clean_exit_beacon.h" #include #include #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/memory/scoped_refptr.h" #include "base/metrics/field_trial.h" #include "base/test/gtest_util.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/mock_entropy_provider.h" #include "base/test/task_environment.h" #include "base/time/time.h" #include "build/build_config.h" #include "components/metrics/metrics_pref_names.h" #include "components/prefs/pref_registry_simple.h" #include "components/prefs/pref_service_factory.h" #include "components/prefs/testing_pref_service.h" #include "components/prefs/testing_pref_store.h" #include "components/variations/pref_names.h" #include "components/variations/variations_test_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/abseil-cpp/absl/types/optional.h" namespace metrics { namespace { const wchar_t kDummyWindowsRegistryKey[] = L""; } // namespace class TestCleanExitBeacon : public CleanExitBeacon { public: explicit TestCleanExitBeacon( PrefService* local_state, const base::FilePath& user_data_dir = base::FilePath()) : CleanExitBeacon(kDummyWindowsRegistryKey, user_data_dir, local_state) { Initialize(); } ~TestCleanExitBeacon() override = default; }; class CleanExitBeaconTest : public ::testing::Test { public: void SetUp() override { metrics::CleanExitBeacon::RegisterPrefs(prefs_.registry()); ASSERT_TRUE(user_data_dir_.CreateUniqueTempDir()); } protected: base::HistogramTester histogram_tester_; TestingPrefServiceSimple prefs_; base::ScopedTempDir user_data_dir_; private: base::test::TaskEnvironment task_environment_; }; struct BadBeaconTestParams { const std::string test_name; bool beacon_file_exists; const std::string beacon_file_contents; BeaconFileState beacon_file_state; }; // Used for testing beacon files that are not well-formed, do not exist, etc. class BadBeaconFileTest : public testing::WithParamInterface, public CleanExitBeaconTest {}; struct BeaconConsistencyTestParams { // Inputs: const std::string test_name; absl::optional beacon_file_beacon_value; absl::optional platform_specific_beacon_value; absl::optional local_state_beacon_value; // Result: CleanExitBeaconConsistency expected_consistency; }; #if BUILDFLAG(IS_IOS) // Used for testing the logic that emits to the UMA.CleanExitBeaconConsistency3 // histogram. class BeaconFileAndPlatformBeaconConsistencyTest : public testing::WithParamInterface, public CleanExitBeaconTest {}; #endif // BUILDFLAG(IS_IOS) // Verify that the crash streak metric is 0 when default pref values are used. TEST_F(CleanExitBeaconTest, CrashStreakMetricWithDefaultPrefs) { CleanExitBeacon::ResetStabilityExitedCleanlyForTesting(&prefs_); TestCleanExitBeacon clean_exit_beacon(&prefs_); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", 0, 1); } // Verify that the crash streak metric is 0 when prefs are explicitly set to // their defaults. TEST_F(CleanExitBeaconTest, CrashStreakMetricWithNoCrashes) { // The default value for kStabilityExitedCleanly is true, but defaults can // change, so we explicitly set it to true here. Similarly, we explicitly set // kVariationsCrashStreak to 0. CleanExitBeacon::SetStabilityExitedCleanlyForTesting(&prefs_, true); prefs_.SetInteger(variations::prefs::kVariationsCrashStreak, 0); TestCleanExitBeacon clean_exit_beacon(&prefs_); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", 0, 1); } // Verify that the crash streak metric is correctly recorded when there is a // non-zero crash streak. TEST_F(CleanExitBeaconTest, CrashStreakMetricWithSomeCrashes) { // The default value for kStabilityExitedCleanly is true, but defaults can // change, so we explicitly set it to true here. CleanExitBeacon::SetStabilityExitedCleanlyForTesting(&prefs_, true); prefs_.SetInteger(variations::prefs::kVariationsCrashStreak, 1); TestCleanExitBeacon clean_exit_beacon(&prefs_); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", 1, 1); } // Verify that the crash streak is correctly incremented and recorded when the // last Chrome session did not exit cleanly. TEST_F(CleanExitBeaconTest, CrashIncrementsCrashStreak) { CleanExitBeacon::SetStabilityExitedCleanlyForTesting(&prefs_, false); prefs_.SetInteger(variations::prefs::kVariationsCrashStreak, 1); TestCleanExitBeacon clean_exit_beacon(&prefs_); EXPECT_EQ(prefs_.GetInteger(variations::prefs::kVariationsCrashStreak), 2); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", 2, 1); } // Verify that the crash streak is correctly incremented and recorded when the // last Chrome session did not exit cleanly and the default crash streak value // is used. TEST_F(CleanExitBeaconTest, CrashIncrementsCrashStreakWithDefaultCrashStreakPref) { CleanExitBeacon::SetStabilityExitedCleanlyForTesting(&prefs_, false); TestCleanExitBeacon clean_exit_beacon(&prefs_); EXPECT_EQ(prefs_.GetInteger(variations::prefs::kVariationsCrashStreak), 1); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", 1, 1); } // Verify that no attempt is made to read the beacon file when no user // data dir is provided. TEST_F(CleanExitBeaconTest, InitWithoutUserDataDir) { TestCleanExitBeacon beacon(&prefs_, base::FilePath()); EXPECT_TRUE(beacon.GetUserDataDirForTesting().empty()); EXPECT_TRUE(beacon.GetBeaconFilePathForTesting().empty()); histogram_tester_.ExpectTotalCount( "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", 0); } INSTANTIATE_TEST_SUITE_P( All, BadBeaconFileTest, ::testing::Values( BadBeaconTestParams{ .test_name = "NoVariationsFile", .beacon_file_exists = false, .beacon_file_contents = "", .beacon_file_state = BeaconFileState::kNotDeserializable}, BadBeaconTestParams{ .test_name = "EmptyVariationsFile", .beacon_file_exists = true, .beacon_file_contents = "", .beacon_file_state = BeaconFileState::kNotDeserializable}, BadBeaconTestParams{ .test_name = "NotDictionary", .beacon_file_exists = true, .beacon_file_contents = "{abc123", .beacon_file_state = BeaconFileState::kNotDeserializable}, BadBeaconTestParams{ .test_name = "EmptyDictionary", .beacon_file_exists = true, .beacon_file_contents = "{}", .beacon_file_state = BeaconFileState::kMissingDictionary}, BadBeaconTestParams{ .test_name = "MissingCrashStreak", .beacon_file_exists = true, .beacon_file_contents = "{\"user_experience_metrics.stability.exited_cleanly\":true}", .beacon_file_state = BeaconFileState::kMissingCrashStreak}, BadBeaconTestParams{ .test_name = "MissingBeacon", .beacon_file_exists = true, .beacon_file_contents = "{\"variations_crash_streak\":1}", .beacon_file_state = BeaconFileState::kMissingBeacon}), [](const ::testing::TestParamInfo& params) { return params.param.test_name; }); // Verify that the inability to get the beacon file's contents for a plethora of // reasons (a) doesn't crash and (b) correctly records the BeaconFileState // metric. TEST_P(BadBeaconFileTest, InitWithUnusableBeaconFile) { BadBeaconTestParams params = GetParam(); const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); if (params.beacon_file_exists) { const base::FilePath temp_beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); ASSERT_LT(0, base::WriteFile(temp_beacon_file_path, params.beacon_file_contents.data())); } TestCleanExitBeacon beacon(&prefs_, user_data_dir_path); histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", params.beacon_file_state, 1); } // Verify that successfully reading the beacon file's contents results in // correctly (a) setting the |did_previous_session_exit_cleanly_| field and (b) // recording metrics when the last session exited cleanly. TEST_F(CleanExitBeaconTest, InitWithBeaconFile) { const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); const base::FilePath temp_beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); const int num_crashes = 2; ASSERT_LT(0, base::WriteFile( temp_beacon_file_path, CleanExitBeacon::CreateBeaconFileContentsForTesting( /*exited_cleanly=*/true, /*crash_streak=*/num_crashes) .data())); TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_path); histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", BeaconFileState::kReadable, 1); EXPECT_TRUE(clean_exit_beacon.exited_cleanly()); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", num_crashes, 1); } // Verify that successfully reading the beacon file's contents results in // correctly (a) setting the |did_previous_session_exit_cleanly_| field and (b) // recording metrics when the last session did not exit cleanly. TEST_F(CleanExitBeaconTest, InitWithCrashAndBeaconFile) { const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); const base::FilePath temp_beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); const int last_session_num_crashes = 2; ASSERT_LT(0, base::WriteFile(temp_beacon_file_path, CleanExitBeacon::CreateBeaconFileContentsForTesting( /*exited_cleanly=*/false, /*crash_streak=*/last_session_num_crashes) .data())); const int updated_num_crashes = last_session_num_crashes + 1; TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_path); histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", BeaconFileState::kReadable, 1); EXPECT_FALSE(clean_exit_beacon.exited_cleanly()); histogram_tester_.ExpectUniqueSample("Variations.SafeMode.Streak.Crashes", updated_num_crashes, 1); } TEST_F(CleanExitBeaconTest, WriteBeaconValueWhenNotExitingCleanly) { const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); const base::FilePath beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); ASSERT_FALSE(base::PathExists(beacon_file_path)); TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_path); clean_exit_beacon.WriteBeaconValue(/*exited_cleanly=*/false, /*is_extended_safe_mode=*/true); // Verify that the beacon file exists and has well-formed contents after // updating the beacon value. EXPECT_TRUE(base::PathExists(beacon_file_path)); std::string beacon_file_contents1; ASSERT_TRUE(base::ReadFileToString(beacon_file_path, &beacon_file_contents1)); EXPECT_EQ(beacon_file_contents1, "{\"user_experience_metrics.stability.exited_cleanly\":false," "\"variations_crash_streak\":0}"); // Verify that the BeaconFileWrite metric was emitted. histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileWrite", 1, 1); // Write the beacon value again. This is done because it is possible for // WriteBeaconValue() to be called twice during startup or shutdown with the // same value for |exited_cleanly|. clean_exit_beacon.WriteBeaconValue(/*exited_cleanly*/ false, /*is_extended_safe_mode=*/false); // Verify that the beacon file exists and has well-formed contents after // updating the beacon value. EXPECT_TRUE(base::PathExists(beacon_file_path)); std::string beacon_file_contents2; ASSERT_TRUE(base::ReadFileToString(beacon_file_path, &beacon_file_contents2)); EXPECT_EQ(beacon_file_contents2, "{\"user_experience_metrics.stability.exited_cleanly\":false," "\"variations_crash_streak\":0}"); // Verify that the BeaconFileWrite metric was not emitted a second time. The // beacon file should not have been written again since the beacon value did // not change. histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileWrite", 1, 1); } TEST_F(CleanExitBeaconTest, WriteBeaconValueWhenExitingCleanly) { const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); const base::FilePath beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); ASSERT_FALSE(base::PathExists(beacon_file_path)); TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_path); clean_exit_beacon.WriteBeaconValue(/*exited_cleanly=*/true, /*is_extended_safe_mode=*/false); // Verify that the beacon file exists and has well-formed contents after // updating the beacon value. EXPECT_TRUE(base::PathExists(beacon_file_path)); std::string beacon_file_contents1; ASSERT_TRUE(base::ReadFileToString(beacon_file_path, &beacon_file_contents1)); EXPECT_EQ(beacon_file_contents1, "{\"user_experience_metrics.stability.exited_cleanly\":true," "\"variations_crash_streak\":0}"); // Verify that the BeaconFileWrite metric was emitted. histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileWrite", 1, 1); // Write the beacon value again. This is done because it is possible for // WriteBeaconValue() to be called twice during startup or shutdown with the // same value for |exited_cleanly|. clean_exit_beacon.WriteBeaconValue(/*exited_cleanly*/ true, /*is_extended_safe_mode=*/false); // Verify that the beacon file exists and has well-formed contents after // updating the beacon value. EXPECT_TRUE(base::PathExists(beacon_file_path)); std::string beacon_file_contents2; ASSERT_TRUE(base::ReadFileToString(beacon_file_path, &beacon_file_contents2)); EXPECT_EQ(beacon_file_contents2, "{\"user_experience_metrics.stability.exited_cleanly\":true," "\"variations_crash_streak\":0}"); // Verify that the BeaconFileWrite metric was not emitted a second time. The // beacon file should not have been written again since the beacon value did // not change. histogram_tester_.ExpectUniqueSample( "Variations.ExtendedSafeMode.BeaconFileWrite", 1, 1); } // Verify that there's a DCHECK when attempting to write a clean beacon with // |is_extended_safe_mode| set to true. When |is_extended_safe_mode| is true, // the only valid value for |exited_cleanly| is false. TEST_F(CleanExitBeaconTest, InvalidWriteBeaconValueArgsTriggerDcheck) { TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_.GetPath()); EXPECT_DCHECK_DEATH( clean_exit_beacon.WriteBeaconValue(/*exited_cleanly=*/true, /*is_extended_safe_mode=*/true)); } #if BUILDFLAG(IS_IOS) // Verify the logic for recording UMA.CleanExitBeaconConsistency3. INSTANTIATE_TEST_SUITE_P( All, BeaconFileAndPlatformBeaconConsistencyTest, ::testing::Values( BeaconConsistencyTestParams{ .test_name = "MissingMissing", .expected_consistency = CleanExitBeaconConsistency::kMissingMissing}, BeaconConsistencyTestParams{ .test_name = "MissingClean", .platform_specific_beacon_value = true, .expected_consistency = CleanExitBeaconConsistency::kMissingClean}, BeaconConsistencyTestParams{ .test_name = "MissingDirty", .platform_specific_beacon_value = false, .expected_consistency = CleanExitBeaconConsistency::kMissingDirty}, BeaconConsistencyTestParams{ .test_name = "CleanMissing", .beacon_file_beacon_value = true, .expected_consistency = CleanExitBeaconConsistency::kCleanMissing}, BeaconConsistencyTestParams{ .test_name = "DirtyMissing", .beacon_file_beacon_value = false, .expected_consistency = CleanExitBeaconConsistency::kDirtyMissing}, BeaconConsistencyTestParams{ .test_name = "CleanClean", .beacon_file_beacon_value = true, .platform_specific_beacon_value = true, .expected_consistency = CleanExitBeaconConsistency::kCleanClean}, BeaconConsistencyTestParams{ .test_name = "CleanDirty", .beacon_file_beacon_value = true, .platform_specific_beacon_value = false, .expected_consistency = CleanExitBeaconConsistency::kCleanDirty}, BeaconConsistencyTestParams{ .test_name = "DirtyClean", .beacon_file_beacon_value = false, .platform_specific_beacon_value = true, .expected_consistency = CleanExitBeaconConsistency::kDirtyClean}, BeaconConsistencyTestParams{ .test_name = "DirtyDirty", .beacon_file_beacon_value = false, .platform_specific_beacon_value = false, .expected_consistency = CleanExitBeaconConsistency::kDirtyDirty}), [](const ::testing::TestParamInfo& params) { return params.param.test_name; }); TEST_P(BeaconFileAndPlatformBeaconConsistencyTest, BeaconConsistency) { // Verify that the beacon file is not present. Unless set below, this beacon // is considered missing. const base::FilePath user_data_dir_path = user_data_dir_.GetPath(); const base::FilePath temp_beacon_file_path = user_data_dir_path.Append(kCleanExitBeaconFilename); ASSERT_FALSE(base::PathExists(temp_beacon_file_path)); // Clear the platform-specific beacon. Unless set below, this beacon is also // considered missing. CleanExitBeacon::ResetStabilityExitedCleanlyForTesting(&prefs_); BeaconConsistencyTestParams params = GetParam(); if (params.beacon_file_beacon_value) { ASSERT_LT( 0, base::WriteFile( temp_beacon_file_path, CleanExitBeacon::CreateBeaconFileContentsForTesting( /*exited_cleanly=*/params.beacon_file_beacon_value.value(), /*crash_streak=*/0) .data())); } if (params.platform_specific_beacon_value) { CleanExitBeacon::SetUserDefaultsBeacon( /*exited_cleanly=*/params.platform_specific_beacon_value.value()); } TestCleanExitBeacon clean_exit_beacon(&prefs_, user_data_dir_path); histogram_tester_.ExpectUniqueSample("UMA.CleanExitBeaconConsistency3", params.expected_consistency, 1); } #endif // BUILDFLAG(IS_IOS) } // namespace metrics