216 lines
7.8 KiB
C++
216 lines
7.8 KiB
C++
// 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
|