Skip to content

Commit

Permalink
feat: Add file-based persistence handler (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrogTheFrog authored Jul 12, 2024
1 parent 66844be commit 704295f
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/common/filesettingspersistence.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// class header include
#include "displaydevice/filesettingspersistence.h"

// system includes
#include <algorithm>
#include <fstream>
#include <iterator>

// local includes
#include "displaydevice/logging.h"

namespace display_device {
FileSettingsPersistence::FileSettingsPersistence(std::filesystem::path filepath):
m_filepath { std::move(filepath) } {
if (m_filepath.empty()) {
throw std::runtime_error { "Empty filename provided for FileSettingsPersistence!" };
}
}

bool
FileSettingsPersistence::store(const std::vector<std::uint8_t> &data) {
try {
std::ofstream stream { m_filepath, std::ios::binary | std::ios::trunc };
if (!stream) {
DD_LOG(error) << "Failed to open " << m_filepath << " for writing!";
return false;
}

std::ranges::copy(data, std::ostreambuf_iterator<char> { stream });
return true;
}
catch (const std::exception &error) {
DD_LOG(error) << "Failed to write to " << m_filepath << "! Error:\n"
<< error.what();
return false;
}
}

std::optional<std::vector<std::uint8_t>>
FileSettingsPersistence::load() const {
if (std::error_code error_code; !std::filesystem::exists(m_filepath, error_code)) {
if (error_code) {
DD_LOG(error) << "Failed to load " << m_filepath << "! Error:\n"
<< "[" << error_code.value() << "] " << error_code.message();
return std::nullopt;
}

return std::vector<std::uint8_t> {};
}

try {
std::ifstream stream { m_filepath, std::ios::binary };
if (!stream) {
DD_LOG(error) << "Failed to open " << m_filepath << " for reading!";
return std::nullopt;
}

return std::vector<std::uint8_t> { std::istreambuf_iterator<char> { stream },
std::istreambuf_iterator<char> {} };
}
catch (const std::exception &error) {
DD_LOG(error) << "Failed to read " << m_filepath << "! Error:\n"
<< error.what();
return std::nullopt;
}
}

bool
FileSettingsPersistence::clear() {
// Return valud does not matter since we check the error code in case the file could NOT be removed.
std::error_code error_code;
std::filesystem::remove(m_filepath, error_code);

if (error_code) {
DD_LOG(error) << "Failed to remove " << m_filepath << "! Error:\n"
<< "[" << error_code.value() << "] " << error_code.message();
return false;
}

return true;
}
} // namespace display_device
48 changes: 48 additions & 0 deletions src/common/include/displaydevice/filesettingspersistence.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

// system includes
#include <filesystem>

// local includes
#include "settingspersistenceinterface.h"

namespace display_device {
/**
* @brief Implementation of the SettingsPersistenceInterface,
* that saves/loads the persistent settings to/from the file.
*/
class FileSettingsPersistence: public SettingsPersistenceInterface {
public:
/**
* Default constructor. Does not perform any operations on the file yet.
* @param filepath A non-empty filepath. Throws on empty.
*/
explicit FileSettingsPersistence(std::filesystem::path filepath);

/**
* Store the data in the file specified in constructor.
* @warning The method does not create missing directories!
* @see SettingsPersistenceInterface::store for more details.
*/
[[nodiscard]] bool
store(const std::vector<std::uint8_t> &data) override;

/**
* Read the data from the file specified in constructor.
* @note If file does not exist, an empty data list will be returned instead of null optional.
* @see SettingsPersistenceInterface::load for more details.
*/
[[nodiscard]] std::optional<std::vector<std::uint8_t>>
load() const override;

/**
* Remove the file specified in constructor (if it exists).
* @see SettingsPersistenceInterface::clear for more details.
*/
[[nodiscard]] bool
clear() override;

private:
std::filesystem::path m_filepath;
};
} // namespace display_device
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ namespace display_device {

/**
* @brief Clear the persistent settings data.
* @returns True if data was cleared, false otherwise.
*
* EXAMPLES:
* ```cpp
Expand Down
115 changes: 115 additions & 0 deletions tests/unit/general/test_filesettingspersistence.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// system includes
#include <fstream>
#include <gmock/gmock.h>

// local includes
#include "displaydevice/filesettingspersistence.h"
#include "fixtures/fixtures.h"

namespace {
// Convenience keywords for GMock
using ::testing::HasSubstr;

// Test fixture(s) for this file
class FileSettingsPersistenceTest: public BaseTest {
public:
~FileSettingsPersistenceTest() override {
std::filesystem::remove(m_filepath);
}

display_device::FileSettingsPersistence &
getImpl(const std::filesystem::path &filepath = "testfile.ext") {
if (!m_impl) {
m_filepath = filepath;
m_impl = std::make_unique<display_device::FileSettingsPersistence>(m_filepath);
}

return *m_impl;
}

private:
std::filesystem::path m_filepath;
std::unique_ptr<display_device::FileSettingsPersistence> m_impl;
};

// Specialized TEST macro(s) for this test file
#define TEST_F_S(...) DD_MAKE_TEST(TEST_F, FileSettingsPersistenceTest, __VA_ARGS__)
} // namespace

TEST_F_S(EmptyFilenameProvided) {
EXPECT_THAT([]() { const display_device::FileSettingsPersistence persistence { {} }; },
ThrowsMessage<std::runtime_error>(HasSubstr("Empty filename provided for FileSettingsPersistence!")));
}

TEST_F_S(Store, NewFileCreated) {
const std::filesystem::path filepath { "myfile.ext" };
const std::vector<std::uint8_t> data { 0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A' };

EXPECT_FALSE(std::filesystem::exists(filepath));
EXPECT_TRUE(getImpl(filepath).store(data));
EXPECT_TRUE(std::filesystem::exists(filepath));

std::ifstream stream { filepath, std::ios::binary };
std::vector<std::uint8_t> file_data { std::istreambuf_iterator<char> { stream }, std::istreambuf_iterator<char> {} };
EXPECT_EQ(file_data, data);
}

TEST_F_S(Store, FileOverwritten) {
const std::filesystem::path filepath { "myfile.ext" };
const std::vector<std::uint8_t> data1 { 0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A', ' ', '1' };
const std::vector<std::uint8_t> data2 { 0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A', ' ', '2' };

{
std::ofstream file { filepath, std::ios_base::binary };
std::ranges::copy(data1, std::ostreambuf_iterator<char> { file });
}

EXPECT_TRUE(std::filesystem::exists(filepath));
EXPECT_TRUE(getImpl(filepath).store(data2));
EXPECT_TRUE(std::filesystem::exists(filepath));

std::ifstream stream { filepath, std::ios::binary };
std::vector<std::uint8_t> file_data { std::istreambuf_iterator<char> { stream }, std::istreambuf_iterator<char> {} };
EXPECT_EQ(file_data, data2);
}

TEST_F_S(Store, FilepathWithDirectory) {
const std::filesystem::path filepath { "somedir/myfile.ext" };
const std::vector<std::uint8_t> data { 0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A' };

EXPECT_FALSE(std::filesystem::exists(filepath));
EXPECT_FALSE(getImpl(filepath).store(data));
EXPECT_FALSE(std::filesystem::exists(filepath));
}

TEST_F_S(Load, NoFileAvailable) {
EXPECT_EQ(getImpl().load(), std::vector<std::uint8_t> {});
}

TEST_F_S(Load, FileRead) {
const std::filesystem::path filepath { "myfile.ext" };
const std::vector<std::uint8_t> data { 0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A' };

{
std::ofstream file { filepath, std::ios_base::binary };
std::ranges::copy(data, std::ostreambuf_iterator<char> { file });
}

EXPECT_EQ(getImpl(filepath).load(), data);
}

TEST_F_S(Clear, NoFileAvailable) {
EXPECT_TRUE(getImpl().clear());
}

TEST_F_S(Clear, FileRemoved) {
const std::filesystem::path filepath { "myfile.ext" };
{
std::ofstream file { filepath };
file << "some data";
}

EXPECT_TRUE(std::filesystem::exists(filepath));
EXPECT_TRUE(getImpl(filepath).clear());
EXPECT_FALSE(std::filesystem::exists(filepath));
}

0 comments on commit 704295f

Please sign in to comment.