mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-28 03:20:25 +08:00
397 lines
19 KiB
C#
397 lines
19 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 System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Main;
|
|
using NUnit.Framework;
|
|
using Assert = NUnit.Framework.Legacy.ClassicAssert;
|
|
|
|
namespace Duplicati.UnitTest
|
|
{
|
|
public class RunScriptTests : BasicSetupHelper
|
|
{
|
|
[Test]
|
|
[Category("Border")]
|
|
[TestCase(0)]
|
|
[TestCase(1)]
|
|
[TestCase(2)]
|
|
[TestCase(3)]
|
|
[TestCase(4)]
|
|
[TestCase(5)]
|
|
[TestCase(6)]
|
|
public void RunScriptAfter(int exitCode)
|
|
{
|
|
const string expectedMessage = "Hello";
|
|
string expectedFile = Path.Combine(this.RESTOREFOLDER, "hello.txt");
|
|
List<string> customCommands = new List<string>
|
|
{
|
|
$"echo {expectedMessage}>\"{expectedFile}\""
|
|
};
|
|
|
|
Dictionary<string, string> options = this.TestOptions;
|
|
options["run-script-after"] = CreateScript(exitCode, null, null, 0, customCommands);
|
|
using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null))
|
|
{
|
|
IBackupResults backupResults = c.Backup(new[] { this.DATAFOLDER });
|
|
|
|
switch (exitCode)
|
|
{
|
|
case 0:
|
|
case 1:
|
|
Assert.AreEqual(0, backupResults.Errors.Count());
|
|
Assert.AreEqual(0, backupResults.Warnings.Count());
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
Assert.AreEqual(0, backupResults.Errors.Count());
|
|
Assert.AreEqual(1, backupResults.Warnings.Count());
|
|
break;
|
|
default:
|
|
Assert.AreEqual(1, backupResults.Errors.Count());
|
|
Assert.AreEqual(0, backupResults.Warnings.Count());
|
|
break;
|
|
}
|
|
// run script after does not set interrupted flag
|
|
Assert.IsFalse(backupResults.Interrupted);
|
|
|
|
string[] targetEntries = Directory.EnumerateFileSystemEntries(this.RESTOREFOLDER).ToArray();
|
|
Assert.AreEqual(1, targetEntries.Length);
|
|
Assert.AreEqual(expectedFile, targetEntries[0]);
|
|
|
|
string[] lines = File.ReadAllLines(expectedFile);
|
|
Assert.AreEqual(1, lines.Length);
|
|
Assert.AreEqual(expectedMessage, lines[0]);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
[Category("Border")]
|
|
public void RunScriptBefore()
|
|
{
|
|
var blocksize = 10 * 1024;
|
|
var options = TestOptions;
|
|
options["blocksize"] = blocksize.ToString() + "b";
|
|
options["run-script-timeout"] = "5s";
|
|
|
|
// We need a small delay as we run very small backups back-to-back
|
|
var PAUSE_TIME = TimeSpan.FromSeconds(3);
|
|
|
|
BorderTests.WriteTestFilesToFolder(DATAFOLDER, blocksize, 0);
|
|
|
|
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null))
|
|
{
|
|
var res = c.Backup(new string[] { DATAFOLDER });
|
|
Assert.AreEqual(0, res.Errors.Count());
|
|
Assert.AreEqual(0, res.Warnings.Count());
|
|
if (res.ParsedResult != ParsedResultType.Success)
|
|
throw new Exception("Unexpected result from base backup");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(0);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Success)
|
|
throw new Exception("Unexpected result from backup with return code 0");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did not examine any files for code 0?");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(1);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Success)
|
|
throw new Exception("Unexpected result from backup with return code 1");
|
|
if (res.ExaminedFiles > 0)
|
|
throw new Exception("Backup did examine files for code 1?");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(2);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Warning)
|
|
throw new Exception("Unexpected result from backup with return code 2");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did not examine any files for code 2?");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(3);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Warning)
|
|
throw new Exception("Unexpected result from backup with return code 3");
|
|
if (res.ExaminedFiles > 0)
|
|
throw new Exception("Backup did examine files for code 3?");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(4);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Error)
|
|
throw new Exception("Unexpected result from backup with return code 4");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did not examine any files for code 4?");
|
|
|
|
foreach (int exitCode in new[] { 5, 6, 10, 99 })
|
|
{
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(exitCode);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Error)
|
|
throw new Exception($"Unexpected result from backup with return code {exitCode}");
|
|
if (res.ExaminedFiles > 0)
|
|
throw new Exception($"Backup did examine files for code {exitCode}?");
|
|
}
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(2, "TEST WARNING MESSAGE");
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Warning)
|
|
throw new Exception("Unexpected result from backup with return code 2");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did examine files for code 2?");
|
|
if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0))
|
|
throw new Exception("Found no warning message in output for code 2");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(3, "TEST WARNING MESSAGE");
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Warning)
|
|
throw new Exception("Unexpected result from backup with return code 3");
|
|
if (res.ExaminedFiles > 0)
|
|
throw new Exception("Backup did examine files for code 3?");
|
|
if (!res.Warnings.Any(x => x.IndexOf("TEST WARNING MESSAGE", StringComparison.Ordinal) >= 0))
|
|
throw new Exception("Found no warning message in output for code 3");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(4, "TEST ERROR MESSAGE");
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Error)
|
|
throw new Exception("Unexpected result from backup with return code 4");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did examine files for code 4?");
|
|
if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0))
|
|
throw new Exception("Found no error message in output for code 4");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(5, "TEST ERROR MESSAGE");
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Error)
|
|
throw new Exception("Unexpected result from backup with return code 5");
|
|
if (res.ExaminedFiles > 0)
|
|
throw new Exception("Backup did examine files for code 5?");
|
|
if (!res.Errors.Any(x => x.IndexOf("TEST ERROR MESSAGE", StringComparison.Ordinal) >= 0))
|
|
throw new Exception("Found no error message in output for code 5");
|
|
|
|
System.Threading.Thread.Sleep(PAUSE_TIME);
|
|
options["run-script-before"] = CreateScript(0, sleeptime: 10);
|
|
res = c.Backup(new string[] { DATAFOLDER });
|
|
if (res.ParsedResult != ParsedResultType.Warning)
|
|
throw new Exception("Unexpected result from backup with timeout script");
|
|
if (res.ExaminedFiles <= 0)
|
|
throw new Exception("Backup did not examine any files after timeout?");
|
|
}
|
|
}
|
|
|
|
private static void AssertWithMessage(IEnumerable<string> messages, int count)
|
|
{
|
|
if (messages.Count() != count)
|
|
{
|
|
string message = string.Join(Environment.NewLine, messages);
|
|
Assert.Fail($"Expected {count} messages, but got {messages.Count()}:\n{message}");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
[Category("Border")]
|
|
[TestCase(0)]
|
|
[TestCase(1)]
|
|
[TestCase(2)]
|
|
[TestCase(3)]
|
|
[TestCase(4)]
|
|
[TestCase(5)]
|
|
[TestCase(6)]
|
|
public void RunScriptParsedResult(int exitCode)
|
|
{
|
|
string parsedResultFile = Path.Combine(this.RESTOREFOLDER, "result.txt");
|
|
List<string> customCommands = new List<string>();
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
customCommands.Add($"echo %DUPLICATI__PARSED_RESULT%>\"{parsedResultFile}\"");
|
|
}
|
|
else
|
|
{
|
|
customCommands.Add($"echo $DUPLICATI__PARSED_RESULT>\"{parsedResultFile}\"");
|
|
}
|
|
|
|
Dictionary<string, string> options = this.TestOptions;
|
|
options["run-script-before"] = this.CreateScript(exitCode);
|
|
options["run-script-after"] = this.CreateScript(0, null, null, 0, customCommands);
|
|
using (var c = new Controller("file://" + this.TARGETFOLDER, options, null))
|
|
{
|
|
var backupResults = c.Backup(new[] { this.DATAFOLDER });
|
|
|
|
bool expectBackup;
|
|
string expectedParsedResult;
|
|
switch (exitCode)
|
|
{
|
|
case 0: // OK, run operation
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
// Windows reports warnings due to CI test mounted folders
|
|
AssertWithMessage(backupResults.Errors, 0);
|
|
}
|
|
else
|
|
{
|
|
TestUtils.AssertResults(backupResults);
|
|
}
|
|
expectBackup = true;
|
|
expectedParsedResult = ParsedResultType.Success.ToString();
|
|
break;
|
|
case 1: // OK, don't run operation
|
|
TestUtils.AssertResults(backupResults);
|
|
Assert.IsTrue(backupResults.Interrupted);
|
|
expectBackup = false;
|
|
expectedParsedResult = ParsedResultType.Success.ToString();
|
|
break;
|
|
case 2: // Warning, run operation
|
|
AssertWithMessage(backupResults.Errors, 0);
|
|
AssertWithMessage(backupResults.Warnings, 1);
|
|
Assert.IsFalse(backupResults.Interrupted);
|
|
expectBackup = true;
|
|
expectedParsedResult = ParsedResultType.Warning.ToString();
|
|
break;
|
|
case 3: // Warning, don't run operation
|
|
AssertWithMessage(backupResults.Errors, 0);
|
|
AssertWithMessage(backupResults.Warnings, 1);
|
|
Assert.IsTrue(backupResults.Interrupted);
|
|
expectBackup = false;
|
|
expectedParsedResult = ParsedResultType.Warning.ToString();
|
|
break;
|
|
case 4: // Error, run operation
|
|
AssertWithMessage(backupResults.Errors, 1);
|
|
AssertWithMessage(backupResults.Warnings, 0);
|
|
Assert.IsFalse(backupResults.Interrupted);
|
|
expectBackup = true;
|
|
expectedParsedResult = ParsedResultType.Error.ToString();
|
|
break;
|
|
default: // Error don't run operation
|
|
AssertWithMessage(backupResults.Errors, 1);
|
|
AssertWithMessage(backupResults.Warnings, 0);
|
|
Assert.IsTrue(backupResults.Interrupted);
|
|
expectBackup = false;
|
|
expectedParsedResult = ParsedResultType.Error.ToString();
|
|
break;
|
|
}
|
|
|
|
IEnumerable<string> targetEntries = Directory.EnumerateFileSystemEntries(this.TARGETFOLDER);
|
|
if (expectBackup)
|
|
{
|
|
// We expect a dblock, dlist, and dindex file.
|
|
Assert.AreEqual(3, targetEntries.Count());
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual(0, targetEntries.Count());
|
|
}
|
|
|
|
string[] lines = File.ReadAllLines(parsedResultFile);
|
|
Assert.AreEqual(1, lines.Length);
|
|
Assert.AreEqual(expectedParsedResult, lines[0]);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
[Category("Border")]
|
|
public void CustomRemoteURL()
|
|
{
|
|
string customTargetFolder = Path.Combine(this.TARGETFOLDER, "destination");
|
|
Directory.CreateDirectory(customTargetFolder);
|
|
|
|
List<string> customCommands = new List<string>
|
|
{
|
|
$"echo --remoteurl = \"{customTargetFolder}\""
|
|
};
|
|
|
|
Dictionary<string, string> options = this.TestOptions;
|
|
options["run-script-before"] = CreateScript(0, null, null, 0, customCommands);
|
|
using (Controller c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null))
|
|
{
|
|
IBackupResults backupResults = c.Backup(new[] { this.DATAFOLDER });
|
|
Assert.AreEqual(0, backupResults.Errors.Count());
|
|
Assert.AreEqual(0, backupResults.Warnings.Count());
|
|
}
|
|
|
|
string[] targetEntries = Directory.EnumerateFileSystemEntries(this.TARGETFOLDER).ToArray();
|
|
Assert.AreEqual(1, targetEntries.Length);
|
|
Assert.AreEqual(customTargetFolder, targetEntries[0]);
|
|
|
|
// We expect a dblock, dlist, and dindex file.
|
|
IEnumerable<string> customTargetEntries = Directory.EnumerateFileSystemEntries(customTargetFolder);
|
|
Assert.AreEqual(3, customTargetEntries.Count());
|
|
}
|
|
|
|
private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0, List<string> customCommands = null)
|
|
{
|
|
var id = Guid.NewGuid().ToString("N").Substring(0, 6);
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
var commands = customCommands ?? new List<string>();
|
|
if (!string.IsNullOrWhiteSpace(stdout))
|
|
commands.Add($@"echo {stdout}");
|
|
if (!string.IsNullOrWhiteSpace(stderr))
|
|
commands.Add($@"echo {stderr} 1>&2");
|
|
if (sleeptime > 0)
|
|
commands.Add($@"sleep {sleeptime}");
|
|
|
|
commands.Add($"exit {exitcode}");
|
|
|
|
var filename = Path.GetFullPath(Path.Combine(DATAFOLDER, $"run-script-{id}.bat"));
|
|
File.WriteAllLines(filename, commands);
|
|
|
|
return filename;
|
|
}
|
|
else
|
|
{
|
|
var commands = new List<string>();
|
|
commands.Add("#!/bin/sh");
|
|
|
|
if (customCommands != null)
|
|
{
|
|
commands.AddRange(customCommands);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(stdout))
|
|
commands.Add($@"echo {stdout}");
|
|
if (!string.IsNullOrWhiteSpace(stderr))
|
|
commands.Add($@"(>&2 echo {stderr})");
|
|
if (sleeptime > 0)
|
|
commands.Add($@"sleep {sleeptime}");
|
|
|
|
commands.Add($"exit {exitcode}");
|
|
var filename = Path.GetFullPath(Path.Combine(DATAFOLDER, $"run-script-{id}.sh"));
|
|
File.WriteAllLines(filename, commands);
|
|
|
|
System.Diagnostics.Process.Start("chmod", $@"+x ""{filename}""").WaitForExit();
|
|
|
|
return filename;
|
|
}
|
|
}
|
|
}
|
|
}
|