mirror of
https://github.com/GothenburgBitFactory/timewarrior.git
synced 2025-06-26 10:54:28 +02:00
Introduce AtomicFiles
Introduce AtomicFile and a test of this module to the code. AtomicFile is like File, except all writes go to temporary files until the class method finalize_all () is called and the temporary files are copied over the real files. If any writes fail, like when there is no more space on the filesystem, none of the files in the database will be modified. Since we need version 1.00 of libfiu, I have only added it to the debian testing container, which includes libfiu-1.00 in the default repository. Related to #155
This commit is contained in:
parent
6db1d2b859
commit
8e99c07d85
9 changed files with 1039 additions and 6 deletions
|
@ -8,6 +8,8 @@ include (CXXSniffer)
|
|||
|
||||
set (PROJECT_VERSION "1.2.0")
|
||||
|
||||
string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE)
|
||||
|
||||
message ("-- Looking for SHA1 references")
|
||||
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
|
||||
set (HAVE_COMMIT true)
|
||||
|
|
441
src/AtomicFile.cpp
Normal file
441
src/AtomicFile.cpp
Normal file
|
@ -0,0 +1,441 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2020, Shaun Ruffell, Thomas Lauf.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// https://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stdio.h>
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <format.h>
|
||||
#include <AtomicFile.h>
|
||||
#include <FS.h>
|
||||
|
||||
struct AtomicFile::impl
|
||||
{
|
||||
using value_type = std::shared_ptr <AtomicFile::impl>;
|
||||
// Since it should be relatively small, keep the atomic files in a vector
|
||||
using atomic_files_t = std::vector <value_type>;
|
||||
using iterator = atomic_files_t::iterator;
|
||||
|
||||
File temp_file;
|
||||
File real_file;
|
||||
|
||||
// After the file is modified in any way, all operations should deal only with
|
||||
// the temp file until finalization.
|
||||
bool is_temp_active {false};
|
||||
|
||||
impl (const Path& path);
|
||||
~impl ();
|
||||
|
||||
std::string name () const;
|
||||
const std::string& path () const;
|
||||
bool exists () const;
|
||||
|
||||
bool open ();
|
||||
void close ();
|
||||
void truncate ();
|
||||
void read (std::string& content);
|
||||
void read (std::vector <std::string>& lines);
|
||||
void append (const std::string& content);
|
||||
void write_raw (const std::string& content);
|
||||
|
||||
void finalize ();
|
||||
|
||||
static atomic_files_t::iterator find (const std::string& path) = delete;
|
||||
static atomic_files_t::iterator find (const Path& path);
|
||||
|
||||
// Static members
|
||||
|
||||
// If there is a problem writing to any of the temporary files, we do not want
|
||||
// any of them to be copied over the "real" file.
|
||||
static bool allow_atomics;
|
||||
static atomic_files_t atomic_files;
|
||||
};
|
||||
|
||||
using atomic_files_t = AtomicFile::impl::atomic_files_t;
|
||||
using atomics_iterator = atomic_files_t::iterator;
|
||||
|
||||
atomic_files_t AtomicFile::impl::atomic_files {};
|
||||
bool AtomicFile::impl::allow_atomics {true};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
AtomicFile::impl::impl (const Path& path)
|
||||
{
|
||||
static pid_t s_pid = ::getpid ();
|
||||
static int s_count = 0;
|
||||
std::stringstream str;
|
||||
|
||||
str << path._data << '.' << s_pid << '-' << ++s_count << ".tmp";
|
||||
temp_file = File (str.str());
|
||||
real_file = File (path);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
AtomicFile::impl::~impl ()
|
||||
{
|
||||
// Make sure we remove any temporary files if AtomicFile::finalize_all was
|
||||
// never called. Typically, this will happen when there are exceptions.
|
||||
try
|
||||
{
|
||||
std::remove (temp_file._data.c_str ());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::string AtomicFile::impl::name () const
|
||||
{
|
||||
return real_file.name ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string& AtomicFile::impl::path () const
|
||||
{
|
||||
return real_file._data;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool AtomicFile::impl::exists () const
|
||||
{
|
||||
return real_file.exists ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool AtomicFile::impl::open ()
|
||||
{
|
||||
assert (!temp_file._data.empty () && !real_file._data.empty ());
|
||||
return real_file.open ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::close ()
|
||||
{
|
||||
try
|
||||
{
|
||||
temp_file.close ();
|
||||
real_file.close ();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
allow_atomics = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::truncate ()
|
||||
{
|
||||
try
|
||||
{
|
||||
temp_file.truncate ();
|
||||
is_temp_active = true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
allow_atomics = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::read (std::string& content)
|
||||
{
|
||||
if (is_temp_active)
|
||||
{
|
||||
// Close the file before reading it in order to flush any buffers.
|
||||
temp_file.close ();
|
||||
}
|
||||
return (is_temp_active) ? temp_file.read (content) :
|
||||
real_file.read (content);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::read (std::vector <std::string>& lines)
|
||||
{
|
||||
if (is_temp_active)
|
||||
{
|
||||
// Close the file before reading it in order to flush any buffers.
|
||||
temp_file.close ();
|
||||
}
|
||||
return (is_temp_active) ? temp_file.read (lines) :
|
||||
real_file.read (lines);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::append (const std::string& content)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!is_temp_active)
|
||||
{
|
||||
is_temp_active = true;
|
||||
|
||||
if (real_file.exists () && ! File::copy (real_file, temp_file))
|
||||
{
|
||||
throw format ("Failed to copy '{1}' to '{2}'",
|
||||
real_file.name (), temp_file.name ());
|
||||
}
|
||||
}
|
||||
return temp_file.append (content);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
allow_atomics = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::write_raw (const std::string& content)
|
||||
{
|
||||
try
|
||||
{
|
||||
temp_file.write_raw (content);
|
||||
is_temp_active = true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
allow_atomics = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::impl::finalize ()
|
||||
{
|
||||
if (is_temp_active && impl::allow_atomics)
|
||||
{
|
||||
if (std::rename (temp_file._data.c_str (), real_file._data.c_str ()))
|
||||
{
|
||||
throw format("Failed copying '{1}' to '{2}'. Database corruption possible.",
|
||||
temp_file._data, real_file._data);
|
||||
}
|
||||
is_temp_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
atomics_iterator AtomicFile::impl::find (const Path& path)
|
||||
{
|
||||
auto end = impl::atomic_files.end ();
|
||||
auto cmp = [&path](const atomic_files_t::value_type& p)
|
||||
{
|
||||
return p->real_file == path;
|
||||
};
|
||||
auto it = std::find_if(impl::atomic_files.begin (), end, cmp);
|
||||
return (it == end) ? end : it;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
AtomicFile::AtomicFile (const Path& path)
|
||||
{
|
||||
auto it = impl::find (path);
|
||||
|
||||
if (it == impl::atomic_files.end ())
|
||||
{
|
||||
pimpl = std::make_shared <impl> (path._data);
|
||||
impl::atomic_files.push_back (pimpl);
|
||||
}
|
||||
else
|
||||
{
|
||||
pimpl = *it;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
AtomicFile::AtomicFile (std::string path) : AtomicFile (Path (path))
|
||||
{
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
AtomicFile::~AtomicFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
pimpl->close ();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
AtomicFile::AtomicFile (AtomicFile&&) = default;
|
||||
AtomicFile& AtomicFile::operator= (AtomicFile&&) = default;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string& AtomicFile::path () const
|
||||
{
|
||||
return pimpl->path ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::string AtomicFile::name () const
|
||||
{
|
||||
return pimpl->name ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool AtomicFile::exists () const
|
||||
{
|
||||
return pimpl->exists ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool AtomicFile::open ()
|
||||
{
|
||||
return pimpl->open ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::close ()
|
||||
{
|
||||
pimpl->close ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::truncate ()
|
||||
{
|
||||
pimpl->truncate ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::read (std::string& content)
|
||||
{
|
||||
pimpl->read (content);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::read (std::vector <std::string>& lines)
|
||||
{
|
||||
pimpl->read (lines);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::append (const std::string& content)
|
||||
{
|
||||
pimpl->append (content);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::write_raw (const std::string& content)
|
||||
{
|
||||
pimpl->write_raw (content);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::append (const std::string& path, const std::string& data)
|
||||
{
|
||||
return AtomicFile(path).append (data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::write (const std::string& path, const std::string& data)
|
||||
{
|
||||
AtomicFile::write (Path (path), data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::write (const Path& path, const std::string& data)
|
||||
{
|
||||
AtomicFile file (path);
|
||||
file.truncate ();
|
||||
file.write_raw (data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::write (const Path& path, const std::vector <std::string>& lines)
|
||||
{
|
||||
AtomicFile file (path);
|
||||
file.truncate ();
|
||||
for (const auto& line : lines)
|
||||
{
|
||||
file.append (line + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::read (const Path& path, std::string& content)
|
||||
{
|
||||
AtomicFile file (path);
|
||||
file.read (content);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void AtomicFile::read (const std::string& path, std::string& content)
|
||||
{
|
||||
AtomicFile::read (Path (path), content);
|
||||
}
|
||||
|
||||
void AtomicFile::read (const Path& path, std::vector <std::string>& lines)
|
||||
{
|
||||
AtomicFile (path).read (lines);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// finalize_all - Close / Flush all temporary files and rename to final.
|
||||
void AtomicFile::finalize_all ()
|
||||
{
|
||||
if (!impl::allow_atomics)
|
||||
{
|
||||
throw std::string {"Unable to update database."};
|
||||
}
|
||||
|
||||
// Step 1: Close / Flush all the atomic files that may still be open. If any
|
||||
// of the files fail this step (close () will throw) then we do not want to
|
||||
// move on to step 2
|
||||
for (auto& file : impl::atomic_files)
|
||||
{
|
||||
file->close ();
|
||||
}
|
||||
|
||||
atomic_files_t new_atomic_files;
|
||||
|
||||
// Step 2: Rename the temp files to the *real* files
|
||||
for (auto& file : impl::atomic_files)
|
||||
{
|
||||
file->finalize ();
|
||||
|
||||
// Delete entry if we are holding the last reference
|
||||
if (file.use_count () > 1)
|
||||
{
|
||||
new_atomic_files.push_back(file);
|
||||
}
|
||||
}
|
||||
|
||||
new_atomic_files.swap(impl::atomic_files);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// reset - Removes all current atomic files from finalization
|
||||
void AtomicFile::reset ()
|
||||
{
|
||||
impl::atomic_files.clear ();
|
||||
impl::allow_atomics = true;
|
||||
}
|
78
src/AtomicFile.h
Normal file
78
src/AtomicFile.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2020, Shaun Ruffell, Thomas Lauf.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// https://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef INCLUDED_ATOMICFILE
|
||||
#define INCLUDED_ATOMICFILE
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class Path;
|
||||
|
||||
class AtomicFile
|
||||
{
|
||||
public:
|
||||
AtomicFile (const Path& path);
|
||||
AtomicFile (std::string path);
|
||||
AtomicFile (const AtomicFile&) = delete;
|
||||
AtomicFile (AtomicFile&&);
|
||||
AtomicFile& operator= (const AtomicFile&) = delete;
|
||||
AtomicFile& operator= (AtomicFile&&);
|
||||
~AtomicFile ();
|
||||
|
||||
std::string name () const;
|
||||
const std::string& path () const;
|
||||
bool exists () const;
|
||||
|
||||
bool open ();
|
||||
void close ();
|
||||
void truncate ();
|
||||
void read (std::string& content);
|
||||
void read (std::vector <std::string>& lines);
|
||||
void append (const std::string& content);
|
||||
void write_raw (const std::string& content);
|
||||
|
||||
static void append (const std::string& path, const std::string& data);
|
||||
static void append (const Path& path, const std::string& data);
|
||||
|
||||
static void write (const std::string& path, const std::string& data);
|
||||
static void write (const Path& path, const std::string& data);
|
||||
static void write (const Path& path, const std::vector <std::string>& lines);
|
||||
|
||||
static void read (const std::string& path, std::string& content);
|
||||
static void read (const Path& path, std::string& content);
|
||||
static void read (const Path& path, std::vector <std::string>& lines);
|
||||
|
||||
static void finalize_all ();
|
||||
static void reset ();
|
||||
|
||||
public:
|
||||
struct impl;
|
||||
|
||||
private:
|
||||
std::shared_ptr <impl> pimpl;
|
||||
};
|
||||
|
||||
#endif // INCLUDED_ATOMICFILE
|
|
@ -5,7 +5,8 @@ include_directories (${CMAKE_SOURCE_DIR}
|
|||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
||||
${TIMEW_INCLUDE_DIRS})
|
||||
|
||||
set (timew_SRCS CLI.cpp CLI.h
|
||||
set (timew_SRCS AtomicFile.cpp AtomicFile.h
|
||||
CLI.cpp CLI.h
|
||||
Chart.cpp Chart.h
|
||||
ChartConfig.h
|
||||
Database.cpp Database.h
|
||||
|
|
1
test/.gitignore
vendored
1
test/.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
all.log
|
||||
atomic
|
||||
data.t
|
||||
exclusion.t
|
||||
helper.t
|
||||
|
|
|
@ -8,6 +8,24 @@ if(POLICY CMP0037 AND ${CMAKE_VERSION} VERSION_LESS "3.11.0")
|
|||
cmake_policy(SET CMP0037 OLD)
|
||||
endif()
|
||||
|
||||
# If this is a debug build, check if we have libfiu installed and available on
|
||||
# the system. If so, we will be able to use it to add additional tests of the
|
||||
# failure conditions
|
||||
|
||||
if (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
|
||||
find_library(FIU_ENABLE fiu)
|
||||
if (FIU_ENABLE)
|
||||
message (STATUS "libfiu found")
|
||||
add_definitions (-DFIU_ENABLE)
|
||||
set (test_LIBS fiu ${TIMEW_LIBRARIES})
|
||||
else (FIU_ENABLE)
|
||||
message (STATUS "NOTE: install libfiu to run additional tests")
|
||||
set (test_LIBS ${TIMEW_LIBRARIES})
|
||||
endif (FIU_ENABLE)
|
||||
else (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
|
||||
set (test_LIBS ${TIMEW_LIBRARIES})
|
||||
endif (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
|
||||
|
||||
include_directories (${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
||||
|
@ -16,7 +34,7 @@ include_directories (${CMAKE_SOURCE_DIR}
|
|||
include_directories (${CMAKE_INSTALL_PREFIX}/include)
|
||||
link_directories(${CMAKE_INSTALL_PREFIX}/lib)
|
||||
|
||||
set (test_SRCS data.t exclusion.t helper.t interval.t range.t rules.t util.t TagInfoDatabase.t)
|
||||
set (test_SRCS atomic data.t exclusion.t helper.t interval.t range.t rules.t util.t TagInfoDatabase.t)
|
||||
|
||||
add_custom_target (test ./run_all --verbose
|
||||
DEPENDS ${test_SRCS}
|
||||
|
@ -24,7 +42,7 @@ add_custom_target (test ./run_all --verbose
|
|||
|
||||
foreach (src_FILE ${test_SRCS})
|
||||
add_executable (${src_FILE} "${src_FILE}.cpp" test.cpp)
|
||||
target_link_libraries (${src_FILE} timew libshared ${TIMEW_LIBRARIES})
|
||||
target_link_libraries (${src_FILE} timew libshared ${test_LIBS})
|
||||
endforeach (src_FILE)
|
||||
|
||||
configure_file(run_all run_all COPYONLY)
|
||||
|
|
483
test/atomic.cpp
Normal file
483
test/atomic.cpp
Normal file
|
@ -0,0 +1,483 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2020, Shaun Ruffell, Thomas Lauf.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// https://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <sstream>
|
||||
#include <memory>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <cmake.h>
|
||||
#include <timew.h>
|
||||
#include <test.h>
|
||||
#include <AtomicFile.h>
|
||||
#include <FS.h>
|
||||
|
||||
#ifdef FIU_ENABLE
|
||||
|
||||
#include <fiu.h>
|
||||
#include <fiu-control.h>
|
||||
|
||||
#else
|
||||
|
||||
#define fiu_init(flags) 0
|
||||
#define fiu_fail(name) 0
|
||||
#define fiu_failinfo() NULL
|
||||
#define fiu_do_on(name, action)
|
||||
#define fiu_exit_on(name)
|
||||
#define fiu_return_on(name, retval)
|
||||
|
||||
#endif /* FIU_ENABLE */
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class TempDir
|
||||
{
|
||||
public:
|
||||
TempDir ();
|
||||
~TempDir ();
|
||||
|
||||
bool is_empty () const;
|
||||
void clear ();
|
||||
std::vector <std::string> file_names () const;
|
||||
|
||||
private:
|
||||
std::string tmpName {};
|
||||
std::string oldDir {};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
TempDir::TempDir ()
|
||||
{
|
||||
oldDir = Directory::cwd ();
|
||||
|
||||
char template_name[] = "atomic_XXXXXX";
|
||||
const char *cwd = ::mkdtemp (template_name);
|
||||
if (cwd == nullptr)
|
||||
{
|
||||
throw std::string ("Failed to create temp directory");
|
||||
}
|
||||
|
||||
tmpName = cwd;
|
||||
|
||||
if (::chdir (tmpName.c_str ()))
|
||||
{
|
||||
throw std::string ("Failed to change to temporary directory");
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
TempDir::~TempDir ()
|
||||
{
|
||||
try
|
||||
{
|
||||
clear ();
|
||||
::chdir (oldDir.c_str ());
|
||||
|
||||
if (!Directory(tmpName).remove ())
|
||||
{
|
||||
std::cerr << "Failed to remove temp dir " << tmpName << '\n';
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Unhandled exception in " << __func__ << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool TempDir::is_empty () const
|
||||
{
|
||||
return file_names().empty ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::vector <std::string> TempDir::file_names () const
|
||||
{
|
||||
return Path (tmpName).glob ("*");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void TempDir::clear ()
|
||||
{
|
||||
for (const auto& file : file_names ())
|
||||
{
|
||||
File::remove (file);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef FIU_ENABLE
|
||||
|
||||
// This class is a helper to make sure we turn off libfiu after the test so that
|
||||
// we can properly write to stdout / stderr.
|
||||
class FIU
|
||||
{
|
||||
public:
|
||||
FIU()
|
||||
{
|
||||
enable();
|
||||
}
|
||||
|
||||
FIU(const FIU&) = delete;
|
||||
FIU& operator= (const FIU&) = delete;
|
||||
|
||||
~FIU()
|
||||
{
|
||||
disable ();
|
||||
std::cout << cbuffer.str();
|
||||
std::stringstream().swap(cbuffer);
|
||||
}
|
||||
|
||||
void enable ()
|
||||
{
|
||||
for (auto test_point : test_points)
|
||||
{
|
||||
fiu_enable_external(test_point, 1, NULL, 0, fiu_cb);
|
||||
}
|
||||
}
|
||||
|
||||
void disable ()
|
||||
{
|
||||
for (auto test_point : test_points)
|
||||
{
|
||||
fiu_disable(test_point);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static const std::vector <const char *> test_points;
|
||||
static std::stringstream cbuffer;
|
||||
|
||||
static int external_cb_was_called;
|
||||
|
||||
static int fiu_cb(const char *name, int *failnum,
|
||||
void **failinfo, unsigned int *flags)
|
||||
{
|
||||
(void)name;
|
||||
(void)flags;
|
||||
external_cb_was_called++;
|
||||
|
||||
// For debugging the tests themselves...
|
||||
// cbuffer << "fiu_cb called for " << name << '\n';
|
||||
|
||||
*failinfo = (void *) EIO;
|
||||
|
||||
return *failnum;
|
||||
}
|
||||
};
|
||||
|
||||
const std::vector <const char *> FIU::test_points {
|
||||
"posix/stdio/gp/fputs",
|
||||
"posix/io/rw/write",
|
||||
};
|
||||
|
||||
std::stringstream FIU::cbuffer;
|
||||
int FIU::external_cb_was_called = 0;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Since AtomicFile is primarily for keeping the database consistent in the
|
||||
// presence of filesystem errors, these tests use libfiu to ensure that
|
||||
// AtomicFile behaves as intended when the underlying system calls fail.
|
||||
int fiu_test (UnitTest& t)
|
||||
{
|
||||
TempDir tempDir;
|
||||
fiu_init (0);
|
||||
|
||||
try { FIU fiu; AtomicFile ("test.txt").write_raw("This is test"); t.fail ("AtomicFile::write_raw throws on error"); }
|
||||
catch (...) { t.pass ("AtomicFile::write_raw throws on error"); }
|
||||
|
||||
try { FIU fiu; AtomicFile::finalize_all (); t.fail ("AtomicFile::finalize_all() throws on error"); }
|
||||
catch (...) { t.pass ("AtomicFile::finalize_all() throws on error"); }
|
||||
|
||||
try { FIU fiu; AtomicFile::reset (); AtomicFile::finalize_all (); t.pass ("AtomicFile::reset clears failure state"); }
|
||||
catch (...) { t.fail ("AtomicFile::reset clears failure state"); }
|
||||
|
||||
File::write ("test.txt", "line1\n");
|
||||
{
|
||||
AtomicFile file ("test.txt");
|
||||
try { FIU fiu; file.append ("append1\n"); t.fail ("AtomicFile::append throws on error"); }
|
||||
catch (...) { t.pass ("AtomicFile::append throws on error"); }
|
||||
|
||||
std::string contents {"should-not-see-this"};
|
||||
file.read (contents);
|
||||
t.is (contents, "", "AtomicFile::append did not partially fill the file.");
|
||||
}
|
||||
|
||||
try { FIU fiu; AtomicFile::finalize_all (); t.fail ("AtomicFile::append failures prevent finalization"); }
|
||||
catch (...) { t.pass ("AtomicFile::append failures prevent finalization"); }
|
||||
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
int fiu_test (UnitTest& t)
|
||||
{
|
||||
t.skip ("AtomicFile::write_raw throws on error");
|
||||
t.skip ("AtomicFile::finalize_all() throws on error");
|
||||
t.skip ("AtomicFile::reset clears failure state");
|
||||
t.skip ("AtomicFile::append throws on error");
|
||||
t.skip ("AtomicFile::append did not partially fill the file.");
|
||||
t.skip ("AtomicFile::append failures prevent finalization");
|
||||
return 0;
|
||||
}
|
||||
#endif // FIU_ENABLE
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
int test (UnitTest& t)
|
||||
{
|
||||
std::string test_name;
|
||||
|
||||
// This will create and change to a temporary directory that will be cleaned
|
||||
// up when the destructor is run
|
||||
TempDir tempDir;
|
||||
|
||||
std::string goldenText = "1\n";
|
||||
std::string contents;
|
||||
std::string expected;
|
||||
|
||||
Path firstFilename ("test-1.txt");
|
||||
Path secondFilename ("test-2.txt");
|
||||
|
||||
{
|
||||
AtomicFile file (firstFilename);
|
||||
file.write_raw (goldenText);
|
||||
file.close ();
|
||||
}
|
||||
|
||||
t.is (firstFilename.exists (), false, "Shall not exists before finalize");
|
||||
AtomicFile::finalize_all ();
|
||||
t.is (firstFilename.exists (), true, "Shall exists after finalize");
|
||||
File::read(firstFilename, contents);
|
||||
t.is (contents == goldenText, true, "Shall have the correct data");
|
||||
|
||||
tempDir.clear ();
|
||||
|
||||
{
|
||||
AtomicFile first (firstFilename);
|
||||
AtomicFile second (secondFilename);
|
||||
|
||||
first.write_raw ("first\n");
|
||||
second.write_raw ("second\n");
|
||||
first.close ();
|
||||
second.close ();
|
||||
}
|
||||
|
||||
t.is (firstFilename.exists () || secondFilename.exists (), false, "Neither shall exist before finalize");
|
||||
|
||||
AtomicFile::finalize_all ();
|
||||
|
||||
t.is (firstFilename.exists () && secondFilename.exists (), true, "Both shall exists after finalize");
|
||||
|
||||
File::read(firstFilename, contents);
|
||||
test_name = "First file shall contain the correct data";
|
||||
expected = "first\n";
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
File::read(secondFilename, contents);
|
||||
test_name = "Second file shall contain the correct data";
|
||||
if (contents == std::string("second\n"))
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected 'second\n' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
// Make sure appending works
|
||||
|
||||
test_name = "Appending does not update original before finalize";
|
||||
expected = "first\n";
|
||||
|
||||
{
|
||||
AtomicFile first (firstFilename);
|
||||
|
||||
first.append("append 1\n");
|
||||
first.append("append 2\n");
|
||||
first.close ();
|
||||
}
|
||||
|
||||
File::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
test_name = "Finalizing updates the appended data";
|
||||
expected = "first\nappend 1\nappend 2\n";
|
||||
AtomicFile::finalize_all ();
|
||||
File::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
test_name = "Read from Atomicfile";
|
||||
// We do not want to update the expected
|
||||
{
|
||||
AtomicFile::read (firstFilename, contents);
|
||||
}
|
||||
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
// If we read from an atomic file before finalizing, we should get the data
|
||||
// that was written to the temporary file and not the 'real' file.
|
||||
|
||||
test_name = "Read from Atomicfile should read unfinalized data";
|
||||
expected += "expected\n";
|
||||
AtomicFile::write (firstFilename, expected);
|
||||
AtomicFile::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
AtomicFile::finalize_all ();
|
||||
{
|
||||
AtomicFile first (firstFilename);
|
||||
AtomicFile second (firstFilename);
|
||||
|
||||
first.write_raw("first\n");
|
||||
second.append ("second\n");
|
||||
|
||||
AtomicFile::finalize_all ();
|
||||
|
||||
first.append ("third\n");
|
||||
second.append ("fourth\n");
|
||||
first.append ("fifth\n");
|
||||
|
||||
}
|
||||
|
||||
test_name = "Two AtomicFiles should access same temp file (part 1)";
|
||||
|
||||
// The atomic files, which were closed above should return all the strings
|
||||
// written since it was not yet finalized.
|
||||
|
||||
expected = "first\nsecond\nthird\nfourth\nfifth\n";
|
||||
AtomicFile::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
// But since the file was not yet finalized, the "real" file should only
|
||||
// contain the data present since before the finalize_all () call.
|
||||
|
||||
test_name = "Two AtomicFiles should access same temp file (part 2)";
|
||||
expected = "first\nsecond\n";
|
||||
File::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
// After we finalize the data, the AtomicFile and the File should now both
|
||||
// return the same information
|
||||
|
||||
test_name = "Two AtomicFiles should access same temp file (part 3)";
|
||||
AtomicFile::finalize_all ();
|
||||
File::read (firstFilename, expected);
|
||||
AtomicFile::read (firstFilename, contents);
|
||||
if (contents == expected)
|
||||
{
|
||||
t.pass (test_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
t.fail (test_name);
|
||||
t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
|
||||
}
|
||||
|
||||
{
|
||||
AtomicFile test ("test");
|
||||
}
|
||||
|
||||
AtomicFile::reset ();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main (int, char**)
|
||||
{
|
||||
UnitTest t (20);
|
||||
try
|
||||
{
|
||||
int ret = test (t);
|
||||
int fiu_ret = fiu_test (t);
|
||||
return (ret == 0) ? fiu_ret : ret;
|
||||
}
|
||||
catch (const std::string& error)
|
||||
{
|
||||
std::cerr << "Test threw exception: " << error << '\n';
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Uncaught exception.\n";
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
7
test/atomic.t
Executable file
7
test/atomic.t
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
BASEDIR=$(dirname "$0")
|
||||
if ldd ${BASEDIR}/atomic | grep -q 'libfiu' ; then
|
||||
exec fiu-run -x ${BASEDIR}/atomic
|
||||
else
|
||||
exec ${BASEDIR}/atomic
|
||||
fi
|
|
@ -2,16 +2,18 @@ FROM debian:testing
|
|||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
cmake \
|
||||
fiu-utils=1.00-* \
|
||||
g++ \
|
||||
git \
|
||||
libfiu-dev=1.00-* \
|
||||
locales \
|
||||
man \
|
||||
python3 \
|
||||
python3-dateutil \
|
||||
tzdata
|
||||
tzdata \
|
||||
&& rm -fr /var/lib/apt/lists/*
|
||||
|
||||
# Setup environment
|
||||
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 10
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue