Files
duplicati/Duplicati/UnitTest/DiskImage/UnitTests/PartitionWriteStreamTests.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

334 lines
14 KiB
C#

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Proprietary.DiskImage.Partition;
using NUnit.Framework;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
using StringAssert = NUnit.Framework.Legacy.StringAssert;
namespace Duplicati.UnitTest.DiskImage.UnitTests;
public partial class DiskImageUnitTests : BasicSetupHelper
{
[Test]
public async Task Test_PartitionWriteStream_WriteAndDispose_DataFlushedToDisk_Async()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 10 * sectorSize;
var partitionSize = 4 * sectorSize;
// Write data using PartitionWriteStream
var writeData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
await stream.WriteAsync(writeData, CancellationToken.None);
// Read back and verify data was flushed to disk
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, writeData.Length, CancellationToken.None);
var readBuffer = new byte[writeData.Length];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(writeData.Length, bytesRead, "Should have read the written data.");
Assert.AreEqual(writeData, readBuffer, "Data should match what was written.");
}
[Test]
public void Test_PartitionWriteStream_SeekBegin_ValidPosition()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 4 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Test Seek with SeekOrigin.Begin
var newPosition = stream.Seek(100, SeekOrigin.Begin);
Assert.AreEqual(100, newPosition, "Position should be 100 after seeking from Begin.");
Assert.AreEqual(100, stream.Position, "Stream.Position should match returned position.");
}
[Test]
public void Test_PartitionWriteStream_SeekCurrent_ValidPosition()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 4 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Write some data first to move position
stream.Write([0x01, 0x02, 0x03], 0, 3);
Assert.AreEqual(3, stream.Position, "Position should be 3 after writing 3 bytes.");
// Test Seek with SeekOrigin.Current
var newPosition = stream.Seek(50, SeekOrigin.Current);
Assert.AreEqual(53, newPosition, "Position should be 53 after seeking 50 from current position 3.");
Assert.AreEqual(53, stream.Position, "Stream.Position should match returned position.");
}
[Test]
public void Test_PartitionWriteStream_SeekEnd_ValidPosition()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 4 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Write some data first to establish length
stream.Write([0x01, 0x02, 0x03, 0x04, 0x05], 0, 5);
Assert.AreEqual(5, stream.Length, "Length should be 5 after writing 5 bytes.");
// Test Seek with SeekOrigin.End
var newPosition = stream.Seek(-2, SeekOrigin.End);
Assert.AreEqual(3, newPosition, "Position should be 3 after seeking -2 from end of length 5.");
Assert.AreEqual(3, stream.Position, "Stream.Position should match returned position.");
}
[Test]
public void Test_PartitionWriteStream_WriteBeyondSize_ThrowsIOException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 10 * sectorSize;
var partitionSize = 2 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize);
// Try to write more than partition size
var largeBuffer = new byte[partitionSize + 100];
new Random().NextBytes(largeBuffer);
var ex = Assert.Throws<IOException>(() =>
{
stream.Write(largeBuffer, 0, largeBuffer.Length);
}, "Writing beyond partition size should throw IOException.");
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
}
[Test]
public void Test_PartitionWriteStream_SeekBeyondSize_ThrowsIOException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 2 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Try to seek beyond partition size
var ex = Assert.Throws<IOException>(() =>
{
stream.Seek(partitionSize + 100, SeekOrigin.Begin);
}, "Seeking beyond partition size should throw IOException.");
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
}
[Test]
public void Test_PartitionWriteStream_SeekNegativePosition_ThrowsIOException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 2 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Try to seek to negative position
Assert.Throws<IOException>(() =>
{
stream.Seek(-100, SeekOrigin.Begin);
}, "Seeking to negative position should throw IOException.");
}
[Test]
public async Task Test_PartitionWriteStream_WriteAtUnalignedPosition_DataMatches_Async()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 15 * sectorSize;
var partitionSize = 4 * sectorSize;
// Write data at various unaligned positions
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
{
// Write at position 0
await stream.WriteAsync([0xAA, 0xBB], 0, 2);
// Seek to unaligned position
stream.Seek(7, SeekOrigin.Begin);
await stream.WriteAsync([0xCC, 0xDD, 0xEE], 0, 3);
// Seek to another unaligned position
stream.Seek(20, SeekOrigin.Begin);
await stream.WriteAsync([0x11, 0x22, 0x33, 0x44], 0, 4);
}
// Read back and verify
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 25, CancellationToken.None);
var readBuffer = new byte[25];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(25, bytesRead, "Should read exactly 25 bytes.");
// Verify data at position 0
Assert.AreEqual(0xAA, readBuffer[0], "Byte at position 0 should be 0xAA.");
Assert.AreEqual(0xBB, readBuffer[1], "Byte at position 1 should be 0xBB.");
// Verify data at position 7
Assert.AreEqual(0xCC, readBuffer[7], "Byte at position 7 should be 0xCC.");
Assert.AreEqual(0xDD, readBuffer[8], "Byte at position 8 should be 0xDD.");
Assert.AreEqual(0xEE, readBuffer[9], "Byte at position 9 should be 0xEE.");
// Verify data at position 20
Assert.AreEqual(0x11, readBuffer[20], "Byte at position 20 should be 0x11.");
Assert.AreEqual(0x22, readBuffer[21], "Byte at position 21 should be 0x22.");
Assert.AreEqual(0x33, readBuffer[22], "Byte at position 22 should be 0x33.");
Assert.AreEqual(0x44, readBuffer[23], "Byte at position 23 should be 0x44.");
}
[Test]
public async Task Test_PartitionWriteStream_WriteMultipleTimes_DataMatches_Async()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 20 * sectorSize;
var partitionSize = 3 * sectorSize;
// Write data in multiple operations
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
{
// First write
await stream.WriteAsync([0x01, 0x02, 0x03], 0, 3);
// Second write (should append)
await stream.WriteAsync([0x04, 0x05], 0, 2);
// Third write
await stream.WriteAsync([0x06, 0x07, 0x08, 0x09], 0, 4);
}
// Read back and verify all data was written
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 9, CancellationToken.None);
var readBuffer = new byte[9];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(9, bytesRead, "Should read exactly 9 bytes.");
var expected = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };
Assert.AreEqual(expected, readBuffer, "All written data should match.");
}
[Test]
public void Test_PartitionWriteStream_SetLengthBeyondMaxSize_ThrowsIOException()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionSize = 2 * sectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize);
// Try to set length beyond partition size
var ex = Assert.Throws<IOException>(() =>
{
stream.SetLength(partitionSize + 100);
}, "Setting length beyond partition size should throw IOException.");
StringAssert.Contains("partition size", ex!.Message, "Exception message should mention partition size.");
}
[Test]
public async Task Test_PartitionWriteStream_SetLengthWithinMaxSize_SetsLength_Async()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 10 * sectorSize;
var partitionSize = 2 * sectorSize;
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
{
// Set length to a specific value within bounds
stream.SetLength(100);
Assert.AreEqual(100, stream.Length, "Length should be set to 100.");
// Write some data at position 0
await stream.WriteAsync([0xAA, 0xBB, 0xCC], 0, 3);
// Length should be max(SetLength(100), written position) = 100
Assert.AreEqual(100, stream.Length, "Length should remain 100 (SetLength value).");
}
// Verify the 3 bytes were written at position 0 and length 100 bytes are flushed
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 100, CancellationToken.None);
var readBuffer = new byte[100];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(100, bytesRead, "Should read exactly 100 bytes.");
Assert.AreEqual(0xAA, readBuffer[0], "Byte at position 0 should be 0xAA.");
Assert.AreEqual(0xBB, readBuffer[1], "Byte at position 1 should be 0xBB.");
Assert.AreEqual(0xCC, readBuffer[2], "Byte at position 2 should be 0xCC.");
}
[Test]
public void Test_PartitionWriteStream_CanRead_ReturnsFalse()
{
var sectorSize = _writableRawDisk.SectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
Assert.IsFalse(stream.CanRead, "CanRead should be false.");
}
[Test]
public void Test_PartitionWriteStream_CanWrite_ReturnsTrue()
{
var sectorSize = _writableRawDisk.SectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
Assert.IsTrue(stream.CanWrite, "CanWrite should be true.");
}
[Test]
public void Test_PartitionWriteStream_CanSeek_ReturnsTrue()
{
var sectorSize = _writableRawDisk.SectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
Assert.IsTrue(stream.CanSeek, "CanSeek should be true.");
}
[Test]
public void Test_PartitionWriteStream_Read_ThrowsNotSupported()
{
var sectorSize = _writableRawDisk.SectorSize;
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
Assert.Throws<NotSupportedException>(() =>
{
var _ = stream.Read(new byte[10], 0, 10);
}, "Read should throw NotSupportedException.");
}
[Test]
public async Task Test_PartitionWriteStream_WriteEmptyData_NoFlush_Async()
{
var sectorSize = _writableRawDisk.SectorSize;
var partitionOffset = 10 * sectorSize;
var partitionSize = sectorSize;
// Read initial data at this offset (may not be zero due to previous tests)
using var initialReadStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 10, CancellationToken.None);
var initialBuffer = new byte[10];
var initialBytesRead = await initialReadStream.ReadAsync(initialBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(10, initialBytesRead, "Should read 10 bytes.");
// Create stream but write nothing
using (var stream = new PartitionWriteStream(_writableRawDisk, partitionOffset, partitionSize))
// Don't write anything
Assert.AreEqual(0, stream.Length, "Length should be 0 when nothing is written.");
// Read back and verify no data was flushed (should be unchanged)
using var readStream = await _writableRawDisk.ReadBytesAsync(partitionOffset, 10, CancellationToken.None);
var readBuffer = new byte[10];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(10, bytesRead, "Should read 10 bytes.");
// All bytes should be unchanged from before the stream was created
for (int i = 0; i < readBuffer.Length; i++)
{
Assert.AreEqual(initialBuffer[i], readBuffer[i], $"Byte at position {i} should be unchanged (no data written).");
}
}
}