Files
duplicati/Duplicati/UnitTest/DiskImage/UnitTests/UnknownFilesystemTests.cs
T
Kenneth Skovhede 179d1ceb47 Make controller Async
This PR has a large blast radius because it takes the final step and bumps up the Controller to be fully async.

We have historically done a piece-by-piece update, so all operations were already async but the controller interface was kept synchronous.

With this update, the controller is now fully async and all tests are updated.

Most places where the new C# compiler warns about function names not ending in `Async` were also adressed, giving a massive refactor change.

Functionally, no changes are done.
2026-05-13 15:04:13 +02:00

327 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Proprietary.DiskImage.Disk;
using Duplicati.Proprietary.DiskImage.Filesystem;
using Duplicati.Proprietary.DiskImage.General;
using Duplicati.Proprietary.DiskImage.Partition;
using NUnit.Framework;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest.DiskImage.UnitTests;
#nullable enable
public partial class DiskImageUnitTests : BasicSetupHelper
{
/// <summary>
/// Tests that ListFilesAsync returns the expected block-based files for an UnknownFilesystem.
/// </summary>
[Test]
public async Task Test_UnknownFilesystem_ListFilesAsync_ReturnsBlockBasedFiles_Async()
{
// Create a partition on the writable disk
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 10 * MiB;
var partitionOffset = sectorSize * 2048; // Start at sector 2048
// Create a simple partition entry
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
// Create UnknownFilesystem with 1MB block size
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// List files
var files = new List<IFile>();
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
files.Add(file);
// Should have 10 files (10MB partition / 1MB block size)
Assert.AreEqual(10, files.Count, "Should have 10 block files for a 10MB partition with 1MB blocks.");
// Verify each file has correct properties
for (int i = 0; i < files.Count; i++)
{
var file = files[i];
Assert.IsInstanceOf<UnknownFilesystemFile>(file, $"File {i} should be UnknownFilesystemFile.");
Assert.IsFalse(file.IsDirectory, $"File {i} should not be a directory.");
var unknownFile = (UnknownFilesystemFile)file;
Assert.AreEqual(i * 1024L * 1024L, unknownFile.Address, $"File {i} should have correct address.");
// All files except possibly the last should have full block size
if (i < files.Count - 1)
Assert.AreEqual(1024 * 1024, unknownFile.Size, $"File {i} should have full block size.");
}
}
/// <summary>
/// Tests that write and read round-trip preserves data correctly.
/// </summary>
[Test]
public async Task Test_UnknownFilesystem_WriteRead_RoundTrip_DataMatches_Async()
{
// Create a partition on the writable disk
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
// Create UnknownFilesystem with 1MB block size
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// Get first file
IFile? firstFile = null;
await foreach (var file in fs.ListFilesAsync(CancellationToken.None))
{
firstFile = file;
break;
}
Assert.IsNotNull(firstFile, "Should have at least one file.");
// Write data to the file
var writeData = new byte[1024 * 1024];
new Random(42).NextBytes(writeData);
using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None))
{
await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None);
}
// Read data back
byte[] readData;
using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None))
{
readData = new byte[writeData.Length];
var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None);
Assert.AreEqual(writeData.Length, bytesRead, "Should have read all bytes.");
}
// Verify data matches
Assert.AreEqual(writeData, readData, "Read data should match written data.");
}
/// <summary>
/// Tests that BoundsCheck validates ranges correctly.
/// </summary>
[Test]
public void Test_UnknownFilesystem_BoundsCheck_ValidRange_DoesNotThrow()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// Valid ranges should not throw
Assert.DoesNotThrow(() => fs.BoundsCheck(0, sectorSize), "BoundsCheck should not throw for valid range at start.");
Assert.DoesNotThrow(() => fs.BoundsCheck(sectorSize, sectorSize), "BoundsCheck should not throw for valid range.");
Assert.DoesNotThrow(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize), "BoundsCheck should not throw for valid range at end.");
}
/// <summary>
/// Tests that BoundsCheck throws for negative start or size.
/// </summary>
[Test]
public void Test_UnknownFilesystem_BoundsCheck_NegativeValues_ThrowsArgumentOutOfRange()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// Negative start should throw
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(-1, sectorSize), "Negative start should throw.");
// Negative size should throw
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(0, -1), "Negative size should throw.");
}
/// <summary>
/// Tests that BoundsCheck throws for ranges exceeding partition size.
/// </summary>
[Test]
public void Test_UnknownFilesystem_BoundsCheck_ExceedsPartitionSize_ThrowsArgumentOutOfRange()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// Range exceeding partition size should throw
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize * 2), "Range exceeding partition should throw.");
Assert.Throws<ArgumentOutOfRangeException>(() => fs.BoundsCheck(partitionSize, sectorSize), "Start at partition end should throw.");
}
/// <summary>
/// Tests that BoundsCheck throws for unaligned start or size.
/// </summary>
[Test]
public void Test_UnknownFilesystem_BoundsCheck_UnalignedValues_ThrowsArgumentException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
// Unaligned start should throw
Assert.Throws<ArgumentException>(() => fs.BoundsCheck(1, sectorSize), "Unaligned start should throw.");
// Unaligned size should throw
Assert.Throws<ArgumentException>(() => fs.BoundsCheck(0, sectorSize + 1), "Unaligned size should throw.");
}
/// <summary>
/// Tests that UnknownFilesystem constructor throws for unaligned block sizes.
/// </summary>
[Test]
public void Test_UnknownFilesystem_Constructor_UnalignedBlockSize_ThrowsArgumentException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
// Block size not a multiple of sector size should throw
var unalignedBlockSize = sectorSize + 1;
var ex = Assert.Throws<ArgumentException>(() => new UnknownFilesystem(partition, blockSize: unalignedBlockSize),
"Unaligned block size should throw ArgumentException.");
Assert.That(ex!.ParamName, Is.EqualTo("blockSize"), "Exception should be for blockSize parameter.");
}
/// <summary>
/// Tests that UnknownFilesystem constructor accepts valid block sizes.
/// </summary>
[Test]
public void Test_UnknownFilesystem_Constructor_ValidBlockSize_CreatesInstance()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 5 * MiB;
var partitionOffset = sectorSize * 2048;
var partition = new TestPartition
{
StartOffset = partitionOffset,
Size = partitionSize,
RawDisk = _writableRawDisk
};
// Valid block sizes should create instance
Assert.DoesNotThrow(() =>
{
using var fs1 = new UnknownFilesystem(partition, blockSize: sectorSize);
}, "Block size equal to sector size should work.");
Assert.DoesNotThrow(() =>
{
using var fs2 = new UnknownFilesystem(partition, blockSize: sectorSize * 4);
}, "Block size multiple of sector size should work.");
Assert.DoesNotThrow(() =>
{
using var fs3 = new UnknownFilesystem(partition, blockSize: 1024 * 1024);
}, "1MB block size should work.");
}
/// <summary>
/// Helper class to implement IPartition for testing.
/// </summary>
private class TestPartition : IPartition
{
public long StartOffset { get; init; }
public long Size { get; init; }
public IRawDisk? RawDisk { get; init; }
public int PartitionNumber => 1;
public string? Name => "TestPartition";
public Guid? UniqueId => Guid.Empty;
public PartitionType Type => PartitionType.Primary;
public FileSystemType FilesystemType => FileSystemType.Unknown;
public Guid? VolumeGuid => Guid.Empty;
public IPartitionTable PartitionTable => new TestPartitionTable { RawDisk = RawDisk };
public Task<Stream> OpenReadAsync(CancellationToken cancellationToken)
=> Task.FromException<Stream>(new NotImplementedException());
public Task<Stream> OpenWriteAsync(CancellationToken cancellationToken)
=> Task.FromException<Stream>(new NotImplementedException());
public void Dispose() { }
}
/// <summary>
/// Helper class to implement IPartitionTable for testing.
/// </summary>
private class TestPartitionTable : IPartitionTable
{
public IRawDisk? RawDisk { get; init; }
public PartitionTableType TableType => PartitionTableType.Unknown;
public long Size => 0;
public IAsyncEnumerable<IPartition> EnumeratePartitions(CancellationToken cancellationToken)
=> AsyncEnumerable.Empty<IPartition>();
public Task<IPartition?> GetPartitionAsync(int partitionNumber, CancellationToken cancellationToken)
=> Task.FromResult<IPartition?>(null);
public Task<Stream> GetProtectiveMbrAsync(CancellationToken cancellationToken)
=> Task.FromException<Stream>(new NotImplementedException());
public Task<Stream> GetPartitionTableDataAsync(CancellationToken cancellationToken)
=> Task.FromException<Stream>(new NotImplementedException());
public void Dispose() { }
}
}