| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/metrics/serialization/serialization_utils.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include "base/check.h" |
| #include "base/containers/span.h" |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "components/metrics/serialization/metric_sample.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace metrics { |
| namespace { |
| |
| using ::testing::IsEmpty; |
| |
| class SerializationUtilsTest : public testing::Test { |
| protected: |
| SerializationUtilsTest() { |
| bool success = temporary_dir_.CreateUniqueTempDir(); |
| if (success) { |
| base::FilePath dir_path = temporary_dir_.GetPath(); |
| filename_ = dir_path.value() + "chromeossampletest"; |
| filepath_ = base::FilePath(filename_); |
| } |
| } |
| |
| void SetUp() override { base::DeleteFile(filepath_); } |
| |
| void TestSerialization(MetricSample* sample) { |
| std::string serialized(sample->ToString()); |
| ASSERT_EQ('\0', serialized.back()); |
| std::unique_ptr<MetricSample> deserialized = |
| SerializationUtils::ParseSample(serialized); |
| ASSERT_TRUE(deserialized); |
| EXPECT_TRUE(sample->IsEqual(*deserialized.get())); |
| } |
| |
| const std::string& filename() const { return filename_; } |
| const base::FilePath& filepath() const { return filepath_; } |
| |
| private: |
| std::string filename_; |
| base::ScopedTempDir temporary_dir_; |
| base::FilePath filepath_; |
| }; |
| |
| TEST_F(SerializationUtilsTest, CrashSerializeTest) { |
| // Should work with both 1 and non-1 values |
| TestSerialization(MetricSample::CrashSample("test", /*num_samples=*/1).get()); |
| TestSerialization( |
| MetricSample::CrashSample("test", /*num_samples=*/10).get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, HistogramSerializeTest) { |
| TestSerialization(MetricSample::HistogramSample( |
| "myhist", /*sample=*/13, /*min=*/1, /*max=*/100, |
| /*bucket_count=*/10, /*num_samples=*/1) |
| .get()); |
| TestSerialization(MetricSample::HistogramSample( |
| "myhist", /*sample=*/13, /*min=*/1, /*max=*/100, |
| /*bucket_count=*/10, /*num_samples=*/2) |
| .get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, LinearSerializeTest) { |
| TestSerialization( |
| MetricSample::LinearHistogramSample("linearhist", /*sample=*/12, |
| /*max=*/30, /*num_samples=*/1) |
| .get()); |
| TestSerialization( |
| MetricSample::LinearHistogramSample("linearhist", /*sample=*/12, |
| /*max=*/30, /*num_samples=*/10) |
| .get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, SparseSerializeTest) { |
| TestSerialization(MetricSample::SparseHistogramSample( |
| "mysparse", /*sample=*/30, /*num_samples=*/1) |
| .get()); |
| TestSerialization(MetricSample::SparseHistogramSample( |
| "mysparse", /*sample=*/30, /*num_samples=*/10) |
| .get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, UserActionSerializeTest) { |
| TestSerialization( |
| MetricSample::UserActionSample("myaction", /*num_samples=*/1).get()); |
| TestSerialization( |
| MetricSample::UserActionSample("myaction", /*num_samples=*/10).get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, InvalidCrashSerialize) { |
| // No name |
| EXPECT_EQ(nullptr, MetricSample::ParseCrash("")); |
| // Empty name |
| EXPECT_EQ(nullptr, MetricSample::ParseCrash(" ")); |
| // num_samples is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseCrash("kernel asdf")); |
| // Too many numbers |
| EXPECT_EQ(nullptr, MetricSample::ParseCrash("kernel 1 2")); |
| // Negative num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseCrash("kernel -1")); |
| } |
| |
| TEST_F(SerializationUtilsTest, InvalidHistogramSample) { |
| // Too few parts |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 3")); |
| // Too many parts |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 3 4 5 6")); |
| // Empty hist name |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram(" 1 2 3 4 5")); |
| // sample is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist a 2 3 4 5")); |
| // min is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 a 3 4 5")); |
| // max is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 a 4 5")); |
| // buckets is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 3 a 5")); |
| // num_samples is not a number |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 3 4 a")); |
| // Negative num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseHistogram("hist 1 2 3 4 -1")); |
| } |
| |
| TEST_F(SerializationUtilsTest, InvalidSparseHistogramSample) { |
| // Too few fields |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram("name")); |
| // Too many fields |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram("name 1 2 3")); |
| // No name |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram(" 1 2")); |
| // Invalid sample |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram("name a 2")); |
| // Invalid num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram("name 1 a")); |
| // Negative num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram("name 1 -1")); |
| } |
| |
| TEST_F(SerializationUtilsTest, InvalidLinearHistogramSample) { |
| // Too few fields |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name 1")); |
| // Too many fields |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name 1 2 3 4")); |
| // No name |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram(" 1 2 3")); |
| // Invalid sample |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name a 2 3")); |
| // Invalid max |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name 1 a 3")); |
| // Invalid num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name 1 2 a")); |
| // Negative num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseLinearHistogram("name 1 2 -1")); |
| } |
| |
| TEST_F(SerializationUtilsTest, InvalidUserAction) { |
| // Too few fields |
| EXPECT_EQ(nullptr, MetricSample::ParseUserAction("")); |
| // Too many fields |
| EXPECT_EQ(nullptr, MetricSample::ParseUserAction("name 1 2")); |
| // No name |
| EXPECT_EQ(nullptr, MetricSample::ParseUserAction(" 1")); |
| // Invalid num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseUserAction("name a")); |
| // Negative num_samples |
| EXPECT_EQ(nullptr, MetricSample::ParseUserAction("name -1")); |
| } |
| |
| TEST_F(SerializationUtilsTest, IllegalNameAreFilteredTest) { |
| std::unique_ptr<MetricSample> sample1 = MetricSample::SparseHistogramSample( |
| "no space", /*sample=*/10, /*num_samples=*/1); |
| std::unique_ptr<MetricSample> sample2 = MetricSample::LinearHistogramSample( |
| base::StringPrintf("here%cbhe", '\0'), /*sample=*/1, /*max=*/3, |
| /*num_samples=*/2); |
| |
| EXPECT_FALSE( |
| SerializationUtils::WriteMetricToFile(*sample1.get(), filename())); |
| EXPECT_FALSE( |
| SerializationUtils::WriteMetricToFile(*sample2.get(), filename())); |
| |
| ASSERT_FALSE(base::PathExists(filepath())); |
| } |
| |
| TEST_F(SerializationUtilsTest, BadInputIsCaughtTest) { |
| std::string input( |
| base::StringPrintf("sparsehistogram%cname foo%c1", '\0', '\0')); |
| EXPECT_EQ(nullptr, MetricSample::ParseSparseHistogram(input).get()); |
| } |
| |
| TEST_F(SerializationUtilsTest, MessageSeparatedByZero) { |
| std::unique_ptr<MetricSample> crash = |
| MetricSample::CrashSample("mycrash", /*num_samples=*/10); |
| |
| SerializationUtils::WriteMetricToFile(*crash.get(), filename()); |
| std::optional<int64_t> size = base::GetFileSize(filepath()); |
| |
| ASSERT_TRUE(size.has_value()); |
| // 4 bytes for the size |
| // 5 bytes for crash |
| // 1 byte for \0 |
| // 7 bytes for mycrash |
| // 3 bytes for " 10" |
| // 1 byte for \0 |
| // -> total of 21 |
| EXPECT_EQ(size.value(), 21); |
| } |
| |
| TEST_F(SerializationUtilsTest, MessagesTooLongAreDiscardedTest) { |
| // Creates a message that is bigger than the maximum allowed size. |
| // As we are adding extra character (crash, \0s, etc), if the name is |
| // kMessageMaxLength long, it will be too long. |
| std::string name(SerializationUtils::kMessageMaxLength, 'c'); |
| |
| std::unique_ptr<MetricSample> crash = |
| MetricSample::CrashSample(name, /*num_samples=*/10); |
| EXPECT_FALSE(SerializationUtils::WriteMetricToFile(*crash.get(), filename())); |
| |
| std::optional<int64_t> size = base::GetFileSize(filepath()); |
| ASSERT_TRUE(size.has_value()); |
| EXPECT_EQ(0, size.value()); |
| } |
| |
| TEST_F(SerializationUtilsTest, ReadLongMessageTest) { |
| base::File test_file(filepath(), |
| base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND); |
| std::string message(SerializationUtils::kMessageMaxLength + 1, 'c'); |
| |
| int32_t message_size = message.length() + sizeof(int32_t); |
| test_file.WriteAtCurrentPos(base::byte_span_from_ref(message_size)); |
| test_file.WriteAtCurrentPos(base::as_byte_span(message)); |
| test_file.Close(); |
| |
| std::unique_ptr<MetricSample> crash = |
| MetricSample::CrashSample("test", /*num_samples=*/10); |
| SerializationUtils::WriteMetricToFile(*crash.get(), filename()); |
| |
| std::vector<std::unique_ptr<MetricSample>> samples; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &samples); |
| ASSERT_EQ(size_t(1), samples.size()); |
| ASSERT_TRUE(samples[0].get() != nullptr); |
| EXPECT_TRUE(crash->IsEqual(*samples[0])); |
| } |
| |
| TEST_F(SerializationUtilsTest, NegativeLengthTest) { |
| // This input is specifically constructed to yield a single crash sample when |
| // parsed by a buggy version of the code but fails to parse and doesn't yield |
| // samples when parsed by a correct implementation. |
| constexpr uint8_t kInput[] = { |
| // Length indicating that next length field is the negative one below. |
| // This sample is invalid as it contains more than three null bytes. |
| 0x14, |
| 0x00, |
| 0x00, |
| 0x00, |
| // Encoding of a valid crash sample. |
| 0x0c, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x63, |
| 0x72, |
| 0x61, |
| 0x73, |
| 0x68, |
| 0x00, |
| 0x61, |
| 0x00, |
| // Invalid sample that jumps past the negative length bytes below. |
| 0x08, |
| 0x00, |
| 0x00, |
| 0x00, |
| // This is -16 in two's complement interpretation, pointing to the valid |
| // crash sample before. |
| 0xf0, |
| 0xff, |
| 0xff, |
| 0xff, |
| }; |
| ASSERT_TRUE(base::WriteFile(filepath(), base::span(kInput))); |
| |
| std::vector<std::unique_ptr<MetricSample>> samples; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &samples); |
| ASSERT_EQ(0U, samples.size()); |
| } |
| |
| TEST_F(SerializationUtilsTest, WriteReadTest_TruncateFile) { |
| std::unique_ptr<MetricSample> hist = MetricSample::HistogramSample( |
| "myhist", /*sample=*/1, /*min=*/2, /*max=*/3, /*bucket_count=*/4, |
| /*num_samples=*/5); |
| std::unique_ptr<MetricSample> crash = |
| MetricSample::CrashSample("mycrash", /*num_samples=*/10); |
| std::unique_ptr<MetricSample> lhist = MetricSample::LinearHistogramSample( |
| "linear", /*sample=*/1, /*max=*/10, /*num_samples=*/10); |
| std::unique_ptr<MetricSample> shist = MetricSample::SparseHistogramSample( |
| "mysparse", /*sample=*/30, /*num_samples=*/10); |
| std::unique_ptr<MetricSample> action = |
| MetricSample::UserActionSample("myaction", /*num_samples=*/1); |
| |
| SerializationUtils::WriteMetricToFile(*hist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*crash.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*lhist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*shist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*action.get(), filename()); |
| std::vector<std::unique_ptr<MetricSample>> vect; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &vect); |
| // NOTE: Should *not* have an entry for each repeated sample. |
| ASSERT_EQ(vect.size(), size_t(5)); |
| for (auto& sample : vect) { |
| ASSERT_NE(nullptr, sample.get()); |
| } |
| EXPECT_TRUE(hist->IsEqual(*vect[0])); |
| EXPECT_TRUE(crash->IsEqual(*vect[1])); |
| EXPECT_TRUE(lhist->IsEqual(*vect[2])); |
| EXPECT_TRUE(shist->IsEqual(*vect[3])); |
| EXPECT_TRUE(action->IsEqual(*vect[4])); |
| |
| std::optional<int64_t> size = base::GetFileSize(filepath()); |
| ASSERT_TRUE(size.has_value()); |
| ASSERT_EQ(0, size.value()); |
| } |
| |
| TEST_F(SerializationUtilsTest, WriteReadTest_DeleteFile) { |
| std::unique_ptr<MetricSample> hist = MetricSample::HistogramSample( |
| "myhist", /*sample=*/1, /*min=*/2, /*max=*/3, /*bucket_count=*/4, |
| /*num_samples=*/5); |
| std::unique_ptr<MetricSample> crash = |
| MetricSample::CrashSample("mycrash", /*num_samples=*/10); |
| std::unique_ptr<MetricSample> lhist = MetricSample::LinearHistogramSample( |
| "linear", /*sample=*/1, /*max=*/10, /*num_samples=*/10); |
| std::unique_ptr<MetricSample> shist = MetricSample::SparseHistogramSample( |
| "mysparse", /*sample=*/30, /*num_samples=*/10); |
| std::unique_ptr<MetricSample> action = |
| MetricSample::UserActionSample("myaction", /*num_samples=*/1); |
| |
| SerializationUtils::WriteMetricToFile(*hist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*crash.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*lhist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*shist.get(), filename()); |
| SerializationUtils::WriteMetricToFile(*action.get(), filename()); |
| std::vector<std::unique_ptr<MetricSample>> vect; |
| SerializationUtils::ReadAndDeleteMetricsFromFile(filename(), &vect); |
| // NOTE: Should *not* have an entry for each repeated sample. |
| ASSERT_EQ(vect.size(), size_t(5)); |
| for (auto& sample : vect) { |
| ASSERT_NE(nullptr, sample.get()); |
| } |
| EXPECT_TRUE(hist->IsEqual(*vect[0])); |
| EXPECT_TRUE(crash->IsEqual(*vect[1])); |
| EXPECT_TRUE(lhist->IsEqual(*vect[2])); |
| EXPECT_TRUE(shist->IsEqual(*vect[3])); |
| EXPECT_TRUE(action->IsEqual(*vect[4])); |
| |
| EXPECT_FALSE(base::PathExists(filepath())); |
| } |
| |
| TEST_F(SerializationUtilsTest, TooManyMessagesTest) { |
| std::unique_ptr<MetricSample> hist = MetricSample::HistogramSample( |
| "myhist", /*sample=*/1, /*min=*/2, /*max=*/3, /*bucket_count=*/4, |
| /*num_samples=*/5); |
| |
| constexpr int kDiscardedSamples = 50000; |
| for (int i = 0; |
| i < SerializationUtils::kMaxMessagesPerRead + kDiscardedSamples; i++) { |
| SerializationUtils::WriteMetricToFile(*hist.get(), filename()); |
| } |
| |
| std::vector<std::unique_ptr<MetricSample>> vect; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &vect); |
| ASSERT_EQ(SerializationUtils::kMaxMessagesPerRead, |
| static_cast<int>(vect.size())); |
| for (auto& sample : vect) { |
| ASSERT_NE(nullptr, sample.get()); |
| EXPECT_TRUE(hist->IsEqual(*sample)); |
| } |
| |
| std::optional<int64_t> size = base::GetFileSize(filepath()); |
| ASSERT_TRUE(size.has_value()); |
| ASSERT_EQ(0, size.value()); |
| } |
| |
| TEST_F(SerializationUtilsTest, ReadEmptyFile) { |
| { |
| // Create a zero-length file and then close file descriptor. |
| base::File file(filepath(), |
| base::File::FLAG_CREATE | base::File::FLAG_WRITE); |
| ASSERT_TRUE(file.IsValid()); |
| } |
| |
| std::vector<std::unique_ptr<MetricSample>> vect; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &vect); |
| EXPECT_THAT(vect, IsEmpty()); |
| } |
| |
| TEST_F(SerializationUtilsTest, ReadNonExistentFile) { |
| base::DeleteFile(filepath()); // Ensure non-existance. |
| base::HistogramTester histogram_tester; |
| std::vector<std::unique_ptr<MetricSample>> vect; |
| SerializationUtils::ReadAndTruncateMetricsFromFile(filename(), &vect); |
| EXPECT_THAT(vect, IsEmpty()); |
| } |
| |
| TEST_F(SerializationUtilsTest, ParseInvalidType) { |
| // Verify that parsing of an invalid sample type fails. |
| EXPECT_EQ(nullptr, SerializationUtils::ParseSample(base::StringPrintf( |
| "not_a_type%cvalue%c", '\0', '\0'))); |
| } |
| |
| } // namespace |
| } // namespace metrics |