duplicati/Duplicati/UnitTest/Issue4693.cs

223 lines
10 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.IO;
using System.Security.Cryptography;
using Duplicati.Library.Main.Database;
using Duplicati.Library.SQLiteHelper;
using NUnit.Framework;
namespace Duplicati.UnitTest
{
public class Issue4693 : BasicSetupHelper
{
[Test]
[Category("Targeted")]
public void RunCommandsOriginal()
{
var testopts = TestOptions.Expand(new { no_encryption = true, keep_versions = 1, dblock_size = "5mb" });
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
File.WriteAllText(Path.Combine(DATAFOLDER, "B.txt"), "B");
var buffer = new byte[1024 * 1024 * 12];
Random.Shared.NextBytes(buffer);
File.WriteAllBytes(Path.Combine(DATAFOLDER, "b1.bin"), buffer);
var a_hash = Convert.ToBase64String(SHA256.HashData(File.ReadAllBytes(Path.Combine(DATAFOLDER, "A.txt"))));
// Backup 1
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
File.Delete(Path.Combine(DATAFOLDER, "A.txt"));
File.WriteAllText(Path.Combine(DATAFOLDER, "C.txt"), "C");
Random.Shared.NextBytes(buffer);
File.WriteAllBytes(Path.Combine(DATAFOLDER, "b2.bin"), buffer);
// Backup 2
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
Random.Shared.NextBytes(buffer);
File.WriteAllBytes(Path.Combine(DATAFOLDER, "b3.bin"), buffer);
if (a_hash != Convert.ToBase64String(SHA256.HashData(File.ReadAllBytes(Path.Combine(DATAFOLDER, "A.txt")))))
throw new Exception("Hash of A.txt changed");
// Backup 3
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
File.Delete(Path.Combine(DATAFOLDER, "b1.bin"));
File.Delete(Path.Combine(DATAFOLDER, "b2.bin"));
// Backup 4
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var res = c.Backup([DATAFOLDER]);
TestUtils.AssertResults(res);
Assert.That(res.CompactResults.DeletedFileCount, Is.GreaterThan(0), "Compact was not run");
}
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
// Test we can recrate without errors
File.Delete(DBFILE);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Repair());
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
}
[Test]
[Category("Targeted")]
public void RunCommandsSimplified()
{
var testopts = TestOptions.Expand(new { no_encryption = true, keep_versions = 1, dblock_size = "5mb" });
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
File.WriteAllText(Path.Combine(DATAFOLDER, "B.txt"), "B");
// Backup 1
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
File.Delete(Path.Combine(DATAFOLDER, "A.txt"));
File.WriteAllText(Path.Combine(DATAFOLDER, "C.txt"), "C");
// Backup 2
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
// Backup 3
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
// Test we can recrate without errors
File.Delete(DBFILE);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Repair());
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
}
[Test]
[Category("Targeted")]
// Use the previous behavior, and ignore deleted blocks
[TestCase(-1)]
// Force a temporary table
[TestCase(0)]
// Use in-memory cache
[TestCase(1000)]
public void RunWithCaches(int cacheSize)
{
Environment.SetEnvironmentVariable("DUPLICATI_DELETEDBLOCKCACHESIZE", cacheSize.ToString());
var testopts = TestOptions.Expand(new { no_encryption = true, keep_versions = 1, dblock_size = "5mb" });
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
File.WriteAllText(Path.Combine(DATAFOLDER, "B.txt"), "B");
// Make sure this volume is not compacted away
File.WriteAllBytes(Path.Combine(DATAFOLDER, "filler.bin"), new byte[1024 * 1024 * 2]);
// Backup 1
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
using (var db = SQLiteLoader.LoadConnection(DBFILE))
using (var cmd = db.CreateCommand())
{
var deletedBlocks = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""DeletedBlock""");
Assert.That(deletedBlocks, Is.EqualTo(0), "DeletedBlock table is not empty");
}
File.Delete(Path.Combine(DATAFOLDER, "A.txt"));
File.WriteAllText(Path.Combine(DATAFOLDER, "C.txt"), "C");
// Backup 2
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
using (var db = SQLiteLoader.LoadConnection(DBFILE))
using (var cmd = db.CreateCommand())
{
var deletedBlocks = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""DeletedBlock""");
Assert.That(deletedBlocks, Is.GreaterThan(0), "DeletedBlock table is empty");
}
using (var db = SQLiteLoader.LoadConnection(DBFILE))
using (var cmd = db.CreateCommand())
{
var deletedBlocks = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""DeletedBlock""");
Assert.That(deletedBlocks, Is.GreaterThan(0), "DeletedBlock table is empty");
}
File.WriteAllText(Path.Combine(DATAFOLDER, "A.txt"), "A");
// Backup 3
System.Threading.Thread.Sleep(1000); // Ensure different timestamp
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Backup([DATAFOLDER]));
using (var db = SQLiteLoader.LoadConnection(DBFILE))
using (var cmd = db.CreateCommand())
{
var alldeletedBlocks = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""DeletedBlock""");
var deletedBlocks = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""DeletedBlock"" WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"")");
if (cacheSize < 0)
Assert.That(deletedBlocks, Is.GreaterThan(0), "DeletedBlock table contains no duplicates, but lookup is turned off");
else
Assert.That(deletedBlocks, Is.EqualTo(0), "DeletedBlock table contains duplicates, but lookup is turned on");
}
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
// Test we can recrate without errors
File.Delete(DBFILE);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Repair());
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Test(long.MaxValue));
Environment.SetEnvironmentVariable("DUPLICATI_DELETEDBLOCKCACHESIZE", null);
}
}
}