Issue 720 (#721)

* Implemented a backup scheme for the file writer.

* Also consider backup files when the writer fails.

* Removed test exception

* Added config option for number of backups.
This commit is contained in:
Matthias Köfferlein 2021-02-02 22:47:25 +01:00 committed by Matthias Koefferlein
parent ca05a83021
commit a0cc5fa52a
16 changed files with 693 additions and 59 deletions

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>606</width>
<height>130</height>
<width>611</width>
<height>271</height>
</rect>
</property>
<property name="windowTitle">
@ -67,6 +67,55 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Keep Backup Files</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_3">
<property name="text">
<string>When saving files, the specified number of backups are kept. Backup files are formed by adding &quot;.1&quot;, &quot;.2&quot; etc. to the original file name. &quot;.1&quot; is the most recent one. On saving, the files will be shuffled and &quot;.1&quot; becomes &quot;.2&quot;, &quot;.2 becomes &quot;.3&quot; up to the specified number of backup files.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>252</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Number of backup files</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="keep_backups"/>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>(0 for &quot;no backups)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>

View File

@ -40,6 +40,10 @@ static const std::string cfg_synchronized_views ("synchronized-views");
static const std::string cfg_edit_mode ("edit-mode");
static const std::string cfg_custom_macro_paths ("custom-macro-paths");
static const std::string cfg_mru ("mru");
static const std::string cfg_mru_layer_properties ("mru-layer-properties");
static const std::string cfg_mru_sessions ("mru-sessions");
static const std::string cfg_mru_bookmarks ("mru-bookmarks");
static const std::string cfg_keep_backups ("keep-backups");
static const std::string cfg_technologies ("technology-data");
static const std::string cfg_key_bindings ("key-bindings");
static const std::string cfg_menu_items_hidden ("menu-items-hidden");

View File

@ -190,6 +190,10 @@ MainConfigPage7::setup (lay::PluginRoot *root)
bool en = true;
root->config_get (cfg_layout_file_watcher_enabled, en);
mp_ui->check_for_updates->setChecked (en);
int kb = 0;
root->config_get (cfg_keep_backups, kb);
mp_ui->keep_backups->setValue (kb);
}
void
@ -197,6 +201,7 @@ MainConfigPage7::commit (lay::PluginRoot *root)
{
try {
root->config_set (cfg_layout_file_watcher_enabled, mp_ui->check_for_updates->isChecked ());
root->config_set (cfg_keep_backups, mp_ui->keep_backups->value ());
} catch (...) { }
}

View File

@ -426,6 +426,7 @@ MainWindow::MainWindow (QApplication *app, lay::Plugin *plugin_parent, const cha
m_mode (std::numeric_limits<unsigned int>::max ()),
mp_setup_form (0),
m_open_mode (0),
m_keep_backups (0),
m_disable_tab_selected (false),
m_exited (false),
dm_do_update_menu (this, &MainWindow::do_update_menu),
@ -1606,6 +1607,14 @@ MainWindow::configure (const std::string &name, const std::string &value)
return true;
} else if (name == cfg_keep_backups) {
int kb = 0;
tl::from_string (value, kb);
m_keep_backups = kb;
return false; // not taken - let others set it too.
} else if (name == cfg_micron_digits) {
unsigned int d = 5;
@ -3160,7 +3169,7 @@ MainWindow::cm_save_current_cell_as ()
}
}
cv->save_as (fn, om, options, false /*don't update*/);
cv->save_as (fn, om, options, false /*don't update*/, m_keep_backups);
add_mru (fn, cv->tech_name ());
@ -3239,7 +3248,7 @@ MainWindow::do_save (bool as)
break;
}
current_view ()->save_as ((unsigned int) cv_index, fn, om, options, true);
current_view ()->save_as ((unsigned int) cv_index, fn, om, options, true, m_keep_backups);
add_mru (fn, current_view ()->cellview (cv_index)->tech_name ());
}
@ -3284,7 +3293,7 @@ MainWindow::cm_save_all ()
}
}
view (view_index)->save_as (cv_index, fn, om, options, true);
view (view_index)->save_as (cv_index, fn, om, options, true, m_keep_backups);
add_mru (fn, current_view ()->cellview (cv_index)->tech_name ());
}

