diff --git a/CMakeLists.txt b/CMakeLists.txt index 07e4707f..0d73ce94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/AtomicFile.cpp b/src/AtomicFile.cpp new file mode 100644 index 00000000..8e12ecef --- /dev/null +++ b/src/AtomicFile.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include + +struct AtomicFile::impl +{ + using value_type = std::shared_ptr ; + // Since it should be relatively small, keep the atomic files in a vector + using atomic_files_t = std::vector ; + 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 & 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 & 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 (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 & 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 & 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 & 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; +} diff --git a/src/AtomicFile.h b/src/AtomicFile.h new file mode 100644 index 00000000..8b7c4503 --- /dev/null +++ b/src/AtomicFile.h @@ -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 +#include + +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 & 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 & 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 & lines); + + static void finalize_all (); + static void reset (); + +public: + struct impl; + +private: + std::shared_ptr pimpl; +}; + +#endif // INCLUDED_ATOMICFILE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 98ba506e..268b7e5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/test/.gitignore b/test/.gitignore index c8b9d655..2840bade 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,4 +1,5 @@ all.log +atomic data.t exclusion.t helper.t diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0bcf23d5..c98c6b26 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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) diff --git a/test/atomic.cpp b/test/atomic.cpp new file mode 100644 index 00000000..f0afd2af --- /dev/null +++ b/test/atomic.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef FIU_ENABLE + +#include +#include + +#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 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 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 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 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"; + } +} + +//////////////////////////////////////////////////////////////////////////////// + diff --git a/test/atomic.t b/test/atomic.t new file mode 100755 index 00000000..788871c2 --- /dev/null +++ b/test/atomic.t @@ -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 diff --git a/test/docker/debiantesting b/test/docker/debiantesting index 88e5fa38..9e306ac4 100644 --- a/test/docker/debiantesting +++ b/test/docker/debiantesting @@ -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