diff --git a/src/AtomicFile.cpp b/src/AtomicFile.cpp index ec5549d2..1e2234c4 100644 --- a/src/AtomicFile.cpp +++ b/src/AtomicFile.cpp @@ -129,7 +129,7 @@ const std::string& AtomicFile::impl::path () const //////////////////////////////////////////////////////////////////////////////// bool AtomicFile::impl::exists () const { - return real_file.exists (); + return (is_temp_active) ? temp_file.exists () : real_file.exists (); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Database.cpp b/src/Database.cpp index 8d8defcc..36cce1ca 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -35,6 +35,58 @@ #include #include +//////////////////////////////////////////////////////////////////////////////// +// This returns a value which is equivalent to std::lower_bound on the +// datafiles. It will either be the Datafile that should contain the range, an +// iterator pointing to where a new Datafile should be created, or the end of +// the datafiles. +// +// Unlike lower_bound, it always searches from the end of files since the most +// common use case for timewarrior are the most recent entries. +using datafiles_t = Database::datafiles_t; +static datafiles_t::iterator findDatafile (const Range& range, datafiles_t* files) +{ + assert (files != nullptr); + for (auto r_it = files->rbegin (); r_it != files->rend (); ++r_it) + { + const Range& df_range = r_it->range (); + if (range.startsWithin (df_range)) + { + // found the datafile that contains the range + return r_it.base () - 1; + } + else if (df_range.end < range.start) + { + // found the place where new datafile should be inserted to contain + // this range + return std::prev(r_it).base () - 1; + } + } + + // New datafile should be created at end + return files->begin (); +} + +//////////////////////////////////////////////////////////////////////////////// +static Datafile& getDatafile (const std::string& location, const Interval& interval, datafiles_t* files) +{ + assert (files != nullptr); + auto it = findDatafile (interval, files); + if (it == files->end () || ! interval.startsWithin (it->range ())) + { + std::stringstream sstream; + sstream << location << '/' + << std::setw (4) << std::setfill ('0') << interval.start.year () + << '-' + << std::setw (2) << std::setfill ('0') << interval.start.month () + << ".data"; + + it = files->emplace (it, sstream.str ()); + } + + return *it; +} + //////////////////////////////////////////////////////////////////////////////// Database::iterator::iterator (files_iterator fbegin, files_iterator fend) : files_it(fbegin), @@ -200,54 +252,39 @@ const std::string* Database::reverse_iterator::operator->() const //////////////////////////////////////////////////////////////////////////////// Database::iterator Database::begin () { - if (_files.empty ()) - { - initializeDatafiles (); - } - return iterator (_files.rbegin (), _files.rend ()); } //////////////////////////////////////////////////////////////////////////////// Database::iterator Database::end () { - if (_files.empty ()) - { - initializeDatafiles (); - } - return iterator (_files.rend (), _files.rend ()); } - //////////////////////////////////////////////////////////////////////////////// Database::reverse_iterator Database::rbegin () { - if (_files.empty ()) - { - initializeDatafiles (); - } - return reverse_iterator(_files.begin (), _files.end ()); } //////////////////////////////////////////////////////////////////////////////// Database::reverse_iterator Database::rend () { - if (_files.empty ()) - { - initializeDatafiles (); - } - return reverse_iterator (_files.end (), _files.end ()); } //////////////////////////////////////////////////////////////////////////////// -void Database::initialize (const std::string& location, Journal& journal) +Database::Database (const std::string& location, int journal_size) : _location (location), _journal (location + "/undo.data", journal_size) { - _location = location; - _journal = &journal; - initializeTagDatabase (); + try + { + initializeDatafiles (); + initializeTagDatabase (); + } + catch (const std::string& error) + { + throw std::string ("Failed to create database at '" ) + _location + "' error: " + error; + } } //////////////////////////////////////////////////////////////////////////////// @@ -264,6 +301,12 @@ void Database::commit () } } +//////////////////////////////////////////////////////////////////////////////// +Journal& Database::journal () +{ + return _journal; +} + //////////////////////////////////////////////////////////////////////////////// std::vector Database::files () const { @@ -302,8 +345,7 @@ void Database::addInterval (const Interval& interval, bool verbose) { assert ( (interval.end == 0) || (interval.start <= interval.end)); - auto tags = interval.tags (); - for (auto& tag : tags) + for (const auto& tag : interval.tags ()) { if (_tagInfoDatabase.incrementTag (tag) == -1 && verbose) { @@ -311,28 +353,28 @@ void Database::addInterval (const Interval& interval, bool verbose) } } - // Get the index into _files for the appropriate Datafile, which may be - // created on demand. - auto df = getDatafile (interval.start.year (), interval.start.month ()); - _files[df].addInterval (interval); - _journal->recordIntervalAction ("", interval.json ()); + if (getDatafile (_location, interval, &_files).addInterval (interval)) + { + _journal.recordIntervalAction ("", interval.json ()); + } } +//////////////////////////////////////////////////////////////////////////////// void Database::deleteInterval (const Interval& interval) { - auto tags = interval.tags (); - - for (auto& tag : tags) + for (const auto& tag : interval.tags ()) { _tagInfoDatabase.decrementTag (tag); } - // Get the index into _files for the appropriate Datafile, which may be - // created on demand. - auto df = getDatafile (interval.start.year (), interval.start.month ()); + getDatafile (_location, interval, &_files).deleteInterval (interval); + auto it = findDatafile (interval, &_files); + if (it == _files.end () || ! interval.startsWithin (it->range ())) + { + throw std::string ("Database failed to find file for deleted Interval"); + } - _files[df].deleteInterval (interval); - _journal->recordIntervalAction (interval.json (), ""); + _journal.recordIntervalAction (interval.json (), ""); } //////////////////////////////////////////////////////////////////////////////// @@ -366,37 +408,6 @@ std::string Database::dump () const return out.str (); } -//////////////////////////////////////////////////////////////////////////////// -unsigned int Database::getDatafile (int year, int month) -{ - std::stringstream file; - file << _location - << '/' - << std::setw (4) << std::setfill ('0') << year - << '-' - << std::setw (2) << std::setfill ('0') << month - << ".data"; - auto name = file.str (); - auto basename = Path (name).name (); - - // If the datafile is already initialized, return. - for (unsigned int i = 0; i < _files.size (); ++i) - { - if (_files[i].name () == basename) - { - return i; - } - } - - // Create the Datafile. - Datafile df; - df.initialize (name); - - // Insert Datafile into _files. The position is not important. - _files.push_back (df); - return _files.size () - 1; -} - //////////////////////////////////////////////////////////////////////////////// // The input Daterange has a start and end, for example: // @@ -460,12 +471,13 @@ bool Database::empty () void Database::initializeTagDatabase () { _tagInfoDatabase = TagInfoDatabase (); - Path tags_path (_location + "/tags.data"); + AtomicFile tags_file {_location + "/tags.data"}; std::string content; - const bool exists = tags_path.exists (); + const bool exists = tags_file.exists (); - if (exists && File::read (tags_path, content)) + if (exists) { + tags_file.read (content); try { std::unique_ptr json (dynamic_cast (json::parse (content))); @@ -505,11 +517,11 @@ void Database::initializeTagDatabase () // We always want the tag database file to exists. _tagInfoDatabase = TagInfoDatabase(); - AtomicFile::write (_location + "/tags.data", _tagInfoDatabase.toJson ()); + tags_file.write_raw (_tagInfoDatabase.toJson ()); auto it = Database::begin (); auto end = Database::end (); - + if (it == end) { return; @@ -519,7 +531,7 @@ void Database::initializeTagDatabase () { std::cout << "Tags database does not exist. "; } - + std::cout << "Recreating from interval data..." << std::endl; for (; it != end; ++it) @@ -533,25 +545,33 @@ void Database::initializeTagDatabase () } //////////////////////////////////////////////////////////////////////////////// -void Database::initializeDatafiles () +void Database::initializeDatafiles () const { + assert (_files.empty ()); + + Directory d (_location); + auto filenames = d.list (); + + const auto& not_valid_filename = [](const std::string& filename) + { + return (filename[filename.length () - 8] != '-' || + filename.find (".data") != filename.length () - 5); + }; + + auto begin = filenames.begin (); + auto end = std::remove_if (begin, filenames.end (), not_valid_filename); + // Because the data files have names YYYY-MM.data, sorting them by name also // sorts by the intervals within. - Directory d (_location); - auto files = d.list (); - std::sort (files.begin (), files.end ()); + std::sort (begin, end); + + // Reserve room for one extra datafile for the common case where we will add a + // new interval to a new datafile. + _files.reserve (std::distance (begin, end) + 1); - for (auto& file : files) + for (auto it = begin; it != end; ++it) { - // If it looks like a data file: *-??.data - if (file[file.length () - 8] == '-' && - file.find (".data") == file.length () - 5) - { - auto basename = Path (file).name (); - auto year = strtol (basename.substr (0, 4).c_str (), NULL, 10); - auto month = strtol (basename.substr (5, 2).c_str (), NULL, 10); - getDatafile (year, month); - } + _files.emplace_back (*it); } } diff --git a/src/Database.h b/src/Database.h index 174ce877..135bb946 100644 --- a/src/Database.h +++ b/src/Database.h @@ -39,14 +39,21 @@ class Database { public: + using datafiles_t = std::vector ; class iterator { + public: + using difference_type = ssize_t; + using value_type = std::string; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::forward_iterator_tag; + private: friend class Database; - typedef std::vector ::reverse_iterator files_iterator; + typedef datafiles_t::reverse_iterator files_iterator; typedef std::vector ::const_reverse_iterator lines_iterator; - typedef std::string value_type; files_iterator files_it; files_iterator files_end; @@ -58,8 +65,6 @@ class Database public: iterator& operator++ (); - iterator& operator++ (int); - iterator& operator-- (); bool operator== (const iterator & other) const; bool operator!= (const iterator & other) const; const value_type& operator* () const; @@ -68,11 +73,17 @@ class Database class reverse_iterator { + public: + using difference_type = ssize_t; + using value_type = std::string; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::forward_iterator_tag; + private: friend class Database; - typedef std::vector ::iterator files_iterator; + typedef datafiles_t::iterator files_iterator; typedef std::vector ::const_iterator lines_iterator; - typedef std::string value_type; files_iterator files_it; files_iterator files_end; @@ -84,8 +95,6 @@ class Database public: reverse_iterator& operator++ (); - reverse_iterator& operator++ (int); - reverse_iterator& operator-- (); bool operator== (const reverse_iterator & other) const; bool operator!= (const reverse_iterator & other) const; const value_type& operator* () const; @@ -93,9 +102,15 @@ class Database }; public: - Database () = default; - void initialize (const std::string&, Journal& journal); + Database () = delete; + Database (const std::string&, int); + Database (const Database&) = delete; + Database (Database&&) = default; + Database& operator= (const Database&) = delete; + Database& operator= (Database&&) = default; + void initialize (const std::string&, int); void commit (); + Journal& journal (); std::vector files () const; std::set tags () const; @@ -114,16 +129,15 @@ class Database reverse_iterator rend (); private: - unsigned int getDatafile (int, int); std::vector segmentRange (const Range&); - void initializeDatafiles (); + void initializeDatafiles () const; void initializeTagDatabase (); private: - std::string _location {"~/.timewarrior/data"}; - std::vector _files {}; + std::string _location; + Journal _journal; + mutable datafiles_t _files {}; TagInfoDatabase _tagInfoDatabase {}; - Journal* _journal {}; }; #endif diff --git a/src/Datafile.cpp b/src/Datafile.cpp index 2fd85454..fc43f1ab 100644 --- a/src/Datafile.cpp +++ b/src/Datafile.cpp @@ -36,10 +36,8 @@ #include //////////////////////////////////////////////////////////////////////////////// -void Datafile::initialize (const std::string& name) +Datafile::Datafile (const std::string& name) : _file (name) { - _file = Path (name); - // From the name, which is of the form YYYY-MM.data, extract the YYYY and MM. auto basename = _file.name (); auto year = strtol (basename.substr (0, 4).c_str (), NULL, 10); @@ -63,6 +61,12 @@ std::string Datafile::name () const return _file.name (); } +//////////////////////////////////////////////////////////////////////////////// +const Range& Datafile::range () const +{ + return _range; +} + //////////////////////////////////////////////////////////////////////////////// // Identifies the last incluѕion (^i) lines std::string Datafile::lastLine () @@ -89,8 +93,11 @@ const std::vector & Datafile::allLines () //////////////////////////////////////////////////////////////////////////////// // Accepted intervals; day1 <= interval.start < dayN -void Datafile::addInterval (const Interval& interval) +// Returns true if the interval was added to the datafile +bool Datafile::addInterval (const Interval& interval) { + bool lineAdded = false; + // Note: end date might be zero. assert (interval.startsWithin (_range)); @@ -111,15 +118,26 @@ void Datafile::addInterval (const Interval& interval) serialization, test.serialize ())); } - _lines.push_back (serialization); - debug (format ("{1}: Added {2}", _file.name (), _lines.back ())); - _dirty = true; + auto it = std::lower_bound (_lines.begin (), _lines.end (), serialization); + if ((it != _lines.end ()) && (*it == serialization)) + { + debug (format ("{1}: already contained {2}", _file.name (), serialization)); + } + else + { + _lines.insert (it, serialization); + debug (format ("{1}: Added {2}", _file.name (), serialization)); + _dirty = true; + lineAdded = true; + } } catch (const std::string& error) { debug (format ("Datafile::addInterval() failed.\n{1}", error)); throw std::string ("Internal error. Failed encode / decode check."); } + + return lineAdded; } //////////////////////////////////////////////////////////////////////////////// @@ -156,9 +174,6 @@ void Datafile::commit () { if (file.open ()) { - // Sort the intervals by ascending start time. - std::sort (_lines.begin (), _lines.end ()); - // Write out all the lines. file.truncate (); for (auto& line : _lines) @@ -202,16 +217,15 @@ void Datafile::load_lines () if (file.open ()) { // Load the data. - std::vector read_lines; - file.read (read_lines); + file.read (_lines); file.close (); - // Append the lines that were read. - for (auto& line : read_lines) - _lines.push_back (line); + // We need the data to be sorted, sort it on load in case other tools or + // the user hand modified the datafiles + std::sort (_lines.begin (), _lines.end ()); _lines_loaded = true; - debug (format ("{1}: {2} intervals", file.name (), read_lines.size ())); + debug (format ("{1}: {2} intervals", file.name (), _lines.size ())); } } diff --git a/src/Datafile.h b/src/Datafile.h index ea608f4c..ffd0f956 100644 --- a/src/Datafile.h +++ b/src/Datafile.h @@ -36,14 +36,14 @@ class Datafile { public: - Datafile () = default; - void initialize (const std::string&); + explicit Datafile (const std::string&); std::string name () const; + const Range& range () const; std::string lastLine (); const std::vector & allLines (); - void addInterval (const Interval&); + bool addInterval (const Interval&); void deleteInterval (const Interval&); void commit (); diff --git a/src/Extensions.cpp b/src/Extensions.cpp index c3174314..8c68d63a 100644 --- a/src/Extensions.cpp +++ b/src/Extensions.cpp @@ -36,7 +36,7 @@ #include //////////////////////////////////////////////////////////////////////////////// -void Extensions::initialize (const std::string& location) +Extensions::Extensions (const std::string& location) { // Scan extension directory. Directory d (location); @@ -47,7 +47,9 @@ void Extensions::initialize (const std::string& location) std::sort (_scripts.begin (), _scripts.end ()); } else + { throw std::string ("Extension directory not readable: ") + d._data; + } } //////////////////////////////////////////////////////////////////////////////// @@ -57,7 +59,7 @@ void Extensions::debug () } //////////////////////////////////////////////////////////////////////////////// -std::vector Extensions::all () const +const std::vector & Extensions::all () const { return _scripts; } diff --git a/src/Extensions.h b/src/Extensions.h index f5385951..fbf800b7 100644 --- a/src/Extensions.h +++ b/src/Extensions.h @@ -33,10 +33,15 @@ class Extensions { public: - Extensions () = default; - void initialize (const std::string&); + Extensions () = delete; + Extensions (const std::string&); + Extensions (const Extensions&) = delete; + Extensions (Extensions&&) = default; + Extensions& operator= (const Extensions&) = delete; + Extensions& operator= (Extensions&&) = default; + void debug (); - std::vector all () const; + const std::vector & all () const; int callExtension (const std::string&, const std::vector &, std::vector &) const; std::string dump () const; diff --git a/src/Interval.cpp b/src/Interval.cpp index 6484b785..994f5377 100644 --- a/src/Interval.cpp +++ b/src/Interval.cpp @@ -208,7 +208,7 @@ void Interval::setAnnotation (const std::string& annotation) } //////////////////////////////////////////////////////////////////////////////// -std::string Interval::getAnnotation () +const std::string& Interval::getAnnotation () const { return annotation; } diff --git a/src/Interval.h b/src/Interval.h index 9ae06ca2..8c8f217e 100644 --- a/src/Interval.h +++ b/src/Interval.h @@ -49,7 +49,7 @@ class Interval : public Range void setRange (const Datetime& start, const Datetime& end); void setAnnotation(const std::string& annotation); - std::string getAnnotation(); + const std::string& getAnnotation() const; std::string serialize () const; std::string json () const; diff --git a/src/Journal.cpp b/src/Journal.cpp index fb08797d..437b945e 100644 --- a/src/Journal.cpp +++ b/src/Journal.cpp @@ -58,11 +58,8 @@ std::vector loadJournal (AtomicFile& undo) } //////////////////////////////////////////////////////////////////////////////// -void Journal::initialize (const std::string& location, int size) +Journal::Journal (const std::string& location, int size) : _location (location), _size (size) { - _location = location; - _size = size; - if (! enabled ()) { AtomicFile undo (_location); diff --git a/src/Journal.h b/src/Journal.h index ecd2e0a0..159c090f 100644 --- a/src/Journal.h +++ b/src/Journal.h @@ -36,11 +36,12 @@ class Journal { public: - Journal() = default; - Journal(const Journal&) = delete; + Journal () = delete; + Journal (const std::string&, int); + Journal (const Journal&) = delete; + Journal (Journal&&) = default; Journal& operator= (const Journal&) = delete; - - void initialize(const std::string&, int); + Journal& operator= (Journal&&) = default; void startTransaction (); void endTransaction (); @@ -53,7 +54,7 @@ class Journal private: void recordUndoAction (const std::string &, const std::string &, const std::string &); - std::string _location {"~/.timewarrior/data/undo.data"}; + std::string _location {}; std::shared_ptr _currentTransaction = nullptr; int _size {0}; }; diff --git a/src/Rules.h b/src/Rules.h index 7b8ddafb..4753429b 100644 --- a/src/Rules.h +++ b/src/Rules.h @@ -38,6 +38,11 @@ class Rules { public: Rules (); + Rules (const Rules&) = delete; + Rules (Rules&&) = default; + Rules& operator= (const Rules&) = delete; + Rules& operator= (Rules&&) = default; + void load (const std::string&, int next = 1); std::string file () const; diff --git a/src/Transaction.cpp b/src/Transaction.cpp index 7db06254..f48d2385 100644 --- a/src/Transaction.cpp +++ b/src/Transaction.cpp @@ -41,11 +41,16 @@ std::vector Transaction::getActions () const std::string Transaction::toString () const { - std::string output = "txn:\n"; + std::string output; - for (auto& action : _actions) + if (! _actions.empty ()) { - output += action.toString (); + output = "txn:\n"; + + for (auto& action : _actions) + { + output += action.toString (); + } } return output; diff --git a/src/init.cpp b/src/init.cpp index 5e8333b4..14caca93 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -120,12 +120,9 @@ void initializeEntities (CLI& cli) } //////////////////////////////////////////////////////////////////////////////// -void initializeDataJournalAndRules ( - const CLI& cli, - Database& database, - Journal& journal, - Rules& rules) +Rules createRules (const CLI& cli) { + Rules rules; // Rose tint my world, make me safe from my trouble and pain. rules.set ("color", isatty (STDOUT_FILENO) ? "on" : "off"); @@ -180,12 +177,6 @@ void initializeDataJournalAndRules ( if (! extensions.exists ()) extensions.create (0700); - // Create data subdirectory if necessary. - Directory data (dbLocation); - data += "data"; - if (! data.exists ()) - data.create (0700); - // Load the configuration data. Path configFile (dbLocation); configFile += "timewarrior.cfg"; @@ -221,41 +212,63 @@ void initializeDataJournalAndRules ( } } - journal.initialize (data._data + "/undo.data", rules.getInteger ("journal.size")); - // Initialize the database (no data read), but files are enumerated. - database.initialize (data._data, journal); + return rules; } //////////////////////////////////////////////////////////////////////////////// -void initializeExtensions ( - CLI& cli, - const Rules& rules, - Extensions& extensions) +Database createDatabase (Rules& rules) { + const std::string& dbLocation = rules.get ("temp.db"); + + // Create data subdirectory if necessary. + Directory data (dbLocation); + data += "data"; + if (! data.exists ()) + { + data.create (0700); + } + + return Database {data._data, rules.getInteger ("journal.size")}; +} + +//////////////////////////////////////////////////////////////////////////////// +Extensions createExtensions (CLI& cli, const Rules& rules) +{ + Directory extDir (rules.get ("temp.db")); extDir += "extensions"; - extensions.initialize (extDir._data); + Extensions extensions {extDir._data}; // Add extensions as CLI entities. - for (auto& ext : extensions.all ()) - cli.entity ("extension", File (ext).name ()); + for (const auto& ext : extensions.all ()) + { + cli.entity ("extension", Path (ext).name ()); + } + + // Re-analyze command because of the new extension entities. + cli.analyze (); // Extensions have a debug mode. if (rules.getBoolean ("debug")) + { extensions.debug (); + } + + return extensions; } //////////////////////////////////////////////////////////////////////////////// int dispatchCommand ( const CLI& cli, Database& database, - Journal& journal, Rules& rules, const Extensions& extensions) { int status {0}; + Journal& journal = database.journal (); + // Debug output. if (rules.getBoolean ("debug")) std::cout << cli.dump () << '\n'; diff --git a/src/timew.cpp b/src/timew.cpp index 49695d30..d598c769 100644 --- a/src/timew.cpp +++ b/src/timew.cpp @@ -80,20 +80,13 @@ int main (int argc, const char** argv) // Scan command line. cli.analyze (); - Journal journal; // Prepare the database, but do not read data. - Database database; - Rules rules; - initializeDataJournalAndRules (cli, database, journal, rules); - - // Load extension script info. - // Re-analyze command because of the new extension entities. - Extensions extensions; - initializeExtensions (cli, rules, extensions); - cli.analyze (); + Rules rules = createRules (cli); + Database database = createDatabase (rules); + Extensions extensions = createExtensions (cli, rules); // Dispatch to commands. - status = dispatchCommand (cli, database, journal, rules, extensions); + status = dispatchCommand (cli, database, rules, extensions); // Save any outstanding changes. database.commit (); diff --git a/src/timew.h b/src/timew.h index 4996a5db..2ed8d89e 100644 --- a/src/timew.h +++ b/src/timew.h @@ -64,9 +64,10 @@ bool validate (const CLI& cli, const Rules& rules, Database&, Interval&); // init.cpp bool lightweightVersionCheck (int, const char**); void initializeEntities (CLI&); -void initializeDataJournalAndRules (const CLI&, Database&, Journal&, Rules&); -void initializeExtensions (CLI&, const Rules&, Extensions&); -int dispatchCommand (const CLI&, Database&, Journal&, Rules&, const Extensions&); +Rules createRules (const CLI&); +Database createDatabase (Rules&); +Extensions createExtensions (CLI&, const Rules&); +int dispatchCommand (const CLI&, Database&, Rules&, const Extensions&); // helper.cpp Color intervalColor (const std::set &, const std::map &); diff --git a/test/.gitignore b/test/.gitignore index 7ada9fc0..4931e237 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,6 +1,7 @@ all.log AtomicFileTest data.t +Database.t Datafile.t DatetimeParser.t exclusion.t diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5339eada..9ce5ef0d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,7 +34,8 @@ include_directories (${CMAKE_SOURCE_DIR} include_directories (${CMAKE_INSTALL_PREFIX}/include) link_directories(${CMAKE_INSTALL_PREFIX}/lib) -set (test_SRCS AtomicFileTest data.t Datafile.t DatetimeParser.t exclusion.t helper.t interval.t range.t rules.t util.t TagInfoDatabase.t) +set (test_SRCS AtomicFileTest data.t Database.t Datafile.t DatetimeParser.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} timew_executable diff --git a/test/Database.t.cpp b/test/Database.t.cpp new file mode 100644 index 00000000..26ddc10d --- /dev/null +++ b/test/Database.t.cpp @@ -0,0 +1,194 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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 + +const std::vector & get_common_inputs () +{ + static std::vector inputs; + if (inputs.empty ()) + { + inputs.reserve (7); + + inputs.emplace_back (Datetime ("2016-06-03T01:00:00"), Datetime ("2016-06-03T02:00:00")); + inputs.emplace_back (Datetime ("2018-06-02T01:00:00"), Datetime ("2018-06-02T02:00:00")); + inputs.emplace_back (Datetime ("2019-06-03T01:00:00"), Datetime ("2019-06-03T02:00:00")); + inputs.emplace_back (Datetime ("2017-06-02T01:00:00"), Datetime ("2017-06-02T02:00:00")); + inputs.emplace_back (Datetime ("2018-06-03T01:00:00"), Datetime ("2018-06-03T02:00:00")); + inputs.emplace_back (Datetime ("2019-06-02T01:00:00"), Datetime ("2019-06-02T02:00:00")); + inputs.emplace_back (Datetime ("2016-06-02T01:00:00"), Datetime ("2016-06-02T02:00:00")); + } + + return inputs; +} + +bool test_database_remains_sorted () +{ + const bool verbose = true; + + TempDir tempDir; + + const std::vector & inputs = get_common_inputs (); + + Database database {tempDir, 0}; + + for (const auto& interval : inputs) + { + database.addInterval (interval, verbose); + } + + database.commit (); + if (!std::is_sorted (database.rbegin (), database.rend ())) + { + return false; + } + + database = Database {tempDir, 0}; + + if (std::distance (database.begin(), database.end()) != static_cast (inputs.size ())) + { + return false; + } + + return true; +} + +bool test_database_handles_add_on_reload () +{ + const bool verbose = true; + TempDir tempDir; + + const std::vector & inputs = get_common_inputs (); + + // First, just load the database with the common inputs and commit + Database database {tempDir, 0}; + + for (const auto& interval : inputs) + { + database.addInterval (interval, verbose); + } + database.commit (); + + // Reset the database + database = Database {tempDir, 0}; + + database.addInterval ({Datetime ("1980-01-01T12:01:01"), Datetime ("1980-01-01T12:01:02")}, verbose); + + if (std::distance (database.begin(), database.end()) != static_cast (inputs.size () + 1)) + { + return false; + } + + if (!std::is_sorted (database.rbegin (), database.rend ())) + { + return false; + } + + return true; +} + +bool test_database_forward_and_reverse_iterators_keep_ordering () +{ + const bool verbose = true; + TempDir tempDir; + + const std::vector & inputs = get_common_inputs (); + + // First, just load the database with the common inputs and commit + Database database {tempDir, 0}; + + for (const auto& interval : inputs) + { + database.addInterval (interval, verbose); + } + + database.commit (); + + // Now reload database and check forward and reverse iterator + database = Database {tempDir, 0}; + + std::vector forward_vector {database.begin (), database.end ()}; + std::reverse (forward_vector.begin (), forward_vector.end ()); + + return std::equal (database.rbegin (), database.rend (), forward_vector.begin ()); +} + +bool test_database_throws_on_bad_location () +{ + try + { + Database database {"baddir", 0}; + return false; + } + catch (const std::string& error) + { + return true; + } + return true; +} + +int main () +{ + enableDebugMode (false); + UnitTest t (5); + bool unexpected_exception = false; + try + { + t.ok (test_database_remains_sorted (), "database stays sorted when intervals are added out of order"); + t.ok (test_database_handles_add_on_reload (), "database remains sorted when mixing new intervals with those loaded from files"); + t.ok (test_database_forward_and_reverse_iterators_keep_ordering (), "database forward and reverse iterators keep same ordering"); + t.ok (test_database_throws_on_bad_location (), "database throws an error when given an invalid location"); + } + catch (const std::string& error) + { + unexpected_exception = true; + std::cerr << "Unexpected exception: " << error << '\n'; + } + catch (const std::exception& error) + { + unexpected_exception = true; + std::cerr << "Unexpected exception: " << error.what () << '\n'; + } + catch (...) + { + unexpected_exception = true; + std::cerr << "Unexpected exception.\n"; + } + + t.ok (unexpected_exception == false, "No unexpected exceptions"); + return 0; +} + diff --git a/test/Datafile.t.cpp b/test/Datafile.t.cpp index d15871cf..ebb1d17a 100644 --- a/test/Datafile.t.cpp +++ b/test/Datafile.t.cpp @@ -25,22 +25,43 @@ //////////////////////////////////////////////////////////////////////////////// #include +#include +#include #include #include #include +bool test_datafile_stays_sorted (UnitTest& t) +{ + TempDir tmp; + + Datafile df {"2020-06.data"}; + + const std::vector & lines = df.allLines (); + + df.addInterval ({Datetime ("2020-06-02T01:00:00"), Datetime ("2020-06-02T02:00:00")}); + t.ok (std::is_sorted (lines.begin (), lines.end ()), "Datafile stays sorted 1"); + df.addInterval ({Datetime ("2020-06-01T01:00:00"), Datetime ("2020-06-01T02:00:00")}); + t.ok (std::is_sorted (lines.begin (), lines.end ()), "Datafile stays sorted 2"); + df.addInterval ({Datetime ("2020-06-04T01:00:00"), Datetime ("2020-06-04T02:00:00")}); + t.ok (std::is_sorted (lines.begin (), lines.end ()), "Datafile stays sorted 3"); + df.addInterval ({Datetime ("2020-06-03T01:00:00"), Datetime ("2020-06-03T02:00:00")}); + t.ok (std::is_sorted (lines.begin (), lines.end ()), "Datafile stays sorted 4"); + + return true; +} + int main () { - UnitTest t (2); + UnitTest t (6); TempDir tempDir; try { - Datafile df; Interval interval {Datetime ("2020-06-01T01:00:00"), Datetime ("2020-06-01T02:00:00")}; - df.initialize ("2020-06.data"); + Datafile df {"2020-06.data"}; df.addInterval (interval); Interval modified {interval}; @@ -58,6 +79,9 @@ int main () { t.fail ("Uncaught exception"); } + + test_datafile_stays_sorted (t); + return 0; } diff --git a/test/TempDir.h b/test/TempDir.h index 0bacc9a0..6f23f170 100644 --- a/test/TempDir.h +++ b/test/TempDir.h @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -45,6 +46,9 @@ class TempDir void clear (); std::vector file_names () const; + const std::string& name () const { return tmpName; }; + operator const std::string& () const { return name (); } + private: std::string tmpName {}; std::string oldDir {}; @@ -53,9 +57,11 @@ class TempDir //////////////////////////////////////////////////////////////////////////////// TempDir::TempDir () { + char template_name[PATH_MAX] = {'\0',}; + oldDir = Directory::cwd (); - char template_name[] = "atomic_XXXXXX"; + strncpy (template_name, (oldDir + "/timew_test_XXXXXX").c_str (), sizeof (template_name) - 1); const char *cwd = ::mkdtemp (template_name); if (cwd == nullptr) {