View File

@ -894,6 +894,7 @@ private:
SettingsForm *mp_setup_form;
std::vector <lay::LayoutView *> mp_views;
int m_open_mode;
int m_keep_backups;
std::vector<std::pair<std::string, std::string> > m_mru;
QStatusBar *mp_status_bar;
QStackedWidget *mp_main_stack_widget;

View File

@ -316,14 +316,14 @@ static void delete_layers2 (lay::LayoutView *view, unsigned int index, const std
static void save_as1 (lay::LayoutView *view, unsigned int index, const std::string &filename, const db::SaveLayoutOptions &options)
{
view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true);
view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true, 0);
}
static void save_as2 (lay::LayoutView *view, unsigned int index, const std::string &filename, bool /*gzip*/, const db::SaveLayoutOptions &options)
{
// because set_format_from_name always returns true now, we ignore the gzip option -
// it's probably used only in that context.
view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true);
view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true, 0);
}
#if defined(HAVE_QTBINDINGS)

View File

@ -290,7 +290,7 @@ LayoutHandle::update_save_options (db::SaveLayoutOptions &options)
}
void
LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update)
LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups)
{
if (update) {
@ -314,8 +314,13 @@ LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode
{
// The write needs to be finished before the file watcher gets the new modification time
db::Writer writer (options);
tl::OutputStream stream (fn, om);
writer.write (*mp_layout, stream);
tl::OutputStream stream (fn, om, false, keep_backups);
try {
writer.write (*mp_layout, stream);
} catch (...) {
stream.reject ();
throw;
}
}
if (update) {

View File

@ -215,7 +215,7 @@ public:
* Save the layout under the given file name and with the given options.
* If update is true, this method updates the cell view's filename, title, save options and dirty flag.
*/
void save_as (const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update = true);
void save_as (const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update = true, int keep_backups = 0);
/**
* @brief Sets the save options and a flag indicating whether they are valid

View File

@ -2078,12 +2078,12 @@ LayoutView::signal_selection_changed ()
}
void
LayoutView::save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update)
LayoutView::save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups)
{
tl_assert (index < cellviews ());
tl::SelfTimer timer (tl::verbosity () >= 11, tl::to_string (QObject::tr ("Saving")));
cellview (index)->save_as (filename, om, options, update);
cellview (index)->save_as (filename, om, options, update, keep_backups);
cellview_changed (index);
}

View File

@ -775,7 +775,7 @@ public:
* @brief Save the given cellview into the given file (with options)
* If "update" is true, the cell view's properties will be updated (options, filename etc.).
*/
void save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update);
void save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups);
/**
* @brief Implementation of the undo operations

View File

@ -435,6 +435,21 @@ bool mkpath (const std::string &p)
return true;
}
bool rename_file (const std::string &path, const std::string &new_name)
{
// resolve relative names in new_name
std::string new_path = new_name;
if (! tl::is_absolute (new_path)) {
new_path = tl::combine_path (tl::dirname (path), new_name);
}
#if defined(_WIN32)
return _wrename (tl::to_wstring (path).c_str (), tl::to_wstring (new_path).c_str ()) == 0;
#else
return rename (tl::to_local (path).c_str (), tl::to_local (new_path).c_str ()) == 0;
#endif
}
bool rm_file (const std::string &path)
{
#if defined(_WIN32)

View File

@ -125,6 +125,11 @@ bool TL_PUBLIC is_dir (const std::string &s);
*/
std::vector<std::string> TL_PUBLIC dir_entries (const std::string &s, bool with_files = true, bool with_dirs = true, bool without_dotfiles = false);
/**
* @brief Rename the given file
*/
bool TL_PUBLIC rename_file (const std::string &path, const std::string &new_name);
/**
* @brief Removes the given file and returns true on success
*/

View File

@ -39,6 +39,7 @@
#include "tlDeflate.h"
#include "tlAssert.h"
#include "tlFileUtils.h"
#include "tlLog.h"
#include "tlException.h"
#include "tlString.h"
@ -692,7 +693,15 @@ OutputStream::OutputStream (OutputStreamBase &delegate, bool as_text)
mp_buffer = new char[m_buffer_capacity];
}
OutputStream::OutputStreamMode
OutputStream::OutputStream (OutputStreamBase *delegate, bool as_text)
: m_pos (0), mp_delegate (delegate), m_owns_delegate (true), m_as_text (as_text)
{
m_buffer_capacity = 16384;
m_buffer_pos = 0;
mp_buffer = new char[m_buffer_capacity];
}
OutputStream::OutputStreamMode
OutputStream::output_mode_from_filename (const std::string &abstract_path, OutputStream::OutputStreamMode om)
{
if (om == OM_Auto) {
@ -707,16 +716,16 @@ OutputStream::output_mode_from_filename (const std::string &abstract_path, Outpu
}
static
OutputStreamBase *create_file_stream (const std::string &path, OutputStream::OutputStreamMode om)
OutputStreamBase *create_file_stream (const std::string &path, OutputStream::OutputStreamMode om, int keep_backups)
{
if (om == OutputStream::OM_Zlib) {
return new OutputZLibFile (path);
return new OutputZLibFile (path, keep_backups);
} else {
return new OutputFile (path);
return new OutputFile (path, keep_backups);
}
}
OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode om, bool as_text)
OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode om, bool as_text, int keep_backups)
: m_pos (0), mp_delegate (0), m_owns_delegate (false), m_as_text (as_text), m_path (abstract_path)
{
// Determine output mode
@ -728,9 +737,9 @@ OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode o
} else if (ex.test ("pipe:")) {
mp_delegate = new OutputPipe (ex.get ());
} else if (ex.test ("file:")) {
mp_delegate = create_file_stream (ex.get (), om);
mp_delegate = create_file_stream (ex.get (), om, keep_backups);
} else {
mp_delegate = create_file_stream (abstract_path, om);
mp_delegate = create_file_stream (abstract_path, om, keep_backups);
}
m_owns_delegate = true;
@ -742,21 +751,42 @@ OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode o
OutputStream::~OutputStream ()
{
close ();
try {
close ();
} catch (...) {
// no recursive exceptions
}
}
void
OutputStream::close ()
{
flush ();
try {
flush ();
if (mp_delegate && m_owns_delegate) {
delete mp_delegate;
mp_delegate = 0;
}
if (mp_buffer) {
delete[] mp_buffer;
mp_buffer = 0;
}
} catch (...) {
if (mp_delegate && m_owns_delegate) {
delete mp_delegate;
mp_delegate = 0;
}
if (mp_buffer) {
delete[] mp_buffer;
mp_buffer = 0;
}
throw;
if (mp_delegate && m_owns_delegate) {
delete mp_delegate;
mp_delegate = 0;
}
if (mp_buffer) {
delete[] mp_buffer;
mp_buffer = 0;
}
}
@ -867,11 +897,110 @@ OutputStream::seek (size_t pos)
m_pos = pos;
}
// ---------------------------------------------------------------
// OutputFileBase implementation
OutputFileBase::OutputFileBase (const std::string &path, int keep_backups)
: m_keep_backups (keep_backups), m_path (path), m_has_error (false)
{
if (tl::file_exists (path)) {
m_backup_path = path + ".~backup";
if (tl::file_exists (m_backup_path)) {
if (! tl::rm_file (m_backup_path)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Could not create backup file: unable to remove existing file '%s'")), m_backup_path);
m_backup_path = std::string ();
}
}
if (! m_backup_path.empty ()) {
if (! tl::rename_file (path, m_backup_path)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Could not create backup file: unable to rename original file '%s' to backup file")), path, m_backup_path);
m_backup_path = std::string ();
}
}
}
}
OutputFileBase::~OutputFileBase ()
{
if (! m_backup_path.empty ()) {
if (m_has_error) {
if (! tl::rm_file (m_path)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Could not restore backup file: unable to remove file '%s'")), m_path);
} else if (! tl::rename_file (m_backup_path, m_path)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Could not restore backup file: unable to rename file '%s' back to '%s'")), m_backup_path, m_path);
}
} else {
if (m_keep_backups == 0) {
if (! tl::rm_file (m_backup_path)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Could not remove backup file '%s'")), m_backup_path);
}
} else {
// shuffle backup files
int n = 1;
for ( ; m_keep_backups < 0 || n < m_keep_backups; ++n) {
std::string p = m_path + "." + tl::to_string (n);
if (! tl::file_exists (p)) {
break;
}
}
while (n > 0) {
std::string p = m_path + "." + tl::to_string (n);
std::string pprev = n > 1 ? (m_path + "." + tl::to_string (n - 1)) : m_backup_path;
if (tl::file_exists (p)) {
if (! tl::rm_file (p)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Error shuffling backup files: unable to remove file '%s'")), p);
}
}
if (! tl::rename_file (pprev, p)) {
tl::warn << tl::sprintf (tl::to_string (tr ("Error shuffling backup files: unable to rename file '%s' to '%s'")), pprev, p);
}
--n;
}
}
}
}
}
void OutputFileBase::seek (size_t s)
{
try {
seek_file (s);
} catch (...) {
reject ();
throw;
}
}
void OutputFileBase::write (const char *b, size_t n)
{
try {
write_file (b, n);
} catch (...) {
reject ();
throw;
}
}
void OutputFileBase::reject ()
{
m_has_error = true;
}
// ---------------------------------------------------------------
// OutputFile implementation
OutputFile::OutputFile (const std::string &path)
: m_fd (-1)
OutputFile::OutputFile (const std::string &path, int keep_backups)
: OutputFileBase (path, keep_backups), m_fd (-1)
{
m_source = path;
#if defined(_WIN32)
@ -898,11 +1027,11 @@ OutputFile::~OutputFile ()
close (m_fd);
#endif
m_fd = -1;
}
}
}
void
OutputFile::seek (size_t s)
void
OutputFile::seek_file (size_t s)
{
tl_assert (m_fd >= 0);
#if defined(_WIN64)
@ -914,8 +1043,8 @@ OutputFile::seek (size_t s)
#endif
}
void
OutputFile::write (const char *b, size_t n)
void
OutputFile::write_file (const char *b, size_t n)
{
tl_assert (m_fd >= 0);
#if defined(_WIN32)
@ -931,8 +1060,8 @@ OutputFile::write (const char *b, size_t n)
// ---------------------------------------------------------------
// OutputZLibFile implementation
OutputZLibFile::OutputZLibFile (const std::string &path)
: mp_d (new ZLibFilePrivate ())
OutputZLibFile::OutputZLibFile (const std::string &path, int keep_backups)
: OutputFileBase (path, keep_backups), mp_d (new ZLibFilePrivate ())
{
m_source = path;
#if defined(_WIN32)
@ -960,7 +1089,7 @@ OutputZLibFile::~OutputZLibFile ()
}
void
OutputZLibFile::write (const char *b, size_t n)
OutputZLibFile::write_file (const char *b, size_t n)
{
tl_assert (mp_d->zs != NULL);
int ret = gzwrite (mp_d->zs, (char *) b, (unsigned int) n);

View File

@ -703,6 +703,14 @@ public:
return false;
}
/**
* @brief Rejects the output - for delegates supporting unrolling, this means the original file is restored
*/
virtual void reject ()
{
// ... the default implementation does not support this feature ..
}
private:
// No copying
OutputStreamBase (const OutputStreamBase &);
@ -831,13 +839,81 @@ private:
std::ostringstream m_stream;
};
/**
* @brief A base for file writer delegates
*
* This class mainly provides safety services for the file writer.
* When writing a file, it will keep a backup until the file actually
* has been written. This way, a network or disk full error will not
* comprimise the file's content. The backup file name is created
* by appending ".~backup" to the original file path.
*
* In addition, a specified number or persistent backup files can
* be kept. The first backup will be called like the original file
* with ".1" appended. The second will be called ".2" etc.
* The backups will be shuffled, so ".1" is always the most recent
* one while older ones get bigger numbers.
*
* The number of backups can be specified with "keep_backups". If this
* count is zero, no backups will be kept. If the count is larger than
* zero, the specified maximum number of backups is kept. If less than
* zero, an infinite number of backups is made. But beware: shuffling
* will become increasingly expensive.
*/
class TL_PUBLIC OutputFileBase
: public OutputStreamBase
{
public:
/**
* @brief Constructor
*
* @param path The (relative) path of the file to write
* @param keep_backups The number of backups to keep (0: none, -1: infinite)
*/
OutputFileBase (const std::string &path, int keep_backups);
/**
* @brief Destructor
*/
virtual ~OutputFileBase ();
/**
* @brief Seek to the specified position
*
* Writing continues at that position after a seek.
*/
virtual void seek (size_t s);
/**
* @brief Write to a file
*
* Implements the basic write method.
* Will throw a FileWriteErrorException if an error occurs.
*/
virtual void write (const char *b, size_t n);
/**
* @brief Unrolls the output
*/
virtual void reject ();
protected:
virtual void seek_file (size_t s) = 0;
virtual void write_file (const char *b, size_t n) = 0;
private:
int m_keep_backups;
std::string m_backup_path, m_path;
bool m_has_error;
};
/**
* @brief A zlib output file delegate
*
* Implements the writer for a zlib stream
*/
class TL_PUBLIC OutputZLibFile
: public OutputStreamBase
: public OutputFileBase
{
public:
/**
@ -847,9 +923,10 @@ public:
* object. open() will throw a FileOpenErrorException if
* an error occurs.
*
* @param path The (relative) path of the file to open
* @param path The (relative) path of the file to write
* @param keep_backups The number of backups to keep (0: none, -1: infinite)
*/
OutputZLibFile (const std::string &path);
OutputZLibFile (const std::string &path, int keep_backups);
/**
* @brief Close the file
@ -858,13 +935,19 @@ public:
*/
virtual ~OutputZLibFile ();
protected:
/**
* @brief Write to a file
*
* Implements the basic write method.
* Will throw a ZLibWriteErrorException if an error occurs.
*/
virtual void write (const char *b, size_t n);
virtual void write_file (const char *b, size_t n);
/**
* @brief The seek operation isn't implemented for zlib files
*/
virtual void seek_file (size_t /*s*/) { }
private:
// No copying
@ -881,7 +964,7 @@ private:
* Implements the writer for ordinary files.
*/
class TL_PUBLIC OutputFile
: public OutputStreamBase
: public OutputFileBase
{
public:
/**
@ -891,10 +974,10 @@ public:
* object. open() will throw a FileOpenErrorException if
* an error occurs.
*
* @param path The (relative) path of the file to open
* @param read True, if the file should be read, false on write.
* @param path The (relative) path of the file to write
* @param keep_backups The number of backups to keep (0: none, -1: infinite)
*/
OutputFile (const std::string &path);
OutputFile (const std::string &path, int keep_backups = 0);
/**
* @brief Close the file
@ -903,20 +986,21 @@ public:
*/
virtual ~OutputFile ();
/**
* @brief Returns a value indicating whether that stream supports seek
*/
virtual bool supports_seek ()
{
return true;
}
protected:
/**
* @brief Seek to the specified position
*
* Writing continues at that position after a seek.
*/
virtual void seek (size_t s);
/**
* @brief Returns a value indicating whether that stream supports seek
*/
bool supports_seek ()
{
return true;
}
virtual void seek_file (size_t s);
/**
* @brief Write to a file
@ -924,7 +1008,7 @@ public:
* Implements the basic write method.
* Will throw a FileWriteErrorException if an error occurs.
*/
virtual void write (const char *b, size_t n);
virtual void write_file (const char *b, size_t n);
private:
// No copying
@ -1030,13 +1114,20 @@ public:
*/
OutputStream (OutputStreamBase &delegate, bool as_text = false);
/**
* @brief Default constructor
*
* This constructor takes a delegate object. The stream will own the delegate.
*/
OutputStream (OutputStreamBase *delegate, bool as_text = false);
/**
* @brief Open an output stream with the given path and stream mode
*
* This will automatically create a delegate object and delete it later.
* If "as_text" is true, the output will be formatted with the system's line separator.
*/
OutputStream (const std::string &abstract_path, OutputStreamMode om = OM_Auto, bool as_text = false);
OutputStream (const std::string &abstract_path, OutputStreamMode om = OM_Auto, bool as_text = false, int keep_backups = 0);
/**
* @brief Destructor
@ -1120,6 +1211,16 @@ public:
return *this;
}
/**
* @brief Rejects the output - for delegates which support backup, this means the original file is restored
*/
void reject () const
{
if (mp_delegate) {
mp_delegate->reject ();
}
}
/**
* @brief Returns a value indicating whether that stream supports seek
*/

View File

@ -676,3 +676,82 @@ TEST (17)
EXPECT_EQ (tl::is_same_file (yfile, tl::combine_path (dpath, "../d/y")), true);
}
// rename_file
TEST (18)
{
std::string tp = tl::absolute_file_path (tmp_file ());
std::string xfile = tl::combine_path (tp, "x");
std::string yfile = tl::combine_path (tp, "y");
std::string zfile = tl::combine_path (tl::combine_path (tp, "dir"), "z");
tl::mkpath (tl::combine_path (tp, "dir"));
{
tl::OutputStream os (xfile);
os << "hello, world!\n";
}
EXPECT_EQ (tl::file_exists (xfile), true);
EXPECT_EQ (tl::file_exists (yfile), false);
EXPECT_EQ (tl::file_exists (zfile), false);
{
tl::InputStream is (xfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
tl::rename_file (xfile, yfile);
EXPECT_EQ (tl::file_exists (xfile), false);
EXPECT_EQ (tl::file_exists (yfile), true);
EXPECT_EQ (tl::file_exists (zfile), false);
{
tl::InputStream is (yfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
tl::rename_file (yfile, "x");
EXPECT_EQ (tl::file_exists (xfile), true);
EXPECT_EQ (tl::file_exists (yfile), false);
EXPECT_EQ (tl::file_exists (zfile), false);
{
tl::InputStream is (xfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
tl::rename_file (xfile, zfile);
EXPECT_EQ (tl::file_exists (xfile), false);
EXPECT_EQ (tl::file_exists (yfile), false);
EXPECT_EQ (tl::file_exists (zfile), true);
{
tl::InputStream is (zfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
tl::rename_file (zfile, xfile);
EXPECT_EQ (tl::file_exists (xfile), true);
EXPECT_EQ (tl::file_exists (yfile), false);
EXPECT_EQ (tl::file_exists (zfile), false);
{
tl::InputStream is (xfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
tl::rename_file (xfile, tl::combine_path ("dir", "z"));
EXPECT_EQ (tl::file_exists (xfile), false);
EXPECT_EQ (tl::file_exists (yfile), false);
EXPECT_EQ (tl::file_exists (zfile), true);
{
tl::InputStream is (zfile);
EXPECT_EQ (is.read_all (), "hello, world!\n");
}
}

View File

@ -162,3 +162,235 @@ TEST(TextInputStream)
EXPECT_EQ (tis.read_all (), "Hello, world!\nWith another line\n\nseparated by a LFCR and CRLF.");
}
}
namespace
{
class BrokenOutputStream
: public tl::OutputFile
{
public:
BrokenOutputStream (const std::string &path, int keep_backups)
: tl::OutputFile (path, keep_backups)
{ }
void write_file(const char *b, size_t n)
{
for (const char *p = b; p < b + n; ++p) {
if (*p == '!') {
throw tl::Exception ("Bang!");
}
}
tl::OutputFile::write (b, n);
}
};
}
TEST(SafeOutput)
{
std::string tp = tmp_file ("x");
{
tl::OutputStream os (tp);
os << "blabla\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::OutputStream os (tp);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "Hello, world!\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "Hello, world!\n");
}
try {
BrokenOutputStream broken (tp, 0);
tl::OutputStream os (broken);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "Hi!\n";
os.flush (); // raises the exception
EXPECT_EQ (true, false);
} catch (...) {
// '!' raises an exception
}
// The original content is restored now
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "Hello, world!\n");
}
try {
BrokenOutputStream *broken = new BrokenOutputStream (tp, 0);
tl::OutputStream os (broken);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "Hi!\n";
os.flush (); // raises the exception
EXPECT_EQ (true, false);
} catch (...) {
// '!' raises an exception
}
// The original content is restored now
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "Hello, world!\n");
}
}
TEST(Backups)
{
std::string tp = tmp_file ("x");
{
tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
os << "1\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp + ".1"), false);
EXPECT_EQ (tl::file_exists (tp + ".2"), false);
EXPECT_EQ (tl::file_exists (tp + ".3"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "1\n");
}
{
tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "2\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp + ".1"), true);
EXPECT_EQ (tl::file_exists (tp + ".2"), false);
EXPECT_EQ (tl::file_exists (tp + ".3"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "2\n");
}
{
tl::InputStream is (tp + ".1");
EXPECT_EQ (is.read_all (), "1\n");
}
{
tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "3\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp + ".1"), true);
EXPECT_EQ (tl::file_exists (tp + ".2"), true);
EXPECT_EQ (tl::file_exists (tp + ".3"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "3\n");
}
{
tl::InputStream is (tp + ".1");
EXPECT_EQ (is.read_all (), "2\n");
}
{
tl::InputStream is (tp + ".2");
EXPECT_EQ (is.read_all (), "1\n");
}
{
tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "4\n";
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp + ".1"), true);
EXPECT_EQ (tl::file_exists (tp + ".2"), true);
EXPECT_EQ (tl::file_exists (tp + ".3"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "4\n");
}
{
tl::InputStream is (tp + ".1");
EXPECT_EQ (is.read_all (), "3\n");
}
{
tl::InputStream is (tp + ".2");
EXPECT_EQ (is.read_all (), "2\n");
}
try {
BrokenOutputStream broken (tp, 2);
tl::OutputStream os (broken);
EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
EXPECT_EQ (tl::file_exists (tp), true);
os << "5!\n";
os.flush (); // raises the exception
EXPECT_EQ (true, false);
} catch (...) {
// '!' raises an exception
}
EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
EXPECT_EQ (tl::file_exists (tp + ".1"), true);
EXPECT_EQ (tl::file_exists (tp + ".2"), true);
EXPECT_EQ (tl::file_exists (tp + ".3"), false);
EXPECT_EQ (tl::file_exists (tp), true);
{
tl::InputStream is (tp);
EXPECT_EQ (is.read_all (), "4\n");
}
{
tl::InputStream is (tp + ".1");
EXPECT_EQ (is.read_all (), "3\n");
}
{
tl::InputStream is (tp + ".2");
EXPECT_EQ (is.read_all (), "2\n");
}
}