481 lines
17 KiB
C++
481 lines
17 KiB
C++
// Copyright 2012 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "base/files/important_file_writer.h"
|
|
|
|
#include "base/compiler_specific.h"
|
|
#include "base/files/file_path.h"
|
|
#include "base/files/file_util.h"
|
|
#include "base/files/scoped_temp_dir.h"
|
|
#include "base/functional/bind.h"
|
|
#include "base/location.h"
|
|
#include "base/memory/ptr_util.h"
|
|
#include "base/notreached.h"
|
|
#include "base/run_loop.h"
|
|
#include "base/sequence_checker.h"
|
|
#include "base/task/single_thread_task_runner.h"
|
|
#include "base/test/bind.h"
|
|
#include "base/test/metrics/histogram_tester.h"
|
|
#include "base/test/task_environment.h"
|
|
#include "base/threading/thread.h"
|
|
#include "base/time/time.h"
|
|
#include "base/timer/mock_timer.h"
|
|
#include "testing/gtest/include/gtest/gtest.h"
|
|
#include "third_party/abseil-cpp/absl/types/optional.h"
|
|
|
|
namespace base {
|
|
|
|
namespace {
|
|
|
|
std::string GetFileContent(const FilePath& path) {
|
|
std::string content;
|
|
if (!ReadFileToString(path, &content)) {
|
|
NOTREACHED();
|
|
}
|
|
return content;
|
|
}
|
|
|
|
class DataSerializer : public ImportantFileWriter::DataSerializer {
|
|
public:
|
|
explicit DataSerializer(const std::string& data) : data_(data) {
|
|
}
|
|
|
|
absl::optional<std::string> SerializeData() override {
|
|
EXPECT_TRUE(sequence_checker_.CalledOnValidSequence());
|
|
return data_;
|
|
}
|
|
|
|
private:
|
|
const base::SequenceChecker sequence_checker_;
|
|
const std::string data_;
|
|
};
|
|
|
|
class FailingDataSerializer : public ImportantFileWriter::DataSerializer {
|
|
public:
|
|
absl::optional<std::string> SerializeData() override { return absl::nullopt; }
|
|
};
|
|
|
|
class BackgroundDataSerializer
|
|
: public ImportantFileWriter::BackgroundDataSerializer {
|
|
public:
|
|
explicit BackgroundDataSerializer(
|
|
ImportantFileWriter::BackgroundDataProducerCallback
|
|
data_producer_callback)
|
|
: data_producer_callback_(std::move(data_producer_callback)) {
|
|
DCHECK(data_producer_callback_);
|
|
}
|
|
|
|
ImportantFileWriter::BackgroundDataProducerCallback
|
|
GetSerializedDataProducerForBackgroundSequence() override {
|
|
EXPECT_TRUE(sequence_checker_.CalledOnValidSequence());
|
|
return std::move(data_producer_callback_);
|
|
}
|
|
|
|
bool producer_callback_obtained() const {
|
|
return data_producer_callback_.is_null();
|
|
}
|
|
|
|
private:
|
|
const base::SequenceChecker sequence_checker_;
|
|
ImportantFileWriter::BackgroundDataProducerCallback data_producer_callback_;
|
|
};
|
|
|
|
enum WriteCallbackObservationState {
|
|
NOT_CALLED,
|
|
CALLED_WITH_ERROR,
|
|
CALLED_WITH_SUCCESS,
|
|
};
|
|
|
|
class WriteCallbacksObserver {
|
|
public:
|
|
WriteCallbacksObserver() = default;
|
|
WriteCallbacksObserver(const WriteCallbacksObserver&) = delete;
|
|
WriteCallbacksObserver& operator=(const WriteCallbacksObserver&) = delete;
|
|
|
|
// Register OnBeforeWrite() and OnAfterWrite() to be called on the next write
|
|
// of |writer|.
|
|
void ObserveNextWriteCallbacks(ImportantFileWriter* writer);
|
|
|
|
// Returns the |WriteCallbackObservationState| which was observed, then resets
|
|
// it to |NOT_CALLED|.
|
|
WriteCallbackObservationState GetAndResetObservationState();
|
|
|
|
private:
|
|
void OnBeforeWrite() {
|
|
EXPECT_FALSE(before_write_called_);
|
|
before_write_called_ = true;
|
|
}
|
|
|
|
void OnAfterWrite(bool success) {
|
|
EXPECT_EQ(NOT_CALLED, after_write_observation_state_);
|
|
after_write_observation_state_ =
|
|
success ? CALLED_WITH_SUCCESS : CALLED_WITH_ERROR;
|
|
}
|
|
|
|
bool before_write_called_ = false;
|
|
WriteCallbackObservationState after_write_observation_state_ = NOT_CALLED;
|
|
};
|
|
|
|
void WriteCallbacksObserver::ObserveNextWriteCallbacks(
|
|
ImportantFileWriter* writer) {
|
|
writer->RegisterOnNextWriteCallbacks(
|
|
base::BindOnce(&WriteCallbacksObserver::OnBeforeWrite,
|
|
base::Unretained(this)),
|
|
base::BindOnce(&WriteCallbacksObserver::OnAfterWrite,
|
|
base::Unretained(this)));
|
|
}
|
|
|
|
WriteCallbackObservationState
|
|
WriteCallbacksObserver::GetAndResetObservationState() {
|
|
EXPECT_EQ(after_write_observation_state_ != NOT_CALLED, before_write_called_)
|
|
<< "The before-write callback should always be called before the "
|
|
"after-write callback";
|
|
|
|
WriteCallbackObservationState state = after_write_observation_state_;
|
|
before_write_called_ = false;
|
|
after_write_observation_state_ = NOT_CALLED;
|
|
return state;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class ImportantFileWriterTest : public testing::Test {
|
|
public:
|
|
ImportantFileWriterTest() = default;
|
|
void SetUp() override {
|
|
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
|
|
file_ = temp_dir_.GetPath().AppendASCII("test-file");
|
|
}
|
|
|
|
protected:
|
|
WriteCallbacksObserver write_callback_observer_;
|
|
FilePath file_;
|
|
test::TaskEnvironment task_environment_;
|
|
|
|
private:
|
|
ScopedTempDir temp_dir_;
|
|
};
|
|
|
|
TEST_F(ImportantFileWriterTest, Basic) {
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
writer.WriteNow("foo");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, WriteWithObserver) {
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
|
|
// Confirm that the observer is invoked.
|
|
write_callback_observer_.ObserveNextWriteCallbacks(&writer);
|
|
writer.WriteNow("foo");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
EXPECT_EQ(CALLED_WITH_SUCCESS,
|
|
write_callback_observer_.GetAndResetObservationState());
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
|
|
// Confirm that re-installing the observer works for another write.
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
write_callback_observer_.ObserveNextWriteCallbacks(&writer);
|
|
writer.WriteNow("bar");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
EXPECT_EQ(CALLED_WITH_SUCCESS,
|
|
write_callback_observer_.GetAndResetObservationState());
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("bar", GetFileContent(writer.path()));
|
|
|
|
// Confirm that writing again without re-installing the observer doesn't
|
|
// result in a notification.
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
writer.WriteNow("baz");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("baz", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, FailedWriteWithObserver) {
|
|
// Use an invalid file path (relative paths are invalid) to get a
|
|
// FILE_ERROR_ACCESS_DENIED error when trying to write the file.
|
|
ImportantFileWriter writer(FilePath().AppendASCII("bad/../path"),
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
write_callback_observer_.ObserveNextWriteCallbacks(&writer);
|
|
writer.WriteNow("foo");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
// Confirm that the write observer was invoked with its boolean parameter set
|
|
// to false.
|
|
EXPECT_EQ(CALLED_WITH_ERROR,
|
|
write_callback_observer_.GetAndResetObservationState());
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, CallbackRunsOnWriterThread) {
|
|
base::Thread file_writer_thread("ImportantFileWriter test thread");
|
|
file_writer_thread.Start();
|
|
ImportantFileWriter writer(file_, file_writer_thread.task_runner());
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
|
|
// Block execution on |file_writer_thread| to verify that callbacks are
|
|
// executed on it.
|
|
base::WaitableEvent wait_helper(
|
|
base::WaitableEvent::ResetPolicy::MANUAL,
|
|
base::WaitableEvent::InitialState::NOT_SIGNALED);
|
|
file_writer_thread.task_runner()->PostTask(
|
|
FROM_HERE, base::BindOnce(&base::WaitableEvent::Wait,
|
|
base::Unretained(&wait_helper)));
|
|
|
|
write_callback_observer_.ObserveNextWriteCallbacks(&writer);
|
|
writer.WriteNow("foo");
|
|
RunLoop().RunUntilIdle();
|
|
|
|
// Expect the callback to not have been executed before the
|
|
// |file_writer_thread| is unblocked.
|
|
EXPECT_EQ(NOT_CALLED, write_callback_observer_.GetAndResetObservationState());
|
|
|
|
wait_helper.Signal();
|
|
file_writer_thread.FlushForTesting();
|
|
|
|
EXPECT_EQ(CALLED_WITH_SUCCESS,
|
|
write_callback_observer_.GetAndResetObservationState());
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, ScheduleWrite) {
|
|
constexpr TimeDelta kCommitInterval = Seconds(12345);
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_, SingleThreadTaskRunner::GetCurrentDefault(),
|
|
kCommitInterval);
|
|
EXPECT_EQ(0u, writer.previous_data_size());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
DataSerializer serializer("foo");
|
|
writer.ScheduleWrite(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
ASSERT_TRUE(timer.IsRunning());
|
|
EXPECT_EQ(kCommitInterval, timer.GetCurrentDelay());
|
|
timer.Fire();
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
EXPECT_FALSE(timer.IsRunning());
|
|
RunLoop().RunUntilIdle();
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
EXPECT_EQ(3u, writer.previous_data_size());
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, DoScheduledWrite) {
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
DataSerializer serializer("foo");
|
|
writer.ScheduleWrite(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
writer.DoScheduledWrite();
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
RunLoop().RunUntilIdle();
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, BatchingWrites) {
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
writer.SetTimerForTesting(&timer);
|
|
DataSerializer foo("foo"), bar("bar"), baz("baz");
|
|
writer.ScheduleWrite(&foo);
|
|
writer.ScheduleWrite(&bar);
|
|
writer.ScheduleWrite(&baz);
|
|
ASSERT_TRUE(timer.IsRunning());
|
|
timer.Fire();
|
|
RunLoop().RunUntilIdle();
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("baz", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, ScheduleWrite_FailToSerialize) {
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
FailingDataSerializer serializer;
|
|
writer.ScheduleWrite(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
ASSERT_TRUE(timer.IsRunning());
|
|
timer.Fire();
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
RunLoop().RunUntilIdle();
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, ScheduleWrite_WriteNow) {
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
DataSerializer serializer("foo");
|
|
writer.ScheduleWrite(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
writer.WriteNow("bar");
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
EXPECT_FALSE(timer.IsRunning());
|
|
|
|
RunLoop().RunUntilIdle();
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("bar", GetFileContent(writer.path()));
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, DoScheduledWrite_FailToSerialize) {
|
|
base::HistogramTester histogram_tester;
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
FailingDataSerializer serializer;
|
|
writer.ScheduleWrite(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
|
|
writer.DoScheduledWrite();
|
|
EXPECT_FALSE(timer.IsRunning());
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
RunLoop().RunUntilIdle();
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
// We don't record metrics in case the serialization fails.
|
|
histogram_tester.ExpectTotalCount("ImportantFile.SerializationDuration", 0);
|
|
histogram_tester.ExpectTotalCount("ImportantFile.WriteDuration", 0);
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest, ScheduleWriteWithBackgroundDataSerializer) {
|
|
base::HistogramTester histogram_tester;
|
|
base::Thread file_writer_thread("ImportantFileWriter test thread");
|
|
file_writer_thread.Start();
|
|
constexpr TimeDelta kCommitInterval = Seconds(12345);
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_, file_writer_thread.task_runner(),
|
|
kCommitInterval);
|
|
EXPECT_EQ(0u, writer.previous_data_size());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
ASSERT_FALSE(file_writer_thread.task_runner()->RunsTasksInCurrentSequence());
|
|
BackgroundDataSerializer serializer(
|
|
base::BindLambdaForTesting([&]() -> absl::optional<std::string> {
|
|
EXPECT_TRUE(
|
|
file_writer_thread.task_runner()->RunsTasksInCurrentSequence());
|
|
return "foo";
|
|
}));
|
|
writer.ScheduleWriteWithBackgroundDataSerializer(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
EXPECT_FALSE(serializer.producer_callback_obtained());
|
|
ASSERT_TRUE(timer.IsRunning());
|
|
EXPECT_EQ(kCommitInterval, timer.GetCurrentDelay());
|
|
|
|
timer.Fire();
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
EXPECT_TRUE(serializer.producer_callback_obtained());
|
|
EXPECT_FALSE(timer.IsRunning());
|
|
file_writer_thread.FlushForTesting();
|
|
ASSERT_TRUE(PathExists(writer.path()));
|
|
EXPECT_EQ("foo", GetFileContent(writer.path()));
|
|
histogram_tester.ExpectTotalCount("ImportantFile.SerializationDuration", 1);
|
|
histogram_tester.ExpectTotalCount("ImportantFile.WriteDuration", 1);
|
|
}
|
|
|
|
TEST_F(ImportantFileWriterTest,
|
|
ScheduleWriteWithBackgroundDataSerializer_FailToSerialize) {
|
|
base::HistogramTester histogram_tester;
|
|
base::Thread file_writer_thread("ImportantFileWriter test thread");
|
|
file_writer_thread.Start();
|
|
constexpr TimeDelta kCommitInterval = Seconds(12345);
|
|
MockOneShotTimer timer;
|
|
ImportantFileWriter writer(file_, file_writer_thread.task_runner(),
|
|
kCommitInterval);
|
|
EXPECT_EQ(0u, writer.previous_data_size());
|
|
writer.SetTimerForTesting(&timer);
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
ASSERT_FALSE(file_writer_thread.task_runner()->RunsTasksInCurrentSequence());
|
|
BackgroundDataSerializer serializer(
|
|
base::BindLambdaForTesting([&]() -> absl::optional<std::string> {
|
|
EXPECT_TRUE(
|
|
file_writer_thread.task_runner()->RunsTasksInCurrentSequence());
|
|
return absl::nullopt;
|
|
}));
|
|
writer.ScheduleWriteWithBackgroundDataSerializer(&serializer);
|
|
EXPECT_TRUE(writer.HasPendingWrite());
|
|
EXPECT_FALSE(serializer.producer_callback_obtained());
|
|
EXPECT_TRUE(timer.IsRunning());
|
|
|
|
timer.Fire();
|
|
EXPECT_FALSE(timer.IsRunning());
|
|
EXPECT_TRUE(serializer.producer_callback_obtained());
|
|
EXPECT_FALSE(writer.HasPendingWrite());
|
|
file_writer_thread.FlushForTesting();
|
|
EXPECT_FALSE(PathExists(writer.path()));
|
|
// We record the foreground serialization metric despite later failure in
|
|
// background sequence.
|
|
histogram_tester.ExpectTotalCount("ImportantFile.SerializationDuration", 1);
|
|
histogram_tester.ExpectTotalCount("ImportantFile.WriteDuration", 0);
|
|
}
|
|
|
|
// Test that the chunking to avoid very large writes works.
|
|
TEST_F(ImportantFileWriterTest, WriteLargeFile) {
|
|
// One byte larger than kMaxWriteAmount.
|
|
const std::string large_data(8 * 1024 * 1024 + 1, 'g');
|
|
EXPECT_FALSE(PathExists(file_));
|
|
EXPECT_TRUE(ImportantFileWriter::WriteFileAtomically(file_, large_data));
|
|
std::string actual;
|
|
EXPECT_TRUE(ReadFileToString(file_, &actual));
|
|
EXPECT_EQ(large_data, actual);
|
|
}
|
|
|
|
// Verify that a UMA metric for the serialization duration is recorded.
|
|
TEST_F(ImportantFileWriterTest, SerializationDuration) {
|
|
base::HistogramTester histogram_tester;
|
|
ImportantFileWriter writer(file_,
|
|
SingleThreadTaskRunner::GetCurrentDefault());
|
|
DataSerializer serializer("foo");
|
|
writer.ScheduleWrite(&serializer);
|
|
writer.DoScheduledWrite();
|
|
RunLoop().RunUntilIdle();
|
|
histogram_tester.ExpectTotalCount("ImportantFile.SerializationDuration", 1);
|
|
histogram_tester.ExpectTotalCount("ImportantFile.WriteDuration", 1);
|
|
}
|
|
|
|
// Verify that a UMA metric for the serialization duration is recorded if the
|
|
// ImportantFileWriter has a custom histogram suffix.
|
|
TEST_F(ImportantFileWriterTest, SerializationDurationWithCustomSuffix) {
|
|
base::HistogramTester histogram_tester;
|
|
ImportantFileWriter writer(file_, SingleThreadTaskRunner::GetCurrentDefault(),
|
|
"Foo");
|
|
DataSerializer serializer("foo");
|
|
writer.ScheduleWrite(&serializer);
|
|
writer.DoScheduledWrite();
|
|
RunLoop().RunUntilIdle();
|
|
histogram_tester.ExpectTotalCount("ImportantFile.SerializationDuration.Foo",
|
|
1);
|
|
histogram_tester.ExpectTotalCount("ImportantFile.WriteDuration.Foo", 1);
|
|
}
|
|
|
|
} // namespace base
|