| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| #include "pw_transfer/atomic_file_transfer_handler.h" |
| |
| #include <cinttypes> |
| #include <filesystem> |
| #include <fstream> |
| #include <random> |
| #include <string> |
| #include <string_view> |
| |
| #include "gtest/gtest.h" |
| #include "pw_random/xor_shift.h" |
| #include "pw_result/result.h" |
| #include "pw_status/status.h" |
| #include "pw_string/string_builder.h" |
| #include "pw_transfer/transfer.h" |
| #include "pw_transfer_private/filename_generator.h" |
| |
| namespace pw::transfer { |
| |
| namespace { |
| |
| // Copied from go/pw-src/+/main:pw_stream/std_file_stream_test.cc;l=75 |
| class TempDir { |
| public: |
| TempDir(std::string_view prefix) : rng_(GetSeed()) { |
| temp_dir_ = std::filesystem::temp_directory_path(); |
| temp_dir_ /= std::string(prefix) + GetRandomSuffix(); |
| PW_ASSERT(std::filesystem::create_directory(temp_dir_)); |
| } |
| |
| ~TempDir() { PW_ASSERT(std::filesystem::remove_all(temp_dir_)); } |
| |
| std::filesystem::path GetTempFileName() { |
| return temp_dir_ / GetRandomSuffix(); |
| } |
| |
| private: |
| std::string GetRandomSuffix() { |
| StringBuffer<9> random_suffix_str; |
| uint32_t random_suffix_int = 0; |
| rng_.GetInt(random_suffix_int); |
| PW_ASSERT(random_suffix_str.Format("%08" PRIx32, random_suffix_int).ok()); |
| return std::string(random_suffix_str.view()); |
| } |
| |
| // Generate a 64-bit random from system entropy pool. This is used to seed a |
| // pseudo-random number generator for individual file names. |
| static uint64_t GetSeed() { |
| std::random_device sys_rand; |
| uint64_t seed = 0; |
| for (size_t seed_bytes = 0; seed_bytes < sizeof(seed); |
| seed_bytes += sizeof(std::random_device::result_type)) { |
| std::random_device::result_type val = sys_rand(); |
| seed = seed << 8 * sizeof(std::random_device::result_type); |
| seed |= val; |
| } |
| return seed; |
| } |
| |
| random::XorShiftStarRng64 rng_; |
| std::filesystem::path temp_dir_; |
| }; |
| |
| class AtomicFileTransferHandlerTest : public ::testing::Test { |
| public: |
| TempDir temp_dir_{"atomic_file_transfer_handler_test"}; |
| std::string test_data_location_pass_ = temp_dir_.GetTempFileName(); |
| std::string transfer_temp_file_ = GetTempFilePath(test_data_location_pass_); |
| |
| protected: |
| static constexpr auto test_data_location_fail = "not/a/directory/no_data.txt"; |
| static constexpr auto temp_file_content = "Temp File Success."; |
| static constexpr auto test_data_content = "Test File Success."; |
| |
| bool WriteContentFile(std::string_view path, std::string_view value) { |
| std::ofstream file(path); |
| if (!file.is_open()) { |
| return false; |
| } |
| file << value; |
| return true; |
| } |
| |
| Result<std::string> ReadFile(std::string_view path) { |
| std::ifstream file(path); |
| if (!file.is_open()) { |
| return Status::NotFound(); |
| } |
| std::string return_value; |
| std::getline(file, return_value); |
| return return_value; |
| } |
| |
| void ClearContent(std::string_view path) { |
| std::ofstream ofs(path, std::ofstream::out | std::ofstream::trunc); |
| } |
| |
| void check_finalize(Status status) { |
| EXPECT_EQ(status, OkStatus()); |
| // Temp file does not exist after finalize. |
| EXPECT_TRUE(!std::filesystem::exists(transfer_temp_file_)); |
| // Test path does exist, file has been created. |
| EXPECT_TRUE(std::filesystem::exists(test_data_location_pass_)); |
| // File content is the same as expected. |
| const auto file_content = ReadFile(test_data_location_pass_); |
| ASSERT_TRUE(file_content.ok()); |
| |
| EXPECT_EQ(file_content.value(), temp_file_content); |
| } |
| |
| void SetUp() override { |
| // Write content file and check correct. |
| ASSERT_TRUE(WriteContentFile(test_data_location_pass_, test_data_content)); |
| const auto file_content_data = ReadFile(test_data_location_pass_); |
| ASSERT_TRUE(file_content_data.ok()); |
| ASSERT_EQ(file_content_data.value(), test_data_content); |
| |
| // Write temp file and check content is correct |
| ASSERT_TRUE(WriteContentFile(transfer_temp_file_, temp_file_content)); |
| const auto file_content_tmp = ReadFile(transfer_temp_file_); |
| ASSERT_TRUE(file_content_tmp.ok()); |
| ASSERT_EQ(file_content_tmp.value(), temp_file_content); |
| } |
| |
| void TearDown() override { |
| // Ensure temp file is deleted. |
| ASSERT_TRUE(!std::filesystem::exists(transfer_temp_file_) || |
| std::filesystem::remove(transfer_temp_file_)); |
| // Ensure test file is deleted. |
| ASSERT_TRUE(!std::filesystem::exists(test_data_location_pass_) || |
| std::filesystem::remove(test_data_location_pass_)); |
| } |
| }; |
| |
| TEST_F(AtomicFileTransferHandlerTest, PrepareReadPass) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ 0, |
| test_data_location_pass_}; |
| EXPECT_EQ(test_handler.PrepareRead(), OkStatus()); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, PrepareReadFail) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ 0, |
| test_data_location_fail}; |
| EXPECT_EQ(test_handler.PrepareRead(), Status::NotFound()); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, PrepareWritePass) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ 0, |
| test_data_location_pass_}; |
| // Open a file for write returns OkStatus. |
| EXPECT_EQ(test_handler.PrepareWrite(), OkStatus()); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, PrepareWriteFail) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ 0, |
| test_data_location_fail}; |
| // Open a file with non existing path pass. |
| // No access to underlying stream |
| // so rely on the write during transfer to catch the error. |
| EXPECT_EQ(test_handler.PrepareWrite(), OkStatus()); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteRenameExisting) { |
| ASSERT_TRUE(std::filesystem::exists(transfer_temp_file_)); |
| ASSERT_TRUE(std::filesystem::exists(test_data_location_pass_)); |
| AtomicFileTransferHandler test_handler{/*resource_id = */ |
| 0, |
| test_data_location_pass_}; |
| // Prepare Write to open the stream. should be closed during Finalize. |
| ASSERT_EQ(test_handler.PrepareWrite(), OkStatus()); |
| WriteContentFile(transfer_temp_file_, temp_file_content); |
| auto status = test_handler.FinalizeWrite(OkStatus()); |
| check_finalize(status); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteNoExistingFile) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ |
| 0, |
| test_data_location_pass_}; |
| // Remove file test file and test creation. |
| ASSERT_TRUE(std::filesystem::remove(test_data_location_pass_)); |
| ASSERT_EQ(test_handler.PrepareWrite(), OkStatus()); |
| WriteContentFile(transfer_temp_file_, temp_file_content); |
| auto status = test_handler.FinalizeWrite(OkStatus()); |
| check_finalize(status); |
| } |
| |
| TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteExpectErr) { |
| AtomicFileTransferHandler test_handler{/*resource_id = */ |
| 0, |
| test_data_location_pass_}; |
| ASSERT_EQ(test_handler.PrepareWrite(), OkStatus()); |
| // Simulate write fails, file is empty, No write here. |
| ClearContent(transfer_temp_file_); |
| ASSERT_TRUE(std::filesystem::is_empty(transfer_temp_file_)); |
| ASSERT_TRUE(std::filesystem::exists(test_data_location_pass_)); |
| EXPECT_EQ(test_handler.FinalizeWrite(Status::DataLoss()), Status::DataLoss()); |
| } |
| |
| } // namespace |
| |
| } // namespace pw::transfer |