Files
duplicati/Duplicati/UnitTest/DisruptionTests.cs
T

1343 lines
61 KiB
C#
Raw Normal View History

// 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;
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;
using Duplicati.Library.Main.Volumes;
2019-09-15 10:33:35 -07:00
using NUnit.Framework;
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
private async Task<(IBackupResults Result, List<string> ModifiedFiles)> RunPartialBackup(Controller controller)
{
this.ModifySourceFiles();
2024-12-18 08:27:59 +01:00
var stopped = new TaskCompletionSource<bool>();
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) =>
{
modifiedFiles.Add(path);
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");
return (await backupTask.ConfigureAwait(false), modifiedFiles);
}
[SetUp]
public void SetUp()
2019-09-15 10:33:35 -07:00
{
this.ModifySourceFiles();
}
2019-09-15 12:50:31 -07: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
// 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",
};
// 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 });
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(0, backupResults.Warnings.Count());
}
2024-04-18 11:49:39 +02:00
// Inject some spacing to allow for the purged fileset
Thread.Sleep(2000);
// Run a partial backup.
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
}
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]}*")));
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]);
// 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());
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-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
// 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.
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 }));
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);
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 }));
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.
DateTime thirdBackupTime;
2019-11-28 23:11:33 -08:00
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
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
List<IListResultFileset> filesets = c.List().Filesets.ToList();
2019-11-28 21:06:24 -08:00
Assert.AreEqual(2, filesets.Count);
Assert.AreEqual(secondBackupTime, filesets[1].Time);
2019-11-28 21:06:24 -08:00
Assert.AreEqual(BackupType.FULL_BACKUP, filesets[1].IsFullBackup);
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
// 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))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
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);
Assert.AreEqual(secondBackupTime, filesets[2].Time);
2019-11-28 21:06:24 -08:00
Assert.AreEqual(BackupType.FULL_BACKUP, filesets[2].IsFullBackup);
Assert.AreEqual(thirdBackupTime, filesets[1].Time);
2019-11-28 21:06:24 -08:00
Assert.AreEqual(BackupType.PARTIAL_BACKUP, filesets[1].IsFullBackup);
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.
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 }));
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))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
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))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
2019-11-28 23:11:33 -08:00
}
// Run a full backup.
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 }));
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";
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
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);
Assert.AreEqual(firstBackupTime, filesets[2].Time);
2019-11-28 23:11:33 -08:00
Assert.AreEqual(BackupType.FULL_BACKUP, filesets[2].IsFullBackup);
Assert.AreEqual(fourthBackupTime, filesets[1].Time);
2019-11-28 23:11:33 -08:00
Assert.AreEqual(BackupType.FULL_BACKUP, filesets[1].IsFullBackup);
Assert.AreEqual(fifthBackupTime, filesets[0].Time);
2019-11-28 23:11:33 -08:00
Assert.AreEqual(BackupType.PARTIAL_BACKUP, filesets[0].IsFullBackup);
}
// 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 }));
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
}
[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
// 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",
};
// 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 }));
// Run a partial backup.
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
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
// 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))
{
(var backupResults, _) = await this.RunPartialBackup(c).ConfigureAwait(false);
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
// 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
// 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
// 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 });
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
// 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 }));
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
// Run a partial backup.
2019-09-15 12:50:31 -07:00
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
{
(var backupResults, var modified) = await this.RunPartialBackup(c).ConfigureAwait(false);
Assert.AreEqual(0, backupResults.Errors.Count());
Assert.AreEqual(1, backupResults.Warnings.Count());
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();
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
}
}
// 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());
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);
}
// 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 }));
Assert.AreEqual(3, c.List().Filesets.Count());
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);
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
}
[Test]
[Category("Disruption")]
2020-02-29 16:33:32 -06:00
public async Task StopNow()
{
2024-12-18 08:27:59 +01:00
#if !DEBUG
Assert.Ignore("This test requires DEBUG to be defined");
#endif
// 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
};
// 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 }));
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".
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]));
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
}
}
2025-04-16 08:05:38 +02: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 }));
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 };
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();
Assert.AreEqual(this.fileSizes.Length, fullVersionFiles.Length);
2025-03-10 14:39:31 +01:00
TestUtils.AssertResults(c.Restore(fullVersionFiles));
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);
}
}
}
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
// 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
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");
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)
{
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);
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)]
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
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-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-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());
}
}
[Test]
[Category("Disruption")]
[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(true, true)]
[TestCase(false, true)]
public void TestUploadExceptionOnFirstDlist(bool before, bool failOnLastDblock)
{
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;
};
2025-03-10 14:39:31 +01:00
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);
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;
// 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))
{
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());
}
// 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));
// 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());
// 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-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());
}
}
[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))
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());
// 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-10 14:39:31 +01:00
var listResults = c.List();
TestUtils.AssertResults(listResults);
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)]
public void TestMultiExceptionOnDlist(bool before, bool modifyInBetween, int runs, bool withBase)
{
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;
}
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 }));
}
// 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)
{
}
// 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);
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));
// 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());
// 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-10 14:39:31 +01:00
var listResults = c.List();
TestUtils.AssertResults(listResults);
Assert.AreEqual(expectedFilesets, listResults.Filesets.Count());
}
}
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")]
public void TestDblockUploadWithOperationCancellation()
{
TestUploadWithOperationCancellation(".dblock.");
}
[Test]
[Category("Disruption")]
public void TestDindexUploadWithOperationCancellation()
{
TestUploadWithOperationCancellation(".dindex.");
}
[Test]
[Category("Disruption")]
public void TestDlistUploadWithOperationCancellation()
{
TestUploadWithOperationCancellation(".dlist.");
}
private void TestUploadWithOperationCancellation(string filefragment)
{
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 }));
// 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;
var failstep = DeterministicErrorBackend.BackendAction.PutBefore;
// Fail the upload before
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
if (action == failstep && remotename.Contains(filefragment))
{
// 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))
{
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());
Assert.AreEqual(2, 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.")));
Assert.AreEqual(1, res.BackendStatistics.RetryAttempts);
}
uploadCount = 0;
failstep = DeterministicErrorBackend.BackendAction.PutAfter;
ModifySourceFiles();
2025-03-10 14:39:31 +01:00
using (var c = new Controller(failtarget, testopts, null))
{
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(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.")));
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 }));
// 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));
// 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);
Assert.AreEqual(4, listResults.Filesets.Count());
2025-02-20 11:00:28 +01:00
}
}
2019-09-15 10:33:35 -07:00
}
}