timewarrior/test/atomic.cpp
Shaun Ruffell 8e99c07d85 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
2020-02-23 19:03:40 +01:00

483 lines
12 KiB
C++

////////////////////////////////////////////////////////////////////////////////
//
// 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";
}
}
////////////////////////////////////////////////////////////////////////////////