Files
duplicati/Duplicati/UnitTest/DiskImage/UnitTests/EdgeCaseBoundaryTests.cs
T

382 lines
16 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
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
{
/// <summary>
/// Tests reading at offset 0 (first sector) returns correct data.
/// </summary>
[Test]
public async Task Test_RawDisk_ReadAtOffsetZero_ReturnsCorrectData()
{
var sectorSize = _writableRawDisk.SectorSize;
// Write known pattern to sector 0 (first sector)
var writeBuffer = new byte[sectorSize];
for (int i = 0; i < sectorSize; i++)
writeBuffer[i] = (byte)(i & 0xFF);
await _writableRawDisk.WriteSectorsAsync(0, writeBuffer, CancellationToken.None);
// Read at offset 0
using var stream = await _writableRawDisk.ReadBytesAsync(0, sectorSize, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data at offset 0 should match what was written.");
}
/// <summary>
/// Tests writing at offset 0 (first sector) works correctly.
/// </summary>
[Test]
public async Task Test_RawDisk_WriteAtOffsetZero_DataMatches()
{
var sectorSize = _writableRawDisk.SectorSize;
// Write at offset 0
var writeBuffer = new byte[sectorSize];
new Random(123).NextBytes(writeBuffer);
var bytesWritten = await _writableRawDisk.WriteBytesAsync(0, writeBuffer, CancellationToken.None);
Assert.AreEqual(sectorSize, bytesWritten, "Should have written one sector.");
// Read back and verify
using var readStream = await _writableRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data at offset 0 should match.");
}
/// <summary>
/// Tests reading at the last sector of the disk returns correct data.
/// </summary>
[Test]
public async Task Test_RawDisk_ReadAtLastSector_ReturnsCorrectData()
{
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
var lastSector = (diskSize / sectorSize) - 1;
// Write known pattern to last sector
var writeBuffer = new byte[sectorSize];
for (int i = 0; i < sectorSize; i++)
writeBuffer[i] = (byte)((i + 100) & 0xFF);
await _writableRawDisk.WriteSectorsAsync(lastSector, writeBuffer, CancellationToken.None);
// Read at last sector offset
var lastSectorOffset = lastSector * sectorSize;
using var stream = await _writableRawDisk.ReadBytesAsync(lastSectorOffset, sectorSize, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data at last sector should match.");
}
/// <summary>
/// Tests writing at the last sector of the disk works correctly.
/// </summary>
[Test]
public async Task Test_RawDisk_WriteAtLastSector_DataMatches()
{
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
var lastSector = (diskSize / sectorSize) - 1;
// Write at last sector
var writeBuffer = new byte[sectorSize];
new Random(456).NextBytes(writeBuffer);
var bytesWritten = await _writableRawDisk.WriteSectorsAsync(lastSector, writeBuffer, CancellationToken.None);
Assert.AreEqual(sectorSize, bytesWritten, "Should have written one sector at last position.");
// Read back and verify
using var readStream = await _writableRawDisk.ReadSectorsAsync(lastSector, 1, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data at last sector should match.");
}
/// <summary>
/// Tests reading exactly one sector returns correct data.
/// </summary>
[Test]
public async Task Test_RawDisk_ReadExactlyOneSector_ReturnsCorrectData()
{
var sectorSize = _writableRawDisk.SectorSize;
// Write known pattern to sector 5
var writeBuffer = new byte[sectorSize];
for (int i = 0; i < sectorSize; i++)
writeBuffer[i] = (byte)((i * 3) & 0xFF);
await _writableRawDisk.WriteSectorsAsync(5, writeBuffer, CancellationToken.None);
// Read exactly one sector using ReadBytesAsync
var offset = 5 * sectorSize;
using var stream = await _writableRawDisk.ReadBytesAsync(offset, sectorSize, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read exactly one sector.");
Assert.AreEqual(sectorSize, stream.Length, "Stream length should be exactly one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data should match exactly.");
}
/// <summary>
/// Tests writing exactly one sector works correctly.
/// </summary>
[Test]
public async Task Test_RawDisk_WriteExactlyOneSector_DataMatches()
{
var sectorSize = _writableRawDisk.SectorSize;
// Write exactly one sector using WriteBytesAsync
var writeBuffer = new byte[sectorSize];
new Random(789).NextBytes(writeBuffer);
var offset = 10 * sectorSize;
var bytesWritten = await _writableRawDisk.WriteBytesAsync(offset, writeBuffer, CancellationToken.None);
Assert.AreEqual(sectorSize, bytesWritten, "Should have written exactly one sector.");
// Read back and verify
using var readStream = await _writableRawDisk.ReadSectorsAsync(10, 1, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, "Should have read one sector.");
Assert.AreEqual(writeBuffer, readBuffer, "Data should match exactly.");
}
/// <summary>
/// Tests reading the full disk size works correctly.
/// </summary>
[Test]
public async Task Test_RawDisk_ReadFullDiskSize_ReturnsCorrectData()
{
// Use a smaller test disk for this test to avoid memory issues
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
// Read full disk in chunks to verify it works
const int chunkSectors = 10;
var chunkSize = chunkSectors * sectorSize;
var totalSectors = diskSize / sectorSize;
var sectorsRead = 0L;
for (long sector = 0; sector < totalSectors; sector += chunkSectors)
{
var sectorsToRead = (int)Math.Min(chunkSectors, totalSectors - sector);
using var stream = await _writableRawDisk.ReadSectorsAsync(sector, sectorsToRead, CancellationToken.None);
var buffer = new byte[sectorsToRead * sectorSize];
var bytesRead = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorsToRead * sectorSize, bytesRead, $"Should have read {sectorsToRead} sectors at offset {sector}.");
sectorsRead += sectorsToRead;
}
Assert.AreEqual(totalSectors, sectorsRead, "Should have read all sectors.");
}
/// <summary>
/// Tests writing the full disk size works correctly.
/// </summary>
[Test]
public async Task Test_RawDisk_WriteFullDiskSize_DataMatches()
{
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
// Write a unique pattern to each sector and verify
var totalSectors = diskSize / sectorSize;
const int testSectors = 20; // Test a subset to keep test fast
for (long sector = 0; sector < Math.Min(testSectors, totalSectors); sector++)
{
// Create unique pattern for this sector
var writeBuffer = new byte[sectorSize];
var patternByte = (byte)(sector & 0xFF);
for (int i = 0; i < sectorSize; i++)
writeBuffer[i] = (byte)((patternByte + i) & 0xFF);
await _writableRawDisk.WriteSectorsAsync(sector, writeBuffer, CancellationToken.None);
// Read back and verify
using var readStream = await _writableRawDisk.ReadSectorsAsync(sector, 1, CancellationToken.None);
var readBuffer = new byte[sectorSize];
var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, bytesRead, $"Should read sector {sector}.");
Assert.AreEqual(writeBuffer, readBuffer, $"Data at sector {sector} should match.");
}
}
/// <summary>
/// Tests writing empty data (0 bytes) returns 0 without error.
/// </summary>
[Test]
public async Task Test_RawDisk_WriteEmptyData_ReturnsZero()
{
var sectorSize = _writableRawDisk.SectorSize;
// Write empty data
var emptyData = Array.Empty<byte>();
var bytesWritten = await _writableRawDisk.WriteBytesAsync(sectorSize, emptyData, CancellationToken.None);
Assert.AreEqual(0, bytesWritten, "Writing empty data should return 0 bytes written.");
}
/// <summary>
/// Tests reading empty data (0 bytes) returns 0 without error.
/// </summary>
[Test]
public async Task Test_RawDisk_ReadEmptyData_ReturnsZero()
{
var sectorSize = _writableRawDisk.SectorSize;
// Read 0 bytes
using var stream = await _writableRawDisk.ReadBytesAsync(sectorSize, 0, CancellationToken.None);
var buffer = Array.Empty<byte>();
var bytesRead = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(0, bytesRead, "Reading 0 bytes should return 0.");
Assert.AreEqual(0, stream.Length, "Stream length should be 0 for empty read.");
}
/// <summary>
/// Tests that a disk with minimum possible size still supports basic operations.
/// Creates a small test disk and verifies read/write operations work.
/// </summary>
[Test]
public async Task Test_RawDisk_MinimumSizeDisk_OperationsWork()
{
// The minimum practical disk size is at least a few sectors
// We'll use the writable disk which is already created at 100 MiB
// but verify operations work at the boundaries
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
// Minimum operations: read/write first sector
var firstSectorData = new byte[sectorSize];
new Random(111).NextBytes(firstSectorData);
await _writableRawDisk.WriteSectorsAsync(0, firstSectorData, CancellationToken.None);
using var firstReadStream = await _writableRawDisk.ReadSectorsAsync(0, 1, CancellationToken.None);
var firstReadBuffer = new byte[sectorSize];
var firstBytesRead = await firstReadStream.ReadAsync(firstReadBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, firstBytesRead, "Should read exactly one sector.");
Assert.AreEqual(firstSectorData, firstReadBuffer, "First sector should be readable/writable.");
// Minimum operations: read/write last sector
var lastSector = (diskSize / sectorSize) - 1;
var lastSectorData = new byte[sectorSize];
new Random(222).NextBytes(lastSectorData);
await _writableRawDisk.WriteSectorsAsync(lastSector, lastSectorData, CancellationToken.None);
using var lastReadStream = await _writableRawDisk.ReadSectorsAsync(lastSector, 1, CancellationToken.None);
var lastReadBuffer = new byte[sectorSize];
var lastBytesRead = await lastReadStream.ReadAsync(lastReadBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(sectorSize, lastBytesRead, "Should read exactly one sector.");
Assert.AreEqual(lastSectorData, lastReadBuffer, "Last sector should be readable/writable.");
// Verify disk size is reported correctly
Assert.Greater(diskSize, 0, "Disk size should be greater than 0.");
Assert.AreEqual(0, diskSize % sectorSize, "Disk size should be sector-aligned.");
}
/// <summary>
/// Tests boundary condition: reading at exactly the disk size boundary should fail gracefully.
/// </summary>
[Test]
public void Test_RawDisk_ReadAtDiskSizeBoundary_ThrowsInvalidOperationException()
{
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
// Attempting to read at or beyond disk size should throw InvalidOperationException
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
using var stream = await _writableRawDisk.ReadBytesAsync(diskSize, sectorSize, CancellationToken.None);
var buffer = new byte[sectorSize];
var _ = await stream.ReadAsync(buffer.AsMemory(), CancellationToken.None);
}, "Reading at disk boundary should throw InvalidOperationException.");
StringAssert.Contains("beyond disk size", ex!.Message, "Exception message should mention beyond disk size.");
}
/// <summary>
/// Tests boundary condition: writing at exactly the disk size boundary should fail gracefully.
/// </summary>
[Test]
public void Test_RawDisk_WriteAtDiskSizeBoundary_ThrowsInvalidOperationException()
{
var sectorSize = _writableRawDisk.SectorSize;
var diskSize = _writableRawDisk.Size;
// Attempting to write at or beyond disk size should throw InvalidOperationException
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
var writeData = new byte[sectorSize];
await _writableRawDisk.WriteBytesAsync(diskSize, writeData, CancellationToken.None);
}, "Writing at disk boundary should throw InvalidOperationException.");
StringAssert.Contains("beyond disk size", ex!.Message, "Exception message should mention beyond disk size.");
}
/// <summary>
/// Tests reading/writing a single byte at various boundary positions.
/// </summary>
[Test]
public async Task Test_RawDisk_SingleByteAtBoundaries_DataMatches()
{
var sectorSize = _writableRawDisk.SectorSize;
// Test positions: first byte of disk, last byte of first sector,
// first byte of second sector, and various other positions
var testPositions = new[]
{
0L, // First byte of disk
sectorSize - 1, // Last byte of first sector
sectorSize, // First byte of second sector
sectorSize + 1, // Second byte of second sector
(2 * sectorSize) - 1, // Last byte of second sector
2 * sectorSize // First byte of third sector
};
for (int i = 0; i < testPositions.Length; i++)
{
var position = testPositions[i];
var testByte = (byte)(0xA0 + i);
// Write single byte
var writeData = new[] { testByte };
var bytesWritten = await _writableRawDisk.WriteBytesAsync(position, writeData, CancellationToken.None);
Assert.AreEqual(1, bytesWritten, $"Should write 1 byte at position {position}.");
// Read back the byte
using var stream = await _writableRawDisk.ReadBytesAsync(position, 1, CancellationToken.None);
var readBuffer = new byte[1];
var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None);
Assert.AreEqual(1, bytesRead, $"Should read 1 byte at position {position}.");
Assert.AreEqual(testByte, readBuffer[0], $"Byte at position {position} should match.");
}
}
}