mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-27 19:10:29 +08:00
647 lines
No EOL
30 KiB
C#
647 lines
No EOL
30 KiB
C#
// Copyright (C) 2025, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using Duplicati.Library.Common.IO;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Main;
|
|
using NUnit.Framework;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Assert = NUnit.Framework.Legacy.ClassicAssert;
|
|
|
|
#nullable enable
|
|
|
|
namespace Duplicati.UnitTest
|
|
{
|
|
|
|
/// <summary>
|
|
/// Tests the tools.
|
|
/// </summary>
|
|
public class ToolTests : BasicSetupHelper
|
|
{
|
|
|
|
/// <summary>
|
|
/// Tests that the remote synchronization tool doesn't do anything when the dry run option is used.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestDryRun()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "l1");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
GenerateTestData(l1, 5, 0, 0, 1024).Wait();
|
|
|
|
var args = new string[] { $"file://{l1}", $"file://{l2}", "--confirm", "--dry-run" };
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(0, return_code, "Remote synchronization tool did not return 0.");
|
|
Assert.IsFalse(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are equal");
|
|
Assert.IsTrue(!Directory.EnumerateFiles(l2).Any(), "Destination directory is not empty");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the remote synchronization tool works with an empty source to an empty destination.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestEmptySourceAndDestination()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
var args = new string[] { $"file://{l1}", $"file://{l2}", "--confirm" };
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(0, return_code, "Remote synchronization tool did not return 0.");
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are not equal");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that remote synchronizing an empty source to a non-empty destination deletes the destination files.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestEmptySourceDeletesDestination()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
GenerateTestData(l2, 5, 0, 0, 1024).Wait();
|
|
|
|
var args = new string[] { $"file://{l1}", $"file://{l2}", "--confirm" };
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(0, return_code, "Remote synchronization tool did not return 0.");
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are not equal");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that remote synchronizing an empty source to a non-empty destination renames the destination files when the `--retention` option is used.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestEmptySourceRenamesDestination()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "empty_src");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
GenerateTestData(l2, 5, 0, 0, 1024).Wait();
|
|
|
|
var filelist = Directory.EnumerateFiles(l2).ToList();
|
|
var files = filelist.Select(x => File.ReadAllBytes(x)).ToList();
|
|
|
|
var args = new string[] { $"file://{l1}", $"file://{l2}", "--confirm", "--retention" };
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(0, return_code, "Remote synchronization tool did not return 0.");
|
|
|
|
var newfilelist = Directory.EnumerateFiles(l2).ToList();
|
|
foreach (var (name, contents) in filelist.Zip(files))
|
|
{
|
|
var filename = Path.GetFileName(name);
|
|
var newfilename = newfilelist.FirstOrDefault(x => x.EndsWith(filename));
|
|
if (newfilename == null)
|
|
{
|
|
Assert.Fail($"File {filename} was not renamed in the destination directory.");
|
|
}
|
|
else
|
|
{
|
|
var newcontents = File.ReadAllBytes(newfilename);
|
|
Assert.AreEqual(contents, newcontents, "File contents are not equal");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests passing all arguments to the main method of the remote synchronization tool.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestMainMethodParsesArgumentsCorrectly()
|
|
{
|
|
string[][] testCases =
|
|
{
|
|
["source", "destination", "--parse-arguments-only"],
|
|
["source", "destination", "--parse-arguments-only", "--auto-create-folders"],
|
|
["source", "destination", "--parse-arguments-only", "--backend-retries", "5"],
|
|
["source", "destination", "--parse-arguments-only", "--backend-retry-delay", "1000"],
|
|
["source", "destination", "--parse-arguments-only", "--backend-retry-with-exponential-backoff"],
|
|
["source", "destination", "--parse-arguments-only", "--dry-run"],
|
|
["source", "destination", "--parse-arguments-only", "--force"],
|
|
["source", "destination", "--parse-arguments-only", "--dry-run", "--force"],
|
|
["source", "destination", "--parse-arguments-only", "--verify-contents"],
|
|
["source", "destination", "--parse-arguments-only", "--verify-get-after-put"],
|
|
["source", "destination", "--parse-arguments-only", "--retry", "3"],
|
|
["source", "destination", "--parse-arguments-only", "--log-level", "Debug"],
|
|
["source", "destination", "--parse-arguments-only", "--log-level", "Information"],
|
|
["source", "destination", "--parse-arguments-only", "--log-level", "Profiling"],
|
|
["source", "destination", "--parse-arguments-only", "--log-level", "Verbose"],
|
|
["source", "destination", "--parse-arguments-only", "--log-file", "somefile.log"],
|
|
["source", "destination", "--parse-arguments-only", "--progress"],
|
|
["source", "destination", "--parse-arguments-only", "--retention"],
|
|
["source", "destination", "--parse-arguments-only", "--confirm"],
|
|
["source", "destination", "--parse-arguments-only", "--global-options", "someglobalkey=someglobalvalue", "anotherglobalkey=anotherglobalvalue"],
|
|
["source", "destination", "--parse-arguments-only", "--src-options", "somesrckey=somesrcvalue", "anothersrckey=anothersrcvalue"],
|
|
["source", "destination", "--parse-arguments-only", "--dst-options", "somedstkey=somedstvalue", "anotherdstkey=anotherdstvalue"],
|
|
[
|
|
"source", "destination", "--parse-arguments-only",
|
|
"--dry-run", "--force", "--verify-contents", "--retry", "3", "--log-level", "Debug", "--log-file", "somefile.log", "--progress", "--retention", "--confirm",
|
|
"--global-options", "someglobalkey=someglobalvalue", "anotherglobalkey=anotherglobalvalue",
|
|
"--src-options", "somesrckey=somesrcvalue", "anothersrckey=anothersrcvalue",
|
|
"--dst-options", "somedstkey=somedstvalue", "anotherdstkey=anotherdstvalue"
|
|
],
|
|
[
|
|
"source", "destination", "--parse-arguments-only",
|
|
"--global-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue",
|
|
"--src-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue",
|
|
"--dst-options", "somekey=somevalue=with=extra=equals", "anotherkey=anothervalue"
|
|
],
|
|
[
|
|
"source", "destination", "--parse-arguments-only",
|
|
"--global-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue",
|
|
"--src-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue",
|
|
"--dst-options", "somekey=\"some value with spaces\"", "anotherkey=anothervalue"
|
|
]
|
|
};
|
|
|
|
foreach (var args in testCases)
|
|
{
|
|
int result = RemoteSynchronization.Program.Main(args).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
Assert.AreEqual(0, result, $"Failed for args: {string.Join(" ", args)}");
|
|
}
|
|
|
|
int failed_result = RemoteSynchronization.Program.Main(["source", "destination", "--bogus-option"]).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
Assert.AreEqual(1, failed_result, "Invalid option did not return 1");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the original inded use of the remote synchronization tool on an empty destination.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestRemoteSynchronization()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "l1");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
var l1r = Path.Combine(RESTOREFOLDER, "l1_restore");
|
|
var l2r = Path.Combine(RESTOREFOLDER, "l2_restore");
|
|
|
|
var options = TestOptions;
|
|
|
|
var now = DateTime.Now;
|
|
GenerateTestData(DATAFOLDER, 5, 2, 2, 1024).Wait();
|
|
Console.WriteLine($"Generated test data in {DATAFOLDER} in {DateTime.Now - now}");
|
|
|
|
// Create the directories if they do not exist
|
|
foreach (var p in new string[] { l1, l2, l1r, l2r })
|
|
{
|
|
if (!SystemIO.IO_OS.DirectoryExists(p))
|
|
SystemIO.IO_OS.DirectoryCreate(p);
|
|
}
|
|
|
|
// Backup the first level
|
|
using (var c = new Controller($"file://{l1}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Backup([DATAFOLDER]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Backed up {results.AddedFiles} files to {l1} in {DateTime.Now - now}");
|
|
}
|
|
|
|
// Call the tool
|
|
now = DateTime.Now;
|
|
var exe = RemoteSynchronization.Program.Main;
|
|
string[] args = [
|
|
$"file://{l1}", $"file://{l2}",
|
|
"--global-options", ..options.Select(x => $"{x.Key}={x.Value}"),
|
|
// Pass along multi token options to test that the parser won't fail
|
|
"--src-options", "ssh-accept-any-fingerprints=true", "ssh-keyfile=/path/to/keyfile",
|
|
"--dst-options", "some-other-key=value", "another-key=value2",
|
|
"--confirm"
|
|
];
|
|
var async_call = exe(args);
|
|
async_call.Wait();
|
|
Assert.AreEqual(0, async_call.Result, "Remote synchronization tool did not return 0.");
|
|
Console.WriteLine($"Remote synchronization tool returned 0 in {DateTime.Now - now}");
|
|
|
|
// Verify that the directories are equal
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are not equal");
|
|
|
|
// Try to restore the first level
|
|
options["restore-path"] = l1r;
|
|
using (var c = new Controller($"file://{l1}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l1r), "Restored first level files is not equal to original files");
|
|
|
|
// Try to restore the second level
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
|
|
|
|
// Delete the l2r directory
|
|
SystemIO.IO_OS.DirectoryDelete(l2r, true);
|
|
|
|
// Delete one file from the l2 backup
|
|
var files = Directory.EnumerateFiles(l2).ToList();
|
|
File.Delete(files.First());
|
|
|
|
// Check that the restore fails
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
try
|
|
{
|
|
var results = c.Restore([]);
|
|
}
|
|
catch (RemoteListVerificationException)
|
|
{
|
|
Console.WriteLine($"Failed (as expected) to restore files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
}
|
|
|
|
// Run the tool again to copy the missing file
|
|
now = DateTime.Now;
|
|
async_call = exe(args);
|
|
async_call.Wait();
|
|
Assert.AreEqual(0, async_call.Result, "Remote synchronization tool did not return 0.");
|
|
Console.WriteLine($"Remote synchronization tool returned 0 in {DateTime.Now - now}");
|
|
|
|
// Try to restore the second level again
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
|
|
|
|
// Add some more files to the source
|
|
GenerateTestData(Path.Combine(DATAFOLDER, "brand_new_files"), 5, 2, 2, 1024).Wait();
|
|
|
|
// Backup the new files to l1
|
|
using (var c = new Controller($"file://{l1}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Backup([DATAFOLDER]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Backed up {results.AddedFiles} files to {l1} in {DateTime.Now - now}");
|
|
}
|
|
|
|
// Run the tool again to copy the new files
|
|
now = DateTime.Now;
|
|
async_call = exe(args);
|
|
async_call.Wait();
|
|
Assert.AreEqual(0, async_call.Result, "Remote synchronization tool did not return 0.");
|
|
Console.WriteLine($"Remote synchronization tool returned 0 in {DateTime.Now - now}");
|
|
|
|
// Try to restore the second level again
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
|
|
|
|
// Delete the l2r directory
|
|
SystemIO.IO_OS.DirectoryDelete(l2r, true);
|
|
|
|
// Remove a directory from the source
|
|
SystemIO.IO_OS.DirectoryDelete(Path.Combine(DATAFOLDER, "dir_0"), true);
|
|
|
|
// Backup the new files to l1
|
|
using (var c = new Controller($"file://{l1}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Backup([DATAFOLDER]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Backed up {results.AddedFiles} files to {l1} in {DateTime.Now - now}");
|
|
}
|
|
|
|
// Compact the backup
|
|
using (var c = new Controller($"file://{l1}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Compact();
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Compacted backup in {DateTime.Now - now}");
|
|
}
|
|
|
|
// Run the tool again to copy the new files
|
|
now = DateTime.Now;
|
|
async_call = exe(args);
|
|
async_call.Wait();
|
|
Assert.AreEqual(0, async_call.Result, "Remote synchronization tool did not return 0.");
|
|
|
|
// Try to restore the second level again
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
|
|
|
|
// Delete the l2r directory
|
|
SystemIO.IO_OS.DirectoryDelete(l2r, true);
|
|
|
|
// Perform a forced synchronization with retention to check that retention doesn't break a restore
|
|
now = DateTime.Now;
|
|
async_call = exe([.. args, "--force", "--retention"]);
|
|
async_call.Wait();
|
|
Assert.AreEqual(0, async_call.Result, "Remote synchronization tool did not return 0.");
|
|
|
|
// Try to restore the second level again
|
|
options["restore-path"] = l2r;
|
|
using (var c = new Controller($"file://{l2}", options, null))
|
|
{
|
|
now = DateTime.Now;
|
|
var results = c.Restore([]);
|
|
Assert.AreEqual(0, results.Errors.Count());
|
|
Assert.AreEqual(0, results.Warnings.Count());
|
|
Console.WriteLine($"Restored {results.RestoredFiles} files to {options["restore-path"]} in {DateTime.Now - now}");
|
|
}
|
|
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(DATAFOLDER, l2r), "Restored second level files is not equal to original files");
|
|
}
|
|
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
[TestCase(true, false, "0")] // Fail first transfer on source.
|
|
[TestCase(true, false, "1,3")] // Fail middle transfers on source.
|
|
[TestCase(true, false, "4")] // Fail last transfer on source.
|
|
[TestCase(true, false, "0,1,2,3,4")] // Fail all transfers on source.
|
|
[TestCase(false, true, "0")] // Fail first transfer on destination.
|
|
[TestCase(false, true, "1,3")] // Fail middle transfers on destination.
|
|
[TestCase(false, true, "4")] // Fail last transfer on destination.
|
|
[TestCase(false, true, "0,1,2,3,4")] // Fail all transfers on destination.
|
|
public void TestRemoteSynchronizationWithFaultyBackend(bool failSource, bool failDest, string failIndices)
|
|
{
|
|
var expect_to_fail = failIndices == "0,1,2,3,4";
|
|
|
|
var l1 = Path.Combine(TARGETFOLDER, "l1");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
int backendCount = 0;
|
|
var failIdxs = failIndices.Split(',').Select(int.Parse).ToList();
|
|
Library.DynamicLoader.BackendLoader.AddBackend(new DeterministicErrorBackend());
|
|
DeterministicErrorBackend.ErrorGenerator = (action, remotename) =>
|
|
{
|
|
if (action == DeterministicErrorBackend.BackendAction.GetBefore || action == DeterministicErrorBackend.BackendAction.PutBefore)
|
|
{
|
|
var currentIdx = backendCount;
|
|
if (failIdxs.Count > 0 && currentIdx == failIdxs.First())
|
|
{
|
|
failIdxs.RemoveAt(0);
|
|
return true; // Simulate failure
|
|
}
|
|
else
|
|
{
|
|
backendCount++;
|
|
}
|
|
}
|
|
|
|
return false; // No failure
|
|
};
|
|
|
|
GenerateTestData(l1, 5, 0, 0, 1024).Wait();
|
|
|
|
// Setup backend URLs with failure injection
|
|
var protocol = new DeterministicErrorBackend().ProtocolKey;
|
|
string srcUrl = failSource ? $"{protocol}://{l1}" : $"file://{l1}";
|
|
string dstUrl = failDest ? $"{protocol}://{l2}" : $"file://{l2}";
|
|
|
|
var args = new string[] {
|
|
srcUrl, dstUrl,
|
|
"--confirm",
|
|
"--backend-retries", expect_to_fail ? "0" : "5",
|
|
"--backend-retry-delay", "10",
|
|
"--retry", "0" // This test only tests the LightWeightBackendManager's retry logic.
|
|
};
|
|
|
|
// Redirect standard error to a buffer if we expect to fail. If it doesn't fail, we can omit it.
|
|
var originalError = Console.Error;
|
|
StringWriter? errorBuffer = null;
|
|
if (expect_to_fail)
|
|
{
|
|
errorBuffer = new StringWriter();
|
|
Console.SetError(errorBuffer);
|
|
}
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
// Expect nonzero return code if any backend fails less than the number of retries
|
|
if (expect_to_fail)
|
|
{
|
|
try
|
|
{
|
|
Assert.AreNotEqual(0, return_code, "Expected failure due to all transfers failing.");
|
|
Assert.IsFalse(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories should not be equal due to failures.");
|
|
}
|
|
catch
|
|
{
|
|
Console.SetError(originalError); // Restore standard error
|
|
Console.Error.WriteLine(errorBuffer?.ToString());
|
|
throw;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual(0, return_code, "Expected success.");
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are not equal.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that the remote synchronization tool verifies the contents of the files.
|
|
/// </summary>
|
|
[Test]
|
|
[Category("Tools/RemoteSynchronization")]
|
|
public void TestVerifies()
|
|
{
|
|
var l1 = Path.Combine(TARGETFOLDER, "l1");
|
|
var l2 = Path.Combine(TARGETFOLDER, "l2");
|
|
|
|
Directory.CreateDirectory(l1);
|
|
Directory.CreateDirectory(l2);
|
|
|
|
GenerateTestData(l1, 5, 0, 0, 1024).Wait();
|
|
|
|
var filenames = Directory.EnumerateFiles(l1).Take(2).ToList();
|
|
var first_file = filenames.First();
|
|
var second_file = filenames.Skip(1).First();
|
|
|
|
File.Copy(first_file, Path.Combine(l2, Path.GetFileName(first_file)));
|
|
File.Copy(second_file, Path.Combine(l2, Path.GetFileName(second_file)));
|
|
|
|
// Touch the first file to give it a different timestamp
|
|
File.SetLastWriteTime(first_file, DateTime.Now.AddMinutes(1));
|
|
|
|
// Modify the second file to give it different contents, but the same size
|
|
var second_file_contents = File.ReadAllBytes(second_file);
|
|
second_file_contents[0] = (byte)(second_file_contents[0] + 1);
|
|
File.WriteAllBytes(second_file, second_file_contents);
|
|
|
|
var args = new string[] { $"file://{l1}", $"file://{l2}", "--confirm", "--verify-contents", "--verify-get-after-put" };
|
|
|
|
var async_call = RemoteSynchronization.Program.Main(args);
|
|
var return_code = async_call.ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
Assert.AreEqual(0, return_code, "Remote synchronization tool did not return 0.");
|
|
Assert.IsTrue(DirectoriesAndContentsAreEqual(l1, l2), "Synchronized directories are not equal");
|
|
}
|
|
|
|
//
|
|
// Helper methods
|
|
//
|
|
|
|
/// <summary>
|
|
/// Compares two directories and their contents.
|
|
/// </summary>
|
|
/// <param name="d1">The first directory.</param>
|
|
/// <param name="d2">The second directory.</param>
|
|
/// <returns>`true` if the two directories contain exactly the same files and if the contents of the files are equivalent. `false` otherwise.</returns>
|
|
public static bool DirectoriesAndContentsAreEqual(string d1, string d2)
|
|
{
|
|
// Recursively get the files in the two directories
|
|
var f1s = Directory.EnumerateFiles(d1, "*", SearchOption.AllDirectories).Select(x => x[(d1.Length + 1)..]).OrderDescending();
|
|
var f2s = Directory.EnumerateFiles(d2, "*", SearchOption.AllDirectories).Select(x => x[(d2.Length + 1)..]).OrderDescending();
|
|
|
|
// If the two directories do not contain the same files, return false
|
|
if (!f1s.SequenceEqual(f2s))
|
|
return false;
|
|
|
|
// Check that each file pair exist and have the same content
|
|
foreach (var f1 in f1s)
|
|
{
|
|
var f1full = Path.Combine(d1, f1);
|
|
if (!File.Exists(f1full))
|
|
return false;
|
|
|
|
var f2full = Path.Combine(d2, f1);
|
|
if (!File.Exists(f2full))
|
|
return false;
|
|
|
|
var c1 = File.ReadAllText(f1full);
|
|
var c2 = File.ReadAllText(f2full);
|
|
|
|
if (!c1.SequenceEqual(c2))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates test data in the specified directory.
|
|
/// </summary>
|
|
/// <param name="dir">The directory to fill with the generated data.</param>
|
|
/// <param name="n_files">How many files the directory should have.</param>
|
|
/// <param name="n_dirs">How many subdirectories the directory should have.</param>
|
|
/// <param name="n_levels">How deep the number of subdirectories within subdirectories should go.</param>
|
|
/// <param name="max_file_size">The maximum size of the files to generate.</param>
|
|
public static async Task GenerateTestData(string dir, int n_files, int n_dirs, int n_levels, int max_file_size)
|
|
{
|
|
if (!SystemIO.IO_OS.DirectoryExists(dir))
|
|
SystemIO.IO_OS.DirectoryCreate(dir);
|
|
|
|
var fs = Enumerable.Range(0, n_files)
|
|
.Select(i => GenerateTestFile(dir, i, max_file_size));
|
|
var ds = n_levels > 0 ?
|
|
Enumerable.Range(0, n_dirs)
|
|
.Select(i =>
|
|
{
|
|
var subdir = Path.Combine(dir, $"dir_{i}");
|
|
return GenerateTestData(subdir, n_files, n_dirs, n_levels - 1, max_file_size);
|
|
})
|
|
: [];
|
|
|
|
await Task.WhenAll([.. fs, .. ds]);
|
|
}
|
|
|
|
public static async Task GenerateTestFile(string dir, int i, int max_file_size)
|
|
{
|
|
var rnd = new Random();
|
|
var file = Path.Combine(dir, $"file_{i}.txt");
|
|
var size = rnd.Next(1, max_file_size);
|
|
var data = new byte[size];
|
|
rnd.NextBytes(data);
|
|
await File.WriteAllBytesAsync(file, data);
|
|
}
|
|
|
|
}
|
|
} |