2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2024-02-28 15:45:30 +01:00
// https://duplicati.com, hello@duplicati.com
2025-06-02 11:50:44 +02:00
//
// 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
2024-02-28 15:45:30 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-06-02 11:50:44 +02:00
//
// The above copyright notice and this permission notice shall be included in
2024-02-28 15:45:30 +01:00
// all copies or substantial portions of the Software.
2025-06-02 11:50:44 +02:00
//
// 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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2019-09-15 10:33:35 -07:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2025-04-16 08:05:38 +02:00
using System.Text ;
2019-09-15 10:33:35 -07:00
using System.Threading ;
using System.Threading.Tasks ;
using Duplicati.Library.Interface ;
2025-03-04 11:48:40 +01:00
using Duplicati.Library.Logging ;
2019-09-15 10:33:35 -07:00
using Duplicati.Library.Main ;
2019-09-15 13:45:43 -07:00
using Duplicati.Library.Main.Database ;
2019-12-30 13:32:21 -08:00
using Duplicati.Library.Main.Volumes ;
2019-09-15 10:33:35 -07:00
using NUnit.Framework ;
2019-12-30 13:32:21 -08:00
using IFileEntry = Duplicati . Library . Interface . IFileEntry ;
2019-09-15 15:05:03 -07:00
using Utility = Duplicati . Library . Utility . Utility ;
2025-06-13 11:48:24 +02:00
using Assert = NUnit . Framework . Legacy . ClassicAssert ;
2019-09-15 10:33:35 -07:00
namespace Duplicati.UnitTest
{
public class DisruptionTests : BasicSetupHelper
{
2019-09-15 15:28:53 -07:00
// Files to create in MB.
2024-04-18 11:49:39 +02:00
private readonly int [] fileSizes = { 10 , 20 , 30 };
2019-09-15 15:28:53 -07:00
2019-09-15 10:33:35 -07:00
private void ModifySourceFiles ()
{
2019-09-15 15:28:53 -07:00
foreach ( int size in this . fileSizes )
2019-09-15 10:33:35 -07:00
{
byte [] data = new byte [ size * 1024 * 1024 ];
Random rng = new Random ();
rng . NextBytes ( data );
File . WriteAllBytes ( Path . Combine ( this . DATAFOLDER , size + "MB" ), data );
}
}
2019-09-15 12:50:31 -07:00
2025-03-06 15:22:57 +01:00
private async Task <( IBackupResults Result , List < string > ModifiedFiles )> RunPartialBackup ( Controller controller )
2019-11-28 21:42:58 -08:00
{
this . ModifySourceFiles ();
2024-12-18 08:27:59 +01:00
var stopped = new TaskCompletionSource < bool >();
2025-03-06 15:22:57 +01:00
var modifiedFiles = new List < string >();
2024-12-18 08:27:59 +01:00
controller . OnOperationStarted = r =>
2024-04-18 11:49:39 +02:00
{
2024-12-18 08:27:59 +01:00
var pv = r as ITaskControlProvider ;
if ( pv == null )
throw new Exception ( "Task control provider not found" );
#if DEBUG
pv . TaskControl . TestMethodCallback = ( path ) =>
{
2025-03-06 15:22:57 +01:00
modifiedFiles . Add ( path );
2025-03-06 18:26:56 +01:00
if ( modifiedFiles . Count == 3 )
2024-12-18 08:27:59 +01:00
{
2025-03-06 10:48:52 +01:00
Thread . Sleep ( 500 );
2024-12-18 08:27:59 +01:00
controller . Stop ();
2025-01-28 08:55:28 +01:00
stopped . TrySetResult ( true );
2024-12-18 08:27:59 +01:00
}
};
#endif
};
2024-04-18 11:49:39 +02:00
2024-12-18 08:27:59 +01:00
// ReSharper disable once AccessToDisposedClosure
Task < IBackupResults > backupTask = Task . Run (() => controller . Backup ( new [] { this . DATAFOLDER }));
2024-04-18 11:49:39 +02:00
2024-12-18 08:27:59 +01:00
var t = await Task . WhenAny ( backupTask , stopped . Task ). ConfigureAwait ( false );
if ( t != stopped . Task )
2024-04-18 11:49:39 +02:00
throw new Exception ( "Backup task completed before we could stop it" );
2019-11-28 21:42:58 -08:00
2025-03-06 15:22:57 +01:00
return ( await backupTask . ConfigureAwait ( false ), modifiedFiles );
2019-11-28 21:42:58 -08:00
}
2022-05-05 22:30:15 -07:00
[SetUp]
public void SetUp ()
2019-09-15 10:33:35 -07:00
{
this . ModifySourceFiles ();
}
2019-09-15 12:50:31 -07:00
2019-12-30 13:32:21 -08:00
[Test]
[Category("Disruption")]
public async Task FilesetFiles ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2019-12-30 13:32:21 -08:00
// Choose a dblock size that is small enough so that more than one volume is needed.
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
["dblock-size"] = "10mb" ,
// This allows us to inspect the dlist files without needing the BackendManager (which is inaccessible here) to decrypt them.
2024-12-18 08:27:59 +01:00
["no-encryption"] = "true" ,
2025-01-28 08:55:28 +01:00
["disable-file-scanner"] = "true" ,
["concurrency-fileprocessors"] = "1" ,
2019-12-30 13:32:21 -08:00
};
// Run a full backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2024-04-18 11:49:39 +02:00
IBackupResults backupResults = c . Backup ( new [] { this . DATAFOLDER });
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
2019-12-30 13:32:21 -08:00
}
2024-04-18 11:49:39 +02:00
// Inject some spacing to allow for the purged fileset
Thread . Sleep ( 2000 );
2019-12-30 13:32:21 -08:00
// Run a partial backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-12-30 13:32:21 -08:00
}
Dictionary < DateTime , int > GetBackupTypesFromRemoteFiles ( Controller c , out List < string > filelistFiles )
{
Dictionary < DateTime , int > map = new Dictionary < DateTime , int >();
filelistFiles = new List < string >();
IListRemoteResults remoteFiles = c . ListRemote ();
foreach ( IFileEntry file in remoteFiles . Files )
{
IParsedVolume volume = VolumeBase . ParseFilename ( file );
if ( volume != null && volume . FileType == RemoteVolumeType . Files )
{
string dlistFile = Path . Combine ( this . TARGETFOLDER , volume . File . Name );
filelistFiles . Add ( dlistFile );
VolumeBase . FilesetData filesetData = VolumeReaderBase . GetFilesetData ( volume . CompressionModule , dlistFile , new Options ( options ));
map [ volume . Time ] = filesetData . IsFullBackup ? BackupType . FULL_BACKUP : BackupType . PARTIAL_BACKUP ;
}
}
return map ;
}
// Purge a file and verify that the fileset file exists in the new dlist files.
List < string > dlistFiles ;
Dictionary < DateTime , int > backupTypeMap ;
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . PurgeFiles ( new Library . Utility . FilterExpression ( $"{this.DATAFOLDER}/*{this.fileSizes[0]}*" )));
2020-07-05 15:00:08 -07:00
2019-12-30 13:32:21 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets . Single ( x => x . Version == 1 ). IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets . Single ( x => x . Version == 0 ). IsFullBackup );
backupTypeMap = GetBackupTypesFromRemoteFiles ( c , out dlistFiles );
}
int [] backupTypes = backupTypeMap . OrderByDescending ( x => x . Key ). Select ( x => x . Value ). ToArray ();
Assert . AreEqual ( 2 , backupTypes . Length );
Assert . AreEqual ( BackupType . FULL_BACKUP , backupTypes [ 1 ]);
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , backupTypes [ 0 ]);
2019-12-30 15:03:43 -08:00
// Remove the dlist files.
foreach ( string dlistFile in dlistFiles )
{
File . Delete ( dlistFile );
}
// Run a repair and verify that the fileset file exists in the new dlist files.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Repair ());
2020-07-05 15:00:08 -07:00
2019-12-30 15:03:43 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets . Single ( x => x . Version == 1 ). IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets . Single ( x => x . Version == 0 ). IsFullBackup );
backupTypeMap = GetBackupTypesFromRemoteFiles ( c , out _ );
}
backupTypes = backupTypeMap . OrderByDescending ( x => x . Key ). Select ( x => x . Value ). ToArray ();
Assert . AreEqual ( 2 , backupTypes . Length );
Assert . AreEqual ( BackupType . FULL_BACKUP , backupTypes [ 1 ]);
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , backupTypes [ 0 ]);
2019-12-30 13:32:21 -08:00
}
2019-11-28 21:06:24 -08:00
[Test]
[Category("Disruption")]
public async Task KeepTimeRetention ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2019-11-28 21:06:24 -08:00
// Choose a dblock size that is small enough so that more than one volume is needed.
2024-12-18 08:27:59 +01:00
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
["dblock-size"] = "10mb" ,
2025-01-28 08:55:28 +01:00
["disable-file-scanner"] = "true" ,
["concurrency-fileprocessors"] = "1" ,
2024-12-18 08:27:59 +01:00
};
2019-11-28 23:11:33 -08:00
2024-07-26 00:08:45 -04:00
// First, run two complete backups followed by a partial backup. We will then set the keep-time
2019-11-28 23:11:33 -08:00
// option so that the threshold lies between the first and second backups.
2019-11-29 09:07:43 -08:00
DateTime firstBackupTime ;
2019-11-28 21:06:24 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 09:07:43 -08:00
firstBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
}
2019-11-28 21:06:24 -08:00
2019-11-28 23:11:33 -08:00
// Wait before the second backup so that we can more easily define the keep-time threshold
// to lie between the first and second backups.
Thread . Sleep ( 5000 );
2019-11-29 09:07:43 -08:00
DateTime secondBackupTime ;
2019-11-28 23:11:33 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2019-11-28 21:06:24 -08:00
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 09:07:43 -08:00
secondBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
}
2019-11-28 21:06:24 -08:00
2019-11-28 23:11:33 -08:00
// Run a partial backup.
2019-11-29 09:07:43 -08:00
DateTime thirdBackupTime ;
2019-11-28 23:11:33 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-29 09:07:43 -08:00
thirdBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
}
2019-11-28 21:06:24 -08:00
2019-11-28 23:11:33 -08:00
// Set the keep-time option so that the threshold lies between the first and second backups
// and run the delete operation.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2024-04-18 11:49:39 +02:00
options [ "keep-time" ] = $"{(int)((DateTime.Now - firstBackupTime).TotalSeconds - (secondBackupTime - firstBackupTime).TotalSeconds / 2)}s" ;
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Delete ());
2019-11-28 21:06:24 -08:00
2019-11-29 09:07:43 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( 2 , filesets . Count );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( secondBackupTime , filesets [ 1 ]. Time );
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( thirdBackupTime , filesets [ 0 ]. Time );
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 0 ]. IsFullBackup );
2019-11-28 23:11:33 -08:00
}
2019-11-28 21:06:24 -08:00
2024-07-26 00:08:45 -04:00
// Run another partial backup. We will then verify that a full backup is retained
2019-11-28 23:11:33 -08:00
// even when all the "recent" backups are partial.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-29 09:07:43 -08:00
DateTime fourthBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 21:06:24 -08:00
// Set the keep-time option so that the threshold lies after the most recent full backup
// and run the delete operation.
options [ "keep-time" ] = "1s" ;
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Delete ());
2019-11-28 21:06:24 -08:00
2019-11-28 23:11:33 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( 3 , filesets . Count );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( secondBackupTime , filesets [ 2 ]. Time );
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 2 ]. IsFullBackup );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( thirdBackupTime , filesets [ 1 ]. Time );
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 1 ]. IsFullBackup );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( fourthBackupTime , filesets [ 0 ]. Time );
2019-11-28 21:06:24 -08:00
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
}
2019-11-28 23:11:33 -08:00
[Test]
[Category("Disruption")]
public async Task KeepVersionsRetention ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2019-11-28 23:11:33 -08:00
// Choose a dblock size that is small enough so that more than one volume is needed.
2024-12-18 08:27:59 +01:00
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
["dblock-size"] = "10mb" ,
["disable-file-scanner"] = "true" ,
2025-01-28 08:55:28 +01:00
["concurrency-fileprocessors"] = "1" ,
2024-12-18 08:27:59 +01:00
};
2019-11-28 23:11:33 -08:00
// Run a full backup.
2019-11-29 09:07:43 -08:00
DateTime firstBackupTime ;
2019-11-28 23:11:33 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 09:07:43 -08:00
firstBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
}
// Run a partial backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-28 23:11:33 -08:00
}
// Run a partial backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-28 23:11:33 -08:00
}
// Run a full backup.
2019-11-29 09:07:43 -08:00
DateTime fourthBackupTime ;
2019-11-28 23:11:33 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 09:07:43 -08:00
fourthBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
}
// Run a partial backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
options [ "keep-versions" ] = "2" ;
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-29 09:07:43 -08:00
DateTime fifthBackupTime = c . List (). Filesets . First (). Time ;
2019-11-28 23:11:33 -08:00
2024-12-18 08:27:59 +01:00
// Since we stopped the operation, files were not deleted automatically
c . Delete ();
2019-11-28 23:11:33 -08:00
// Partial backups that are followed by a full backup can be deleted.
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 3 , filesets . Count );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( firstBackupTime , filesets [ 2 ]. Time );
2019-11-28 23:11:33 -08:00
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 2 ]. IsFullBackup );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( fourthBackupTime , filesets [ 1 ]. Time );
2019-11-28 23:11:33 -08:00
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
2019-11-29 09:07:43 -08:00
Assert . AreEqual ( fifthBackupTime , filesets [ 0 ]. Time );
2019-11-28 23:11:33 -08:00
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
2019-11-29 14:13:15 -08:00
// Run a full backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 14:13:15 -08:00
DateTime sixthBackupTime = c . List (). Filesets . First (). Time ;
// Since the last backup was full, we can now expect to have just the 2 most recent full backups.
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( fourthBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( sixthBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
2019-11-28 23:11:33 -08:00
}
2019-12-30 16:31:33 -08:00
[Test]
[Category("Disruption")]
public async Task ListWithoutLocalDb ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2019-12-30 16:31:33 -08:00
// Choose a dblock size that is small enough so that more than one volume is needed.
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
["dblock-size"] = "10mb" ,
2024-12-18 08:27:59 +01:00
["no-local-db"] = "true" ,
2025-01-28 08:55:28 +01:00
["disable-file-scanner"] = "true" ,
["concurrency-fileprocessors"] = "1" ,
2019-12-30 16:31:33 -08:00
};
// Run a full backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-12-30 16:31:33 -08:00
// Run a partial backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-12-30 16:31:33 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
}
2019-11-29 16:22:55 -08:00
[Test]
[Category("Disruption")]
public async Task RetentionPolicyRetention ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2019-11-29 16:22:55 -08:00
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
// Choose a dblock size that is small enough so that more than one volume is needed.
["dblock-size"] = "10mb" ,
// This test assumes that we can perform 3 backups within 1 minute.
["retention-policy"] = "1m:59s,U:1m" ,
2024-12-18 08:27:59 +01:00
["no-backend-verification"] = "true" ,
2025-01-28 08:55:28 +01:00
["disable-file-scanner"] = "true" ,
["concurrency-fileprocessors"] = "1"
2019-11-29 16:22:55 -08:00
};
DateTime firstBackupTime ;
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 16:22:55 -08:00
firstBackupTime = c . List (). Filesets . First (). Time ;
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 1 , filesets . Count );
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 16:22:55 -08:00
DateTime secondBackupTime = c . List (). Filesets . First (). Time ;
// Since the most recent backup is not considered in the retention logic, the only backup in the first time frame
2024-07-26 00:08:45 -04:00
// is the initial one. As a result, we should have 2 backups.
2019-11-29 16:22:55 -08:00
filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( firstBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( secondBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
// Wait so that the next backups fall in the next retention interval.
Thread . Sleep ( new TimeSpan ( 0 , 0 , 1 , 0 ));
DateTime thirdBackupTime ;
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , _ ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2019-11-29 16:22:55 -08:00
thirdBackupTime = c . List (). Filesets . First (). Time ;
2024-12-18 08:27:59 +01:00
// Since we stopped the backup, files were not deleted automatically
c . Delete ();
2019-11-29 16:22:55 -08:00
// Since the most recent backup is not considered in the retention logic, there are no backups in the first time
2024-07-26 00:08:45 -04:00
// frame. The original 2 backups have now spilled over to the U:1m specification. Since we keep the first
2019-11-29 16:22:55 -08:00
// backup in the interval, we should be left with the first backup, as well as the third partial one.
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( firstBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( thirdBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
DateTime fourthBackupTime ;
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 16:22:55 -08:00
fourthBackupTime = c . List (). Filesets . First (). Time ;
// Since the most recent backup is not considered in the retention logic, the third backup is the only backup
2024-07-26 00:08:45 -04:00
// in the first time frame. There is no further spillover, so we simply add the fourth backup to the
2019-11-29 16:22:55 -08:00
// collection of retained backups.
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 3 , filesets . Count );
Assert . AreEqual ( firstBackupTime , filesets [ 2 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 2 ]. IsFullBackup );
Assert . AreEqual ( thirdBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( fourthBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
this . ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-29 16:22:55 -08:00
DateTime fifthBackupTime = c . List (). Filesets . First (). Time ;
// Since the most recent backup is not considered in the retention logic, we now have two backups in the
2024-07-26 00:08:45 -04:00
// first time frame: the third (partial) and fourth (full). Since the first backup in each interval is
// kept, we would typically keep just the third backup. However, since we should not discard a full
// backup in favor of a partial one, we keep the fourth as well. We also still have the initial backup.
2019-11-29 16:22:55 -08:00
filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 4 , filesets . Count );
Assert . AreEqual ( firstBackupTime , filesets [ 3 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 3 ]. IsFullBackup );
Assert . AreEqual ( thirdBackupTime , filesets [ 2 ]. Time );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 2 ]. IsFullBackup );
Assert . AreEqual ( fourthBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( fifthBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
// Wait so that the next backups fall in the next retention interval.
Thread . Sleep ( new TimeSpan ( 0 , 0 , 1 , 0 ));
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
this . ModifySourceFiles ();
2024-04-18 11:49:39 +02:00
IBackupResults backupResults = c . Backup ( new [] { this . DATAFOLDER });
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
2019-11-29 16:22:55 -08:00
DateTime sixthBackupTime = c . List (). Filesets . First (). Time ;
// Since the most recent backup is not considered in the retention logic, we now have three backups in the
2024-07-26 00:08:45 -04:00
// second time frame: the third (partial), fourth (full), and fifth (full). Since we keep up to the first
2019-11-29 16:22:55 -08:00
// full backup in each time frame, we now drop the fifth backup.
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 4 , filesets . Count );
Assert . AreEqual ( firstBackupTime , filesets [ 3 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 3 ]. IsFullBackup );
Assert . AreEqual ( thirdBackupTime , filesets [ 2 ]. Time );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , filesets [ 2 ]. IsFullBackup );
Assert . AreEqual ( fourthBackupTime , filesets [ 1 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( sixthBackupTime , filesets [ 0 ]. Time );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
}
2019-09-15 10:33:35 -07:00
[Test]
[Category("Disruption")]
public async Task StopAfterCurrentFile ()
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
// Choose a dblock size that is small enough so that more than one volume is needed.
["dblock-size"] = "10mb" ,
["disable-file-scanner"] = "true" ,
2025-01-28 08:55:28 +01:00
["concurrency-fileprocessors"] = "1"
2024-12-18 08:27:59 +01:00
};
2019-09-15 12:50:31 -07:00
// Run a complete backup.
2019-09-15 10:33:35 -07:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2020-07-05 15:00:08 -07:00
2019-09-15 10:33:35 -07:00
Assert . AreEqual ( 1 , c . List (). Filesets . Count ());
2019-09-15 13:45:43 -07:00
Assert . AreEqual ( BackupType . FULL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 0 ). IsFullBackup );
2019-09-15 12:50:31 -07:00
}
2019-09-15 10:33:35 -07:00
2019-11-28 21:42:58 -08:00
// Run a partial backup.
2019-09-15 12:50:31 -07:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-06 15:22:57 +01:00
( var backupResults , var modified ) = await this . RunPartialBackup ( c ). ConfigureAwait ( false );
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 1 , backupResults . Warnings . Count ());
2025-03-06 15:22:57 +01:00
if ( backupResults . ModifiedFiles == 0 )
throw new Exception ( $"No files were modified, likely the stop was issued too early, list is: {string.Join(" , ", modified)}" );
2024-12-18 08:27:59 +01:00
Assert . That ( backupResults . ModifiedFiles , Is . GreaterThan ( 0 ), "No files were added, likely the stop was issued too early" );
Assert . That ( backupResults . ModifiedFiles , Is . LessThan ( fileSizes . Length ), "All files were added, likely the stop was issued too late" );
2019-09-15 12:50:31 -07:00
2019-09-15 10:33:35 -07:00
// If we interrupt the backup, the most recent Fileset should be marked as partial.
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
2019-09-15 13:45:43 -07:00
Assert . AreEqual ( BackupType . FULL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 1 ). IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 0 ). IsFullBackup );
2019-09-15 10:33:35 -07:00
}
2019-09-15 12:50:31 -07:00
2019-09-15 15:05:03 -07:00
// Restore files from the partial backup set.
2024-04-18 11:49:39 +02:00
Dictionary < string , string > restoreOptions = new Dictionary < string , string >( options ) { [ "restore-path" ] = this . RESTOREFOLDER };
2019-09-15 15:05:03 -07:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , restoreOptions , null ))
{
2025-03-10 14:39:31 +01:00
var lastResults = c . List ( "*" );
var partialVersionFiles = lastResults . Files . Select ( x => x . Path ). Where ( x => ! Utility . IsFolder ( x , File . GetAttributes )). ToArray ();
2019-09-15 15:30:44 -07:00
Assert . GreaterOrEqual ( partialVersionFiles . Length , 1 );
2019-09-15 15:05:03 -07:00
c . Restore ( partialVersionFiles );
foreach ( string filepath in partialVersionFiles )
{
2025-03-10 14:39:31 +01:00
var filename = Path . GetFileName ( filepath );
2020-09-10 18:41:55 -07:00
TestUtils . AssertFilesAreEqual ( filepath , Path . Combine ( this . RESTOREFOLDER , filename ?? String . Empty ), false , filename );
2019-09-15 15:05:03 -07:00
}
}
2019-09-15 13:25:22 -07:00
// Recreating the database should preserve the backup types.
File . Delete ( this . DBFILE );
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Repair ());
2020-07-05 15:00:08 -07:00
2019-09-15 13:25:22 -07:00
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
2019-09-15 13:45:43 -07:00
Assert . AreEqual ( BackupType . FULL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 1 ). IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 0 ). IsFullBackup );
2019-09-15 13:25:22 -07:00
}
2024-07-26 00:08:45 -04:00
// Run a complete backup. Listing the Filesets should include both full and partial backups.
2019-09-15 12:50:31 -07:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2019-11-18 19:34:25 -08:00
Assert . AreEqual ( 3 , c . List (). Filesets . Count ());
2020-07-05 15:00:08 -07:00
2019-11-18 19:34:25 -08:00
Assert . AreEqual ( BackupType . FULL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 2 ). IsFullBackup );
Assert . AreEqual ( BackupType . PARTIAL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 1 ). IsFullBackup );
2019-09-15 13:45:43 -07:00
Assert . AreEqual ( BackupType . FULL_BACKUP , c . List (). Filesets . Single ( x => x . Version == 0 ). IsFullBackup );
2019-09-15 12:50:31 -07:00
}
2019-09-15 15:31:02 -07:00
// Restore files from the full backup set.
restoreOptions [ "overwrite" ] = "true" ;
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , restoreOptions , null ))
{
2025-03-10 14:39:31 +01:00
var lastResults = c . List ( "*" );
var fullVersionFiles = lastResults . Files . Select ( x => x . Path ). Where ( x => ! Utility . IsFolder ( x , File . GetAttributes )). ToArray ();
2019-09-15 15:31:02 -07:00
Assert . AreEqual ( this . fileSizes . Length , fullVersionFiles . Length );
2020-07-05 15:00:08 -07:00
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Restore ( fullVersionFiles ));
2019-09-15 15:31:02 -07:00
foreach ( string filepath in fullVersionFiles )
{
string filename = Path . GetFileName ( filepath );
2020-09-10 18:41:55 -07:00
TestUtils . AssertFilesAreEqual ( filepath , Path . Combine ( this . RESTOREFOLDER , filename ?? String . Empty ), false , filename );
2019-09-15 15:31:02 -07:00
}
}
2019-09-15 10:33:35 -07:00
}
2020-01-01 09:40:47 -08:00
[Test]
[Category("Disruption")]
2020-02-29 16:33:32 -06:00
public async Task StopNow ()
2020-01-01 09:40:47 -08:00
{
2024-12-18 08:27:59 +01:00
#if ! DEBUG
Assert . Ignore ( "This test requires DEBUG to be defined" );
#endif
2020-01-01 09:40:47 -08:00
// Choose a dblock size that is small enough so that more than one volume is needed.
2024-12-18 08:27:59 +01:00
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions )
{
["dblock-size"] = "10mb" ,
["disable-synthetic-filelist"] = "true" ,
2025-01-28 08:55:28 +01:00
["disable-file-scanner"] = "true" ,
["concurrency-fileprocessors"] = "1" ,
2024-12-18 08:27:59 +01:00
};
2020-01-01 09:40:47 -08:00
// Run a complete backup.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2020-07-05 15:00:08 -07:00
2020-01-01 09:40:47 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 1 , filesets . Count );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
2024-12-18 08:27:59 +01:00
// Interrupt a backup with "abort".
2020-01-01 09:40:47 -08:00
this . ModifySourceFiles ();
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-12 10:40:20 -03:00
var startedTcs = new TaskCompletionSource < bool >();
c . OnOperationStarted += results =>
{
(( BackupResults ) results ). OperationProgressUpdater . PhaseChanged += ( phase , previousPhase ) =>
{
if ( phase == OperationPhase . Backup_ProcessingFiles )
startedTcs . SetResult ( true );
};
};
2025-04-16 08:05:38 +02:00
2025-03-12 10:40:20 -03:00
Task backupTask = Task . Run (() => c . Backup ([ DATAFOLDER ]));
2020-01-01 09:40:47 -08:00
2025-03-12 10:40:20 -03:00
Task completedTask = await Task . WhenAny ( startedTcs . Task , Task . Delay ( 5000 ));
if ( completedTask == startedTcs . Task )
2024-12-18 08:27:59 +01:00
{
2025-03-12 10:40:20 -03:00
c . Abort ();
try
{
await backupTask . ConfigureAwait ( false );
Assert . Fail ( "Expected OperationCanceledException but none was thrown." );
}
catch ( OperationCanceledException )
{
// Success, exception thrown as expected
}
2024-12-18 08:27:59 +01:00
}
2025-03-12 10:40:20 -03:00
else
2024-12-18 08:27:59 +01:00
{
2025-03-12 10:40:20 -03:00
Assert . Fail ( "Operation did not reach ready state within timeout." );
2024-12-18 08:27:59 +01:00
}
2020-01-01 09:40:47 -08:00
}
2025-04-16 08:05:38 +02:00
2020-01-01 09:40:47 -08:00
// The next backup should proceed without issues.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2020-07-05 15:00:08 -07:00
2020-01-01 09:40:47 -08:00
List < IListResultFileset > filesets = c . List (). Filesets . ToList ();
Assert . AreEqual ( 2 , filesets . Count );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 1 ]. IsFullBackup );
Assert . AreEqual ( BackupType . FULL_BACKUP , filesets [ 0 ]. IsFullBackup );
}
// Restore from the backup that followed the interruption.
2024-04-18 11:49:39 +02:00
Dictionary < string , string > restoreOptions = new Dictionary < string , string >( options ) { [ "restore-path" ] = this . RESTOREFOLDER };
2020-01-01 09:40:47 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , restoreOptions , null ))
{
2025-03-10 14:39:31 +01:00
var lastResults = c . List ( "*" );
var fullVersionFiles = lastResults . Files . Select ( x => x . Path ). Where ( x => ! Utility . IsFolder ( x , File . GetAttributes )). ToArray ();
2020-01-01 09:40:47 -08:00
Assert . AreEqual ( this . fileSizes . Length , fullVersionFiles . Length );
2020-07-05 15:00:08 -07:00
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Restore ( fullVersionFiles ));
2020-01-01 09:40:47 -08:00
foreach ( string filepath in fullVersionFiles )
{
string filename = Path . GetFileName ( filepath );
2020-09-10 18:41:55 -07:00
TestUtils . AssertFilesAreEqual ( filepath , Path . Combine ( this . RESTOREFOLDER , filename ?? String . Empty ), false , filename );
2020-01-01 09:40:47 -08:00
}
}
}
2025-02-20 11:00:28 +01:00
[Test]
[Category("Disruption")]
2025-06-02 11:50:35 +02:00
public async Task TestUploadExceptionWithNoRetries ()
2025-02-20 11:00:28 +01:00
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "0" ;
testopts [ "dblock-size" ] = "10mb" ;
// Make a base backup
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Backup ( new string [] { DATAFOLDER }));
2025-02-20 11:00:28 +01:00
2025-04-23 15:40:06 +02:00
// Ensure that the target folder only has a single dlist file
Assert . AreEqual ( 1 , Directory . EnumerateFiles ( TARGETFOLDER , "*.dlist.*" ). Count (), "There should be only one dlist file in the target folder" );
2025-02-20 11:00:28 +01:00
// Make a new backup that fails uploading a dblock file
ModifySourceFiles ();
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var hasFailed = false ;
var secondUploadStarted = false ;
var secondUploadCompleted = false ;
// Fail the compact after the first dblock put is completed
2025-04-23 15:40:06 +02:00
var uploads = new List < string >();
2025-02-20 11:00:28 +01:00
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
if ( action . IsGetOperation )
{
return true ;
}
if (! hasFailed && action == DeterministicErrorBackend . BackendAction . PutBefore )
{
// We only fail one upload, but there are no retries
hasFailed = true ;
// Make sure we can start a second upload
Thread . Sleep ( 1000 );
return true ;
}
if ( action == DeterministicErrorBackend . BackendAction . PutBefore )
secondUploadStarted = true ;
if ( action == DeterministicErrorBackend . BackendAction . PutAfter )
secondUploadCompleted = true ;
return false ;
};
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( failtarget , testopts , null ))
2025-02-20 11:00:28 +01:00
Assert . Throws < DeterministicErrorBackend . DeterministicErrorBackendException >(() => c . Backup ( new string [] { DATAFOLDER }));
Assert . That ( secondUploadStarted , Is . True , "Second upload was not started" );
Assert . That ( secondUploadCompleted , Is . True , "Second upload was not started" );
2025-04-23 15:40:06 +02:00
Assert . That ( hasFailed , Is . True , "Failed to fail the upload" );
Assert . That (! uploads . Any ( x => x . Contains ( "dlist" )), Is . True , "Upload of dlist file was not skipped" );
// Ensure that the target folder only has a single dlist file
Assert . AreEqual ( 1 , Directory . EnumerateFiles ( TARGETFOLDER , "*.dlist.*" ). Count (), $"There should be only one dlist file in the target folder: {string.Join(" , ", uploads)}" );
2025-02-20 11:00:28 +01:00
// Create a regular backup
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Backup ( new string [] { DATAFOLDER }));
// Verify that all is in order
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts . Expand ( new { full_remote_verification = true }), null ))
2025-04-16 08:05:38 +02:00
try
{
TestUtils . AssertResults ( c . Test ( long . MaxValue ));
}
catch ( TestUtils . TestVerificationException e )
{
2025-06-19 11:41:39 +02:00
using var db = await LocalDatabase . CreateLocalDatabaseAsync ( testopts [ "dbpath" ], "test" , true , null , CancellationToken . None ). ConfigureAwait ( false );
2025-04-16 08:05:38 +02:00
using var cmd = db . Connection . CreateCommand ();
var sb = new StringBuilder ();
sb . AppendLine ( e . Message );
2026-02-06 14:37:01 +01:00
sb . AppendLine ( TestUtils . DumpTable ( cmd , "Remotevolume" , null ));
2025-04-16 08:05:38 +02:00
sb . AppendLine ( TestUtils . DumpTable ( cmd , "File" , null ));
sb . AppendLine ( TestUtils . DumpTable ( cmd , "FilesetEntry" , null ));
sb . AppendLine ( TestUtils . DumpTable ( cmd , "Fileset" , null ));
sb . AppendLine ( "Files in the source folder:" );
foreach ( var fe in Directory . EnumerateFileSystemEntries ( this . DATAFOLDER ))
sb . AppendLine ( $"File: {fe}" );
Assert . Fail ( sb . ToString ());
}
2025-03-10 14:39:31 +01:00
// Test that we can recreate
var recreatedDatabaseFile = Path . Combine ( BASEFOLDER , "recreated-database.sqlite" );
if ( File . Exists ( recreatedDatabaseFile ))
File . Delete ( recreatedDatabaseFile );
testopts [ "dbpath" ] = recreatedDatabaseFile ;
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
// Check that we have 3 versions
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-02-20 11:00:28 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
Assert . AreEqual ( 3 , listResults . Filesets . Count ());
2025-02-20 11:00:28 +01:00
}
2025-03-10 14:39:31 +01:00
}
[Test]
[Category("Disruption")]
[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(true, true)]
[TestCase(false, true)]
2025-03-10 22:04:48 +01:00
public void TestUploadExceptionOnFirstDlistWithRepair ( bool before , bool failOnLastDblock )
2025-03-10 14:39:31 +01:00
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "0" ;
testopts [ "dblock-size" ] = "10mb" ;
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var operation = before
? DeterministicErrorBackend . BackendAction . PutBefore
: DeterministicErrorBackend . BackendAction . PutAfter ;
// We expect to upload 7 volumes, and fail on the last one
var count = 7 ;
var failed = false ;
// Fail when uploading the dlist file
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
if ( failOnLastDblock )
{
if ( action == operation && remotename . Contains ( ".dblock." ))
{
if ( Interlocked . Decrement ( ref count ) == 0 )
{
// Allow others to progress
Thread . Sleep ( 2000 );
failed = true ;
return true ;
}
}
}
else
{
if ( action == operation && remotename . Contains ( ".dlist." ))
{
failed = true ;
return true ;
}
}
return false ;
};
using ( var c = new Controller ( failtarget , testopts , null ))
Assert . Throws < DeterministicErrorBackend . DeterministicErrorBackendException >(() => c . Backup ( new string [] { DATAFOLDER }));
Assert . That ( failed , Is . True , "Failed to fail the upload" );
// Make spacing for the next backup
Thread . Sleep ( 3000 );
// Issue repair
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-02-20 11:00:28 +01:00
// Verify that all is in order
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts . Expand ( new { full_remote_verification = true }), null ))
TestUtils . AssertResults ( c . Test ( long . MaxValue ));
2025-04-23 11:49:06 +02:00
// If we fail on the dlist after the upload, it is promoted to a regular backup
var expectedFilesets = before || failOnLastDblock ? 2 : 1 ;
2025-03-10 14:39:31 +01:00
// Make a new backup, should continue as normal
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-02-20 11:00:28 +01:00
{
2025-03-10 14:39:31 +01:00
var backupResults = c . Backup ( new string [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
2025-04-23 11:49:06 +02:00
Assert . AreEqual ( expectedFilesets , c . List (). Filesets . Count ());
2025-02-20 11:00:28 +01:00
}
// Test that we can recreate
2025-03-04 11:48:40 +01:00
var recreatedDatabaseFile = Path . Combine ( BASEFOLDER , "recreated-database.sqlite" );
if ( File . Exists ( recreatedDatabaseFile ))
File . Delete ( recreatedDatabaseFile );
testopts [ "dbpath" ] = recreatedDatabaseFile ;
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-03-04 11:48:40 +01:00
2025-04-23 11:49:06 +02:00
// Check that we have the correct versions
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-04 11:48:40 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
2025-04-23 11:49:06 +02:00
Assert . AreEqual ( expectedFilesets , listResults . Filesets . Count ());
2025-03-04 11:48:40 +01:00
}
}
2025-03-08 15:14:03 +01:00
[Test]
[Category("Disruption")]
[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(true, true)]
[TestCase(false, true)]
2025-03-10 22:04:48 +01:00
public void TestUploadExceptionOnFirstDlist ( bool before , bool failOnLastDblock )
2025-03-08 15:14:03 +01:00
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "0" ;
testopts [ "dblock-size" ] = "10mb" ;
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var operation = before
? DeterministicErrorBackend . BackendAction . PutBefore
: DeterministicErrorBackend . BackendAction . PutAfter ;
// We expect to upload 7 volumes, and fail on the last one
var count = 7 ;
2025-03-09 22:01:01 +01:00
var failed = false ;
2025-03-08 15:14:03 +01:00
// Fail when uploading the dlist file
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
if ( failOnLastDblock )
{
if ( action == operation && remotename . Contains ( ".dblock." ))
{
if ( Interlocked . Decrement ( ref count ) == 0 )
{
// Allow others to progress
Thread . Sleep ( 2000 );
2025-03-09 22:01:01 +01:00
failed = true ;
2025-03-08 15:14:03 +01:00
return true ;
}
}
}
else
{
if ( action == operation && remotename . Contains ( ".dlist." ))
2025-03-09 22:01:01 +01:00
{
failed = true ;
2025-03-08 15:14:03 +01:00
return true ;
2025-03-09 22:01:01 +01:00
}
2025-03-08 15:14:03 +01:00
}
return false ;
};
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( failtarget , testopts , null ))
2025-03-08 15:14:03 +01:00
Assert . Throws < DeterministicErrorBackend . DeterministicErrorBackendException >(() => c . Backup ( new string [] { DATAFOLDER }));
2025-03-09 22:01:01 +01:00
Assert . That ( failed , Is . True , "Failed to fail the upload" );
2025-03-08 15:14:03 +01:00
// Make spacing for the next backup
Thread . Sleep ( 3000 );
2025-04-23 11:49:06 +02:00
// If we fail on the dlist after the upload, it is promoted to a regular backup
var expectedFilesets = before || failOnLastDblock ? 2 : 1 ;
2025-03-08 15:14:03 +01:00
// Make a new backup, should continue as normal
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-08 15:14:03 +01:00
{
IBackupResults backupResults = c . Backup ( new string [] { DATAFOLDER });
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( backupResults );
2025-04-23 11:49:06 +02:00
Assert . AreEqual ( expectedFilesets , c . List (). Filesets . Count ());
2025-03-08 15:14:03 +01:00
}
// Verify that all is in order
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts . Expand ( new { full_remote_verification = true }), null ))
TestUtils . AssertResults ( c . Test ( long . MaxValue ));
2025-03-08 15:14:03 +01:00
// Test that we can recreate
var recreatedDatabaseFile = Path . Combine ( BASEFOLDER , "recreated-database.sqlite" );
if ( File . Exists ( recreatedDatabaseFile ))
File . Delete ( recreatedDatabaseFile );
testopts [ "dbpath" ] = recreatedDatabaseFile ;
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-03-08 15:14:03 +01:00
// Check that we have 2 versions
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-08 15:14:03 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
2025-04-23 11:49:06 +02:00
Assert . AreEqual ( expectedFilesets , listResults . Filesets . Count ());
2025-03-08 15:14:03 +01:00
}
}
[Test]
[Category("Disruption")]
[TestCase(true)]
[TestCase(false)]
public void TestRepairWithNoDlist ( bool before )
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "0" ;
testopts [ "dblock-size" ] = "10mb" ;
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var operation = before
? DeterministicErrorBackend . BackendAction . PutBefore
: DeterministicErrorBackend . BackendAction . PutAfter ;
// Fail when uploading the first dlist file
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
if ( action == operation && remotename . Contains ( ".dlist." ))
return true ;
return false ;
};
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( failtarget , testopts , null ))
2025-03-08 15:14:03 +01:00
Assert . Throws < DeterministicErrorBackend . DeterministicErrorBackendException >(() => c . Backup ( new string [] { DATAFOLDER }));
// Test that we can repair
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-03-08 15:14:03 +01:00
// Check that we have 1 version
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-08 15:14:03 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
2025-03-08 15:14:03 +01:00
Assert . AreEqual ( 1 , listResults . Filesets . Count ());
}
}
[Test]
[Category("Disruption")]
[TestCase(true, true, 3, false)]
[TestCase(false, false, 3, false)]
[TestCase(true, false, 3, false)]
[TestCase(false, true, 3, false)]
[TestCase(true, true, 3, true)]
[TestCase(false, true, 3, true)]
2025-04-23 11:49:06 +02:00
[TestCase(false, false, 3, true)]
[TestCase(true, false, 3, true)]
2025-03-10 22:04:48 +01:00
public void TestMultiExceptionOnDlist ( bool before , bool modifyInBetween , int runs , bool withBase )
2025-03-08 15:14:03 +01:00
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "0" ;
testopts [ "dblock-size" ] = "10mb" ;
2025-04-23 11:49:06 +02:00
// We always have at least 1 backup at the end
var expectedFilesets = 1 ;
// If the files are modified, this will create multiple versions
if ( modifyInBetween )
{
// If we prevent the file from being uploaded,
// it will be a temporary file, and a new synthetic file
// will be attempted, but that fails as well
2025-06-02 11:50:44 +02:00
// If it is after, then the upload completes,
2025-04-23 11:49:06 +02:00
// and it will be promoted to a regular uploaded backup
expectedFilesets += before ? 1 : runs ;
// If we modify with a base, then that creates a version as well
if ( withBase )
expectedFilesets += 1 ;
}
else if ( before && ! withBase )
{
// Because an incomplete fileset is treated as a partial fileset,
// the final non-interrupted backup will see the syntheticly
// created fileset as a partial fileset, and create a new non-partial
// fileset, even though there are no changes between the two
// This does not happen with a base, because it simply does not
// attempt to upload the failing filesets
expectedFilesets += 1 ;
}
2025-03-08 15:14:03 +01:00
if ( withBase )
{
// Make a regular backup
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-10 14:39:31 +01:00
TestUtils . AssertResults ( c . Backup ( new string [] { DATAFOLDER }));
2025-03-08 15:14:03 +01:00
}
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var operation = before
? DeterministicErrorBackend . BackendAction . PutBefore
: DeterministicErrorBackend . BackendAction . PutAfter ;
// Fail when uploading the first dlist file
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
if ( action == operation && remotename . Contains ( ".dlist." ))
return true ;
return false ;
};
for ( var i = 0 ; i < runs ; i ++)
{
if ( modifyInBetween )
ModifySourceFiles ();
using ( var c = new Controller ( failtarget , testopts , null ))
2025-03-10 14:39:31 +01:00
try
{
c . Backup ( new string [] { DATAFOLDER });
}
catch ( DeterministicErrorBackend . DeterministicErrorBackendException )
{
}
2025-03-08 15:14:03 +01:00
// Prevent clashes in timestamps
Thread . Sleep ( 3000 );
}
if ( modifyInBetween )
ModifySourceFiles ();
// Make a new backup, should continue as normal
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
2025-03-10 14:39:31 +01:00
var backupResults = c . Backup ( new string [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
2025-03-08 15:14:03 +01:00
Assert . AreEqual ( expectedFilesets , c . List (). Filesets . Count ());
}
// Verify that all is in order
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts . Expand ( new { full_remote_verification = true }), null ))
TestUtils . AssertResults ( c . Test ( long . MaxValue ));
2025-03-08 15:14:03 +01:00
// Test that we can recreate
var recreatedDatabaseFile = Path . Combine ( BASEFOLDER , "recreated-database.sqlite" );
if ( File . Exists ( recreatedDatabaseFile ))
File . Delete ( recreatedDatabaseFile );
testopts [ "dbpath" ] = recreatedDatabaseFile ;
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-03-08 15:14:03 +01:00
// Check that we have 2 versions
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-03-08 15:14:03 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
2025-03-08 15:14:03 +01:00
Assert . AreEqual ( expectedFilesets , listResults . Filesets . Count ());
}
}
2025-03-04 11:48:40 +01:00
private class LogSink : IMessageSink
{
public List < string > StartedFiles { get ; } = new List < string >();
public List < string > CompletedFiles { get ; } = new List < string >();
public void BackendEvent ( BackendActionType action , BackendEventType type , string path , long size )
{
if ( action == BackendActionType . Put && type == BackendEventType . Started )
{
this . StartedFiles . Add ( path );
}
if ( action == BackendActionType . Put && type == BackendEventType . Completed )
{
this . CompletedFiles . Add ( path );
}
}
public void SetBackendProgress ( IBackendProgress progress )
{
}
public void SetOperationProgress ( IOperationProgress progress )
{
}
public void WriteMessage ( LogEntry entry )
{
}
}
[Test]
[Category("Disruption")]
2025-03-10 22:04:48 +01:00
public void TestDblockUploadWithOperationCancellation ()
2025-03-04 11:48:40 +01:00
{
2025-03-10 22:04:48 +01:00
TestUploadWithOperationCancellation ( ".dblock." );
2025-03-06 14:53:46 +01:00
}
2025-03-04 11:48:40 +01:00
2025-03-06 14:53:46 +01:00
[Test]
[Category("Disruption")]
2025-03-10 22:04:48 +01:00
public void TestDindexUploadWithOperationCancellation ()
2025-03-06 14:53:46 +01:00
{
2025-03-10 22:04:48 +01:00
TestUploadWithOperationCancellation ( ".dindex." );
2025-03-04 11:48:40 +01:00
}
[Test]
[Category("Disruption")]
2025-03-10 22:04:48 +01:00
public void TestDlistUploadWithOperationCancellation ()
2025-03-06 14:53:46 +01:00
{
2025-03-10 22:04:48 +01:00
TestUploadWithOperationCancellation ( ".dlist." );
2025-03-06 14:53:46 +01:00
}
2025-03-10 22:04:48 +01:00
private void TestUploadWithOperationCancellation ( string filefragment )
2025-03-04 11:48:40 +01:00
{
var testopts = TestOptions ;
testopts [ "number-of-retries" ] = "1" ;
testopts [ "dblock-size" ] = "10mb" ;
// Make a base backup
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Backup ( new string [] { DATAFOLDER }));
2025-03-04 11:48:40 +01:00
// Make a new backup that fails uploading a dblock file
ModifySourceFiles ();
// Deterministic error backend
Library . DynamicLoader . BackendLoader . AddBackend ( new DeterministicErrorBackend ());
var failtarget = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
var uploadCount = 0 ;
2025-03-06 14:53:46 +01:00
var failstep = DeterministicErrorBackend . BackendAction . PutBefore ;
2025-03-04 11:48:40 +01:00
2025-03-06 14:53:46 +01:00
// Fail the upload before
2025-03-04 11:48:40 +01:00
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
{
2025-03-06 14:53:46 +01:00
if ( action == failstep && remotename . Contains ( filefragment ))
2025-03-04 11:48:40 +01:00
{
// Fail once with a cancellation
if ( Interlocked . Increment ( ref uploadCount ) == 1 )
throw new OperationCanceledException ();
}
return false ;
};
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( failtarget , testopts , null ))
2025-03-04 11:48:40 +01:00
{
var sink = new LogSink ();
c . AppendSink ( sink );
var res = c . Backup ( new string [] { DATAFOLDER });
if ( uploadCount == 0 )
Assert . Fail ( "Upload count was not incremented" );
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
2025-03-06 14:53:46 +01:00
Assert . AreEqual ( 2 , Directory . GetFiles ( TARGETFOLDER , "*.dlist.*" , SearchOption . TopDirectoryOnly ). Count ());
2025-03-04 11:48:40 +01:00
Assert . AreEqual ( 7 * 2 + 1 , sink . CompletedFiles . Count ); // 7 completed (dblock + dindex) + 1 dlist
Assert . AreEqual ( 7 * 2 + 2 , sink . StartedFiles . Count ); // 1 retry
Assert . AreEqual ( sink . CompletedFiles . Count ( x => x . Contains ( ".dblock." )), sink . CompletedFiles . Count ( x => x . Contains ( ".dindex." )));
Assert . AreEqual ( 1 , res . BackendStatistics . RetryAttempts );
}
2025-03-06 14:53:46 +01:00
uploadCount = 0 ;
failstep = DeterministicErrorBackend . BackendAction . PutAfter ;
2025-03-04 11:48:40 +01:00
ModifySourceFiles ();
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( failtarget , testopts , null ))
2025-03-04 11:48:40 +01:00
{
2025-03-06 14:53:46 +01:00
var sink = new LogSink ();
c . AppendSink ( sink );
2025-03-04 11:48:40 +01:00
var res = c . Backup ( new string [] { DATAFOLDER });
if ( uploadCount == 0 )
Assert . Fail ( "Upload count was not incremented" );
2025-03-06 14:53:46 +01:00
Assert . AreEqual ( 3 , c . List (). Filesets . Count ());
Assert . AreEqual ( 3 , Directory . GetFiles ( TARGETFOLDER , "*.dlist.*" , SearchOption . TopDirectoryOnly ). Count ());
Assert . AreEqual ( 7 * 2 + 1 , sink . CompletedFiles . Count ); // 7 completed (dblock + dindex) + 1 dlist
Assert . AreEqual ( 7 * 2 + 2 , sink . StartedFiles . Count ); // 1 retry
Assert . AreEqual ( sink . CompletedFiles . Count ( x => x . Contains ( ".dblock." )), sink . CompletedFiles . Count ( x => x . Contains ( ".dindex." )));
2025-03-04 11:48:40 +01:00
Assert . AreEqual ( 1 , res . BackendStatistics . RetryAttempts );
}
ModifySourceFiles ();
// Create a regular backup
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Backup ( new string [] { DATAFOLDER }));
2025-03-04 11:48:40 +01:00
// Verify that all is in order
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts . Expand ( new { full_remote_verification = true }), null ))
TestUtils . AssertResults ( c . Test ( long . MaxValue ));
2025-03-04 11:48:40 +01:00
// Test that we can recreate
2025-02-20 11:00:28 +01:00
var recreatedDatabaseFile = Path . Combine ( BASEFOLDER , "recreated-database.sqlite" );
if ( File . Exists ( recreatedDatabaseFile ))
File . Delete ( recreatedDatabaseFile );
testopts [ "dbpath" ] = recreatedDatabaseFile ;
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
TestUtils . AssertResults ( c . Repair ());
2025-02-20 11:00:28 +01:00
// Check that we have 3 versions
2025-03-10 14:39:31 +01:00
using ( var c = new Controller ( "file://" + TARGETFOLDER , testopts , null ))
2025-02-20 11:00:28 +01:00
{
2025-03-10 14:39:31 +01:00
var listResults = c . List ();
TestUtils . AssertResults ( listResults );
2025-03-06 14:53:46 +01:00
Assert . AreEqual ( 4 , listResults . Filesets . Count ());
2025-02-20 11:00:28 +01:00
}
}
2019-09-15 10:33:35 -07:00
}
}