Convert Ryujinx.Tests.Memory tests to xUnit

This commit is contained in:
TSR Berry 2023-07-08 20:11:28 +02:00
commit f0441bf28b
No known key found for this signature in database
GPG key ID: 52353C0A4CCA15E2
5 changed files with 157 additions and 122 deletions

View file

@ -79,7 +79,7 @@ namespace Ryujinx.Tests.Memory
IEnumerable<MemoryRange> IVirtualMemoryManager.GetPhysicalRegions(ulong va, ulong size) IEnumerable<MemoryRange> IVirtualMemoryManager.GetPhysicalRegions(ulong va, ulong size)
{ {
return NoMappings ? Array.Empty<MemoryRange>() : new MemoryRange[] { new MemoryRange(va, size) }; return NoMappings ? Array.Empty<MemoryRange>() : new[] { new MemoryRange(va, size) };
} }
public bool IsMapped(ulong va) public bool IsMapped(ulong va)

View file

@ -1,13 +1,13 @@
using NUnit.Framework;
using Ryujinx.Memory; using Ryujinx.Memory;
using Ryujinx.Memory.Tracking; using Ryujinx.Memory.Tracking;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Xunit;
namespace Ryujinx.Tests.Memory namespace Ryujinx.Tests.Memory
{ {
public class MultiRegionTrackingTests public class MultiRegionTrackingTests : IDisposable
{ {
private const ulong MemorySize = 0x8000; private const ulong MemorySize = 0x8000;
private const int PageSize = 4096; private const int PageSize = 4096;
@ -16,17 +16,16 @@ namespace Ryujinx.Tests.Memory
private MemoryTracking _tracking; private MemoryTracking _tracking;
private MockVirtualMemoryManager _memoryManager; private MockVirtualMemoryManager _memoryManager;
[SetUp] public MultiRegionTrackingTests()
public void Setup()
{ {
_memoryBlock = new MemoryBlock(MemorySize); _memoryBlock = new MemoryBlock(MemorySize);
_memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize);
_tracking = new MemoryTracking(_memoryManager, PageSize); _tracking = new MemoryTracking(_memoryManager, PageSize);
} }
[TearDown] public void Dispose()
public void Teardown()
{ {
GC.SuppressFinalize(this);
_memoryBlock.Dispose(); _memoryBlock.Dispose();
} }
@ -56,8 +55,8 @@ namespace Ryujinx.Tests.Memory
handle.QueryModified(startAddress, size, (address, range) => handle.QueryModified(startAddress, size, (address, range) =>
{ {
Assert.IsTrue(addressPredicate(address)); // Written pages must be even. Assert.True(addressPredicate(address)); // Written pages must be even.
Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. Assert.True(address >= lastAddress); // Must be signalled in ascending order, regardless of write order.
lastAddress = address; lastAddress = address;
regionCount++; regionCount++;
}); });
@ -72,8 +71,8 @@ namespace Ryujinx.Tests.Memory
handle.QueryModified(startAddress, size, (address, range) => handle.QueryModified(startAddress, size, (address, range) =>
{ {
Assert.IsTrue(addressPredicate(address)); // Written pages must be even. Assert.True(addressPredicate(address)); // Written pages must be even.
Assert.GreaterOrEqual(address, lastAddress); // Must be signalled in ascending order, regardless of write order. Assert.True(address >= lastAddress); // Must be signalled in ascending order, regardless of write order.
lastAddress = address; lastAddress = address;
regionCount++; regionCount++;
}, sequenceNumber); }, sequenceNumber);
@ -93,12 +92,14 @@ namespace Ryujinx.Tests.Memory
{ {
resultAddress = address; resultAddress = address;
}); });
Assert.AreEqual(resultAddress, (ulong)i * PageSize + address); Assert.Equal((ulong)i * PageSize + address, resultAddress);
}); });
} }
[Test] [Theory]
public void DirtyRegionOrdering([Values] bool smart) [InlineData(true)]
[InlineData(false)]
public void DirtyRegionOrdering(bool smart)
{ {
const int PageCount = 32; const int PageCount = 32;
IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * PageCount, PageSize); IMultiRegionHandle handle = GetGranular(smart, 0, PageSize * PageCount, PageSize);
@ -119,7 +120,7 @@ namespace Ryujinx.Tests.Memory
int oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 1); int oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 1);
Assert.AreEqual(oddRegionCount, PageCount / 2); // Must have written to all odd pages. Assert.Equal(PageCount / 2, oddRegionCount); // Must have written to all odd pages.
// Write to all the even pages. // Write to all the even pages.
RandomOrder(random, even, (i) => RandomOrder(random, even, (i) =>
@ -129,11 +130,13 @@ namespace Ryujinx.Tests.Memory
int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 0); int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 0);
Assert.AreEqual(evenRegionCount, PageCount / 2); Assert.Equal(PageCount / 2, evenRegionCount);
} }
[Test] [Theory]
public void SequenceNumber([Values] bool smart) [InlineData(true)]
[InlineData(false)]
public void SequenceNumber(bool smart)
{ {
// The sequence number can be used to ignore dirty flags, and defer their consumption until later. // The sequence number can be used to ignore dirty flags, and defer their consumption until later.
// If a user consumes a dirty flag with sequence number 1, then there is a write to the protected region, // If a user consumes a dirty flag with sequence number 1, then there is a write to the protected region,
@ -172,7 +175,7 @@ namespace Ryujinx.Tests.Memory
}, 1); }, 1);
} }
Assert.AreEqual(oddRegionCount, PageCount / 2); // Must have written to all odd pages. Assert.Equal(PageCount / 2, oddRegionCount); // Must have written to all odd pages.
// Write to all pages. // Write to all pages.
@ -182,22 +185,22 @@ namespace Ryujinx.Tests.Memory
int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 0, 1); int evenRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 0, 1);
Assert.AreEqual(evenRegionCount, PageCount / 2); // Must have written to all even pages. Assert.Equal(PageCount / 2, evenRegionCount); // Must have written to all even pages.
oddRegionCount = 0; oddRegionCount = 0;
handle.QueryModified(0, PageSize * PageCount, (address, range) => { oddRegionCount++; }, 1); handle.QueryModified(0, PageSize * PageCount, (address, range) => { oddRegionCount++; }, 1);
Assert.AreEqual(oddRegionCount, 0); // Sequence number has not changed, so found no dirty subregions. Assert.Equal(0, oddRegionCount); // Sequence number has not changed, so found no dirty subregions.
// With sequence number 2, all all pages should be reported as modified. // With sequence number 2, all all pages should be reported as modified.
oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 1, 2); oddRegionCount = ExpectQueryInOrder(handle, 0, PageSize * PageCount, (address) => (address / PageSize) % 2 == 1, 2);
Assert.AreEqual(oddRegionCount, PageCount / 2); // Must have written to all odd pages. Assert.Equal(PageCount / 2, oddRegionCount); // Must have written to all odd pages.
} }
[Test] [Fact]
public void SmartRegionTracking() public void SmartRegionTracking()
{ {
// Smart multi region handles dynamically change their tracking granularity based on QueryMemory calls. // Smart multi region handles dynamically change their tracking granularity based on QueryMemory calls.
@ -208,7 +211,7 @@ namespace Ryujinx.Tests.Memory
// Query some large regions to prep the subdivision of the tracking region. // Query some large regions to prep the subdivision of the tracking region.
int[] regionSizes = new int[] { 6, 4, 3, 2, 6, 1 }; int[] regionSizes = { 6, 4, 3, 2, 6, 1 };
ulong address = 0; ulong address = 0;
for (int i = 0; i < regionSizes.Length; i++) for (int i = 0; i < regionSizes.Length; i++)
@ -242,26 +245,28 @@ namespace Ryujinx.Tests.Memory
{ {
int region = regionSizes[regionInd++]; int region = regionSizes[regionInd++];
Assert.AreEqual(address, expectedAddress); Assert.Equal(expectedAddress, address);
Assert.AreEqual(size, (ulong)(PageSize * region)); Assert.Equal((ulong)(PageSize * region), size);
expectedAddress += (ulong)(PageSize * (region + 1)); expectedAddress += (ulong)(PageSize * (region + 1));
}); });
} }
[Test] [Theory]
public void DisposeMultiHandles([Values] bool smart) [InlineData(true)]
[InlineData(false)]
public void DisposeMultiHandles(bool smart)
{ {
// Create and initialize two overlapping Multi Region Handles, with PageSize granularity. // Create and initialize two overlapping Multi Region Handles, with PageSize granularity.
const int PageCount = 32; const int PageCount = 32;
const int OverlapStart = 16; const int OverlapStart = 16;
Assert.AreEqual(0, _tracking.GetRegionCount()); Assert.Equal(0, _tracking.GetRegionCount());
IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * PageCount, PageSize); IMultiRegionHandle handleLow = GetGranular(smart, 0, PageSize * PageCount, PageSize);
PreparePages(handleLow, PageCount); PreparePages(handleLow, PageCount);
Assert.AreEqual(PageCount, _tracking.GetRegionCount()); Assert.Equal(PageCount, _tracking.GetRegionCount());
IMultiRegionHandle handleHigh = GetGranular(smart, PageSize * OverlapStart, PageSize * PageCount, PageSize); IMultiRegionHandle handleHigh = GetGranular(smart, PageSize * OverlapStart, PageSize * PageCount, PageSize);
PreparePages(handleHigh, PageCount, PageSize * OverlapStart); PreparePages(handleHigh, PageCount, PageSize * OverlapStart);
@ -269,18 +274,18 @@ namespace Ryujinx.Tests.Memory
// Combined pages (and assuming overlapStart <= pageCount) should be pageCount after overlapStart. // Combined pages (and assuming overlapStart <= pageCount) should be pageCount after overlapStart.
int totalPages = OverlapStart + PageCount; int totalPages = OverlapStart + PageCount;
Assert.AreEqual(totalPages, _tracking.GetRegionCount()); Assert.Equal(totalPages, _tracking.GetRegionCount());
handleLow.Dispose(); // After disposing one, the pages for the other remain. handleLow.Dispose(); // After disposing one, the pages for the other remain.
Assert.AreEqual(PageCount, _tracking.GetRegionCount()); Assert.Equal(PageCount, _tracking.GetRegionCount());
handleHigh.Dispose(); // After disposing the other, there are no pages left. handleHigh.Dispose(); // After disposing the other, there are no pages left.
Assert.AreEqual(0, _tracking.GetRegionCount()); Assert.Equal(0, _tracking.GetRegionCount());
} }
[Test] [Fact]
public void InheritHandles() public void InheritHandles()
{ {
// Test merging the following into a granular region handle: // Test merging the following into a granular region handle:
@ -333,8 +338,7 @@ namespace Ryujinx.Tests.Memory
// Finally, create a granular handle that inherits all these handles. // Finally, create a granular handle that inherits all these handles.
IEnumerable<IRegionHandle>[] handleGroups = new IEnumerable<IRegionHandle>[] IEnumerable<IRegionHandle>[] handleGroups = {
{
granular.GetHandles(), granular.GetHandles(),
singlePages, singlePages,
doublePages, doublePages,
@ -342,8 +346,7 @@ namespace Ryujinx.Tests.Memory
MultiRegionHandle combined = _tracking.BeginGranularTracking(0, PageSize * 18, handleGroups.SelectMany((handles) => handles), PageSize, 0); MultiRegionHandle combined = _tracking.BeginGranularTracking(0, PageSize * 18, handleGroups.SelectMany((handles) => handles), PageSize, 0);
bool[] expectedDirty = new bool[] bool[] expectedDirty = {
{
true, true, true, // Gap. true, true, true, // Gap.
false, true, false, // Multi-region. false, true, false, // Multi-region.
true, true, // Gap. true, true, // Gap.
@ -357,19 +360,19 @@ namespace Ryujinx.Tests.Memory
bool modified = false; bool modified = false;
combined.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { modified = true; }); combined.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { modified = true; });
Assert.AreEqual(expectedDirty[i], modified); Assert.Equal(expectedDirty[i], modified);
} }
Assert.AreEqual(new bool[3], actionsTriggered); Assert.Equal(new bool[3], actionsTriggered);
_tracking.VirtualMemoryEvent(PageSize * 5, PageSize, false); _tracking.VirtualMemoryEvent(PageSize * 5, PageSize, false);
Assert.IsTrue(actionsTriggered[0]); Assert.True(actionsTriggered[0]);
_tracking.VirtualMemoryEvent(PageSize * 10, PageSize, false); _tracking.VirtualMemoryEvent(PageSize * 10, PageSize, false);
Assert.IsTrue(actionsTriggered[1]); Assert.True(actionsTriggered[1]);
_tracking.VirtualMemoryEvent(PageSize * 15, PageSize, false); _tracking.VirtualMemoryEvent(PageSize * 15, PageSize, false);
Assert.IsTrue(actionsTriggered[2]); Assert.True(actionsTriggered[2]);
// The double page handles should be disposed, as they were split into granular handles. // The double page handles should be disposed, as they were split into granular handles.
foreach (RegionHandle doublePage in doublePages) foreach (RegionHandle doublePage in doublePages)
@ -386,21 +389,21 @@ namespace Ryujinx.Tests.Memory
throws = true; throws = true;
} }
Assert.IsTrue(throws); Assert.True(throws);
} }
IEnumerable<IRegionHandle> combinedHandles = combined.GetHandles(); IEnumerable<IRegionHandle> combinedHandles = combined.GetHandles();
Assert.AreEqual(handleGroups[0].ElementAt(0), combinedHandles.ElementAt(3)); Assert.Equal(handleGroups[0].ElementAt(0), combinedHandles.ElementAt(3));
Assert.AreEqual(handleGroups[0].ElementAt(1), combinedHandles.ElementAt(4)); Assert.Equal(handleGroups[0].ElementAt(1), combinedHandles.ElementAt(4));
Assert.AreEqual(handleGroups[0].ElementAt(2), combinedHandles.ElementAt(5)); Assert.Equal(handleGroups[0].ElementAt(2), combinedHandles.ElementAt(5));
Assert.AreEqual(singlePages[0], combinedHandles.ElementAt(8)); Assert.Equal(singlePages[0], combinedHandles.ElementAt(8));
Assert.AreEqual(singlePages[1], combinedHandles.ElementAt(9)); Assert.Equal(singlePages[1], combinedHandles.ElementAt(9));
Assert.AreEqual(singlePages[2], combinedHandles.ElementAt(10)); Assert.Equal(singlePages[2], combinedHandles.ElementAt(10));
} }
[Test] [Fact]
public void PreciseAction() public void PreciseAction()
{ {
bool actionTriggered = false; bool actionTriggered = false;
@ -413,11 +416,11 @@ namespace Ryujinx.Tests.Memory
// Precise write to first handle in the multiregion. // Precise write to first handle in the multiregion.
_tracking.VirtualMemoryEvent(PageSize * 3, PageSize, true, precise: true); _tracking.VirtualMemoryEvent(PageSize * 3, PageSize, true, precise: true);
Assert.IsFalse(actionTriggered); // Action not triggered. Assert.False(actionTriggered); // Action not triggered.
bool firstPageModified = false; bool firstPageModified = false;
granular.QueryModified(PageSize * 3, PageSize, (_, _) => { firstPageModified = true; }); granular.QueryModified(PageSize * 3, PageSize, (_, _) => { firstPageModified = true; });
Assert.IsTrue(firstPageModified); // First page is modified. Assert.True(firstPageModified); // First page is modified.
// Precise write to all handles in the multiregion. // Precise write to all handles in the multiregion.
_tracking.VirtualMemoryEvent(PageSize * 3, PageSize * 3, true, precise: true); _tracking.VirtualMemoryEvent(PageSize * 3, PageSize * 3, true, precise: true);
@ -430,10 +433,10 @@ namespace Ryujinx.Tests.Memory
granular.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { pagesModified[index] = true; }); granular.QueryModified(PageSize * (ulong)i, PageSize, (_, _) => { pagesModified[index] = true; });
} }
Assert.IsTrue(actionTriggered); // Action triggered. Assert.True(actionTriggered); // Action triggered.
// Precise writes are ignored on two later handles due to the action returning true. // Precise writes are ignored on two later handles due to the action returning true.
Assert.AreEqual(pagesModified, new bool[] { true, false, false }); Assert.Equal(new[] { true, false, false }, pagesModified);
} }
} }
} }

View file

@ -0,0 +1,27 @@
using System;
using Xunit;
namespace Ryujinx.Tests.Memory
{
public class RandomRangeUL2TheoryData : TheoryData<ulong, ulong>
{
public RandomRangeUL2TheoryData(ulong from, ulong to, int count)
{
byte[] buffer = new byte[8];
for (int i = 0; i < count; i++)
{
ulong[] results = new ulong[2];
for (int j = 0; j < results.Length; j++)
{
Random.Shared.NextBytes(buffer);
// NOTE: The result won't be perfectly random, but it should be random enough for tests
results[j] = BitConverter.ToUInt64(buffer) % (to + 1 - from) + from;
}
Add(results[0], results[1]);
}
}
}
}

View file

@ -1,47 +1,44 @@
using NUnit.Framework;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Xunit;
namespace Ryujinx.Tests.Memory namespace Ryujinx.Tests.Memory
{ {
public class Tests public class Tests : IDisposable
{ {
private static readonly ulong _memorySize = MemoryBlock.GetPageSize() * 8; private static readonly ulong _memorySize = MemoryBlock.GetPageSize() * 8;
private MemoryBlock _memoryBlock; private MemoryBlock _memoryBlock;
[SetUp] public Tests()
public void Setup()
{ {
_memoryBlock = new MemoryBlock(_memorySize); _memoryBlock = new MemoryBlock(_memorySize);
} }
[TearDown] public void Dispose()
public void Teardown()
{ {
GC.SuppressFinalize(this);
_memoryBlock.Dispose(); _memoryBlock.Dispose();
} }
[Test] [Fact]
public void Test_Read() public void Test_Read()
{ {
Marshal.WriteInt32(_memoryBlock.Pointer, 0x2020, 0x1234abcd); Marshal.WriteInt32(_memoryBlock.Pointer, 0x2020, 0x1234abcd);
Assert.AreEqual(_memoryBlock.Read<int>(0x2020), 0x1234abcd); Assert.Equal(0x1234abcd, _memoryBlock.Read<int>(0x2020));
} }
[Test] [Fact]
public void Test_Write() public void Test_Write()
{ {
_memoryBlock.Write(0x2040, 0xbadc0de); _memoryBlock.Write(0x2040, 0xbadc0de);
Assert.AreEqual(Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040), 0xbadc0de); Assert.Equal(0xbadc0de, Marshal.ReadInt32(_memoryBlock.Pointer, 0x2040));
} }
[Test] [Fact]
// Memory aliasing tests fail on CI at the moment.
[Platform(Exclude = "MacOsX")]
public void Test_Alias() public void Test_Alias()
{ {
ulong pageSize = MemoryBlock.GetPageSize(); ulong pageSize = MemoryBlock.GetPageSize();
@ -54,14 +51,15 @@ namespace Ryujinx.Tests.Memory
toAlias.UnmapView(backing, pageSize * 3, pageSize); toAlias.UnmapView(backing, pageSize * 3, pageSize);
toAlias.Write(0, 0xbadc0de); toAlias.Write(0, 0xbadc0de);
Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, (int)pageSize), 0xbadc0de); Assert.Equal(0xbadc0de, Marshal.ReadInt32(backing.Pointer, (int)pageSize));
} }
[Test] [Fact]
// Memory aliasing tests fail on CI at the moment.
[Platform(Exclude = "MacOsX")]
public void Test_AliasRandom() public void Test_AliasRandom()
{ {
// Memory aliasing tests fail on CI at the moment.
Skip.If(OperatingSystem.IsMacOS());
ulong pageSize = MemoryBlock.GetPageSize(); ulong pageSize = MemoryBlock.GetPageSize();
int pageBits = (int)ulong.Log2(pageSize); int pageBits = (int)ulong.Log2(pageSize);
ulong blockSize = MemoryBlock.GetPageSize() * 128; ulong blockSize = MemoryBlock.GetPageSize() * 128;
@ -84,7 +82,7 @@ namespace Ryujinx.Tests.Memory
int offset = rng.Next(0, (int)pageSize - sizeof(int)); int offset = rng.Next(0, (int)pageSize - sizeof(int));
toAlias.Write((ulong)((dstPage << pageBits) + offset), 0xbadc0de); toAlias.Write((ulong)((dstPage << pageBits) + offset), 0xbadc0de);
Assert.AreEqual(Marshal.ReadInt32(backing.Pointer, (srcPage << pageBits) + offset), 0xbadc0de); Assert.Equal(0xbadc0de, Marshal.ReadInt32(backing.Pointer, (srcPage << pageBits) + offset));
} }
else else
{ {
@ -93,11 +91,12 @@ namespace Ryujinx.Tests.Memory
} }
} }
[Test] [Fact]
// Memory aliasing tests fail on CI at the moment.
[Platform(Exclude = "MacOsX")]
public void Test_AliasMapLeak() public void Test_AliasMapLeak()
{ {
// Memory aliasing tests fail on CI at the moment.
Skip.If(OperatingSystem.IsMacOS());
ulong pageSize = MemoryBlock.GetPageSize(); ulong pageSize = MemoryBlock.GetPageSize();
ulong size = 100000 * pageSize; // The mappings limit on Linux is usually around 65K, so let's make sure we are above that. ulong size = 100000 * pageSize; // The mappings limit on Linux is usually around 65K, so let's make sure we are above that.
@ -109,7 +108,7 @@ namespace Ryujinx.Tests.Memory
toAlias.MapView(backing, 0, offset, pageSize); toAlias.MapView(backing, 0, offset, pageSize);
toAlias.Write(offset, 0xbadc0de); toAlias.Write(offset, 0xbadc0de);
Assert.AreEqual(0xbadc0de, backing.Read<int>(0)); Assert.Equal(0xbadc0de, backing.Read<int>(0));
toAlias.UnmapView(backing, offset, pageSize); toAlias.UnmapView(backing, offset, pageSize);
} }

View file

@ -1,14 +1,14 @@
using NUnit.Framework;
using Ryujinx.Memory; using Ryujinx.Memory;
using Ryujinx.Memory.Tracking; using Ryujinx.Memory.Tracking;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using Xunit;
namespace Ryujinx.Tests.Memory namespace Ryujinx.Tests.Memory
{ {
public class TrackingTests public class TrackingTests : IDisposable
{ {
private const int RndCnt = 3; private const int RndCnt = 3;
@ -19,17 +19,16 @@ namespace Ryujinx.Tests.Memory
private MemoryTracking _tracking; private MemoryTracking _tracking;
private MockVirtualMemoryManager _memoryManager; private MockVirtualMemoryManager _memoryManager;
[SetUp] public TrackingTests()
public void Setup()
{ {
_memoryBlock = new MemoryBlock(MemorySize); _memoryBlock = new MemoryBlock(MemorySize);
_memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize); _memoryManager = new MockVirtualMemoryManager(MemorySize, PageSize);
_tracking = new MemoryTracking(_memoryManager, PageSize); _tracking = new MemoryTracking(_memoryManager, PageSize);
} }
[TearDown] public void Dispose()
public void Teardown()
{ {
GC.SuppressFinalize(this);
_memoryBlock.Dispose(); _memoryBlock.Dispose();
} }
@ -42,7 +41,7 @@ namespace Ryujinx.Tests.Memory
return handle.Dirty; return handle.Dirty;
} }
[Test] [Fact]
public void SingleRegion() public void SingleRegion()
{ {
RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
@ -66,13 +65,13 @@ namespace Ryujinx.Tests.Memory
bool dirtyAfterUnrelatedReadWrite = handle.Dirty; bool dirtyAfterUnrelatedReadWrite = handle.Dirty;
Assert.False(dirtyAfterUnrelatedReadWrite); // Not dirtied, as the write was to an unrelated address. Assert.False(dirtyAfterUnrelatedReadWrite); // Not dirtied, as the write was to an unrelated address.
Assert.IsNull(readTrackingTriggered); // Hasn't been triggered yet Assert.Null(readTrackingTriggered); // Hasn't been triggered yet
_tracking.VirtualMemoryEvent(0, 4, false); _tracking.VirtualMemoryEvent(0, 4, false);
bool dirtyAfterRelatedRead = handle.Dirty; bool dirtyAfterRelatedRead = handle.Dirty;
Assert.False(dirtyAfterRelatedRead); // Only triggers on write. Assert.False(dirtyAfterRelatedRead); // Only triggers on write.
Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. Assert.Equal(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered.
readTrackingTriggered = null; readTrackingTriggered = null;
_tracking.VirtualMemoryEvent(0, 4, true); _tracking.VirtualMemoryEvent(0, 4, true);
@ -95,7 +94,7 @@ namespace Ryujinx.Tests.Memory
Assert.False(dirtyAfterDispose); // Handle cannot be triggered when disposed Assert.False(dirtyAfterDispose); // Handle cannot be triggered when disposed
} }
[Test] [Fact]
public void OverlappingRegions() public void OverlappingRegions()
{ {
RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16, 0); RegionHandle allHandle = _tracking.BeginTracking(0, PageSize * 16, 0);
@ -127,7 +126,7 @@ namespace Ryujinx.Tests.Memory
{ {
// No handles are dirty. // No handles are dirty.
Assert.False(allHandle.Dirty); Assert.False(allHandle.Dirty);
Assert.IsNull(readTrackingTriggeredAll); Assert.Null(readTrackingTriggeredAll);
for (int j = 0; j < 16; j++) for (int j = 0; j < 16; j++)
{ {
Assert.False(containedHandles[j].Dirty); Assert.False(containedHandles[j].Dirty);
@ -137,7 +136,7 @@ namespace Ryujinx.Tests.Memory
// Only the handle covering the entire range and the relevant contained handle are dirty. // Only the handle covering the entire range and the relevant contained handle are dirty.
Assert.True(allHandle.Dirty); Assert.True(allHandle.Dirty);
Assert.AreEqual(readTrackingTriggeredAll, ((ulong)i * PageSize, 1UL)); // Triggered read tracking Assert.Equal(readTrackingTriggeredAll, ((ulong)i * PageSize, 1UL)); // Triggered read tracking
for (int j = 0; j < 16; j++) for (int j = 0; j < 16; j++)
{ {
if (j == i) if (j == i)
@ -157,10 +156,16 @@ namespace Ryujinx.Tests.Memory
} }
} }
[Test] public static readonly RandomRangeUL2TheoryData TestDataPageAlignment = new(1ul, 65536ul, RndCnt);
public void PageAlignment(
[Values(1ul, 512ul, 2048ul, 4096ul, 65536ul)][Random(1ul, 65536ul, RndCnt)] ulong address, [Theory]
[Values(1ul, 4ul, 1024ul, 4096ul, 65536ul)][Random(1ul, 65536ul, RndCnt)] ulong size) [InlineData(1ul, 1ul)]
[InlineData(512ul, 4ul)]
[InlineData(2048ul, 1024ul)]
[InlineData(4096ul, 4096ul)]
[InlineData(65536ul, 65536ul)]
[MemberData(nameof(TestDataPageAlignment))]
public void PageAlignment(ulong address, ulong size)
{ {
ulong alignedStart = (address / PageSize) * PageSize; ulong alignedStart = (address / PageSize) * PageSize;
ulong alignedEnd = ((address + size + PageSize - 1) / PageSize) * PageSize; ulong alignedEnd = ((address + size + PageSize - 1) / PageSize) * PageSize;
@ -191,7 +196,8 @@ namespace Ryujinx.Tests.Memory
Assert.False(alignedAfterTriggers); Assert.False(alignedAfterTriggers);
} }
[Test, Explicit, Timeout(1000)] // This test used to be skipped unless explicitly executed
[Fact(Timeout = 1000)]
public void Multithreading() public void Multithreading()
{ {
// Multithreading sanity test // Multithreading sanity test
@ -287,12 +293,12 @@ namespace Ryujinx.Tests.Memory
thread.Join(); thread.Join();
} }
Assert.Greater(dirtyFlagReprotects, 10); Assert.True(dirtyFlagReprotects > 10);
Assert.Greater(writeTriggers, 10); Assert.True(writeTriggers >= 10);
Assert.Greater(handleLifecycles, 10); Assert.True(handleLifecycles >= 10);
} }
[Test] [Fact]
public void ReadActionThreadConsumption() public void ReadActionThreadConsumption()
{ {
// Read actions should only be triggered once for each registration. // Read actions should only be triggered once for each registration.
@ -354,10 +360,10 @@ namespace Ryujinx.Tests.Memory
// The action should trigger exactly once for every registration, // The action should trigger exactly once for every registration,
// then we register once after all the threads signalling it cease. // then we register once after all the threads signalling it cease.
Assert.AreEqual(registeredCount, triggeredCount + 1); Assert.Equal(registeredCount, triggeredCount + 1);
} }
[Test] [Fact]
public void DisposeHandles() public void DisposeHandles()
{ {
// Ensure that disposed handles correctly remove their virtual and physical regions. // Ensure that disposed handles correctly remove their virtual and physical regions.
@ -365,11 +371,11 @@ namespace Ryujinx.Tests.Memory
RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
handle.Reprotect(); handle.Reprotect();
Assert.AreEqual(1, _tracking.GetRegionCount()); Assert.Equal(1, _tracking.GetRegionCount());
handle.Dispose(); handle.Dispose();
Assert.AreEqual(0, _tracking.GetRegionCount()); Assert.Equal(0, _tracking.GetRegionCount());
// Two handles, small entirely contains big. // Two handles, small entirely contains big.
// We expect there to be three regions after creating both, one for the small region and two covering the big one around it. // We expect there to be three regions after creating both, one for the small region and two covering the big one around it.
@ -378,33 +384,33 @@ namespace Ryujinx.Tests.Memory
RegionHandle handleSmall = _tracking.BeginTracking(PageSize, PageSize, 0); RegionHandle handleSmall = _tracking.BeginTracking(PageSize, PageSize, 0);
RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4, 0); RegionHandle handleBig = _tracking.BeginTracking(0, PageSize * 4, 0);
Assert.AreEqual(3, _tracking.GetRegionCount()); Assert.Equal(3, _tracking.GetRegionCount());
// After disposing the big region, only the small one will remain. // After disposing the big region, only the small one will remain.
handleBig.Dispose(); handleBig.Dispose();
Assert.AreEqual(1, _tracking.GetRegionCount()); Assert.Equal(1, _tracking.GetRegionCount());
handleSmall.Dispose(); handleSmall.Dispose();
Assert.AreEqual(0, _tracking.GetRegionCount()); Assert.Equal(0, _tracking.GetRegionCount());
} }
[Test] [Fact]
public void ReadAndWriteProtection() public void ReadAndWriteProtection()
{ {
MemoryPermission protection = MemoryPermission.ReadAndWrite; MemoryPermission protection = MemoryPermission.ReadAndWrite;
_memoryManager.OnProtect += (va, size, newProtection) => _memoryManager.OnProtect += (va, size, newProtection) =>
{ {
Assert.AreEqual((0, PageSize), (va, size)); // Should protect the exact region all the operations use. Assert.Equal((0ul, (ulong)PageSize), (va, size)); // Should protect the exact region all the operations use.
protection = newProtection; protection = newProtection;
}; };
RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
// After creating the handle, there is no protection yet. // After creating the handle, there is no protection yet.
Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); Assert.Equal(MemoryPermission.ReadAndWrite, protection);
bool dirtyInitial = handle.Dirty; bool dirtyInitial = handle.Dirty;
Assert.True(dirtyInitial); // Handle starts dirty. Assert.True(dirtyInitial); // Handle starts dirty.
@ -412,7 +418,7 @@ namespace Ryujinx.Tests.Memory
handle.Reprotect(); handle.Reprotect();
// After a reprotect, there is write protection, which will set a dirty flag when any write happens. // After a reprotect, there is write protection, which will set a dirty flag when any write happens.
Assert.AreEqual(MemoryPermission.Read, protection); Assert.Equal(MemoryPermission.Read, protection);
(ulong address, ulong size)? readTrackingTriggered = null; (ulong address, ulong size)? readTrackingTriggered = null;
handle.RegisterAction((address, size) => handle.RegisterAction((address, size) =>
@ -421,7 +427,7 @@ namespace Ryujinx.Tests.Memory
}); });
// Registering an action adds read/write protection. // Registering an action adds read/write protection.
Assert.AreEqual(MemoryPermission.None, protection); Assert.Equal(MemoryPermission.None, protection);
bool dirtyAfterReprotect = handle.Dirty; bool dirtyAfterReprotect = handle.Dirty;
Assert.False(dirtyAfterReprotect); // Handle is no longer dirty. Assert.False(dirtyAfterReprotect); // Handle is no longer dirty.
@ -433,9 +439,9 @@ namespace Ryujinx.Tests.Memory
bool dirtyAfterRead = handle.Dirty; bool dirtyAfterRead = handle.Dirty;
Assert.False(dirtyAfterRead); // Not dirtied, as this was a read. Assert.False(dirtyAfterRead); // Not dirtied, as this was a read.
Assert.AreEqual(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered. Assert.Equal(readTrackingTriggered, (0UL, 4UL)); // Read action was triggered.
Assert.AreEqual(MemoryPermission.Read, protection); // Write protection is still present. Assert.Equal(MemoryPermission.Read, protection); // Write protection is still present.
readTrackingTriggered = null; readTrackingTriggered = null;
@ -446,14 +452,14 @@ namespace Ryujinx.Tests.Memory
bool dirtyAfterWriteAfterRead = handle.Dirty; bool dirtyAfterWriteAfterRead = handle.Dirty;
Assert.True(dirtyAfterWriteAfterRead); // Should be dirty. Assert.True(dirtyAfterWriteAfterRead); // Should be dirty.
Assert.AreEqual(MemoryPermission.ReadAndWrite, protection); // All protection is now be removed from the memory. Assert.Equal(MemoryPermission.ReadAndWrite, protection); // All protection is now be removed from the memory.
Assert.IsNull(readTrackingTriggered); // Read tracking was removed when the action fired, as it can only fire once. Assert.Null(readTrackingTriggered); // Read tracking was removed when the action fired, as it can only fire once.
handle.Dispose(); handle.Dispose();
} }
[Test] [Fact]
public void PreciseAction() public void PreciseAction()
{ {
RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0); RegionHandle handle = _tracking.BeginTracking(0, PageSize, 0);
@ -476,22 +482,22 @@ namespace Ryujinx.Tests.Memory
_tracking.VirtualMemoryEvent(0, 4, false, precise: true); _tracking.VirtualMemoryEvent(0, 4, false, precise: true);
Assert.IsNull(readTrackingTriggered); // Hasn't been triggered - precise action returned true. Assert.Null(readTrackingTriggered); // Hasn't been triggered - precise action returned true.
Assert.AreEqual(preciseTriggered, (0UL, 4UL, false)); // Precise action was triggered. Assert.Equal(preciseTriggered, (0UL, 4UL, false)); // Precise action was triggered.
_tracking.VirtualMemoryEvent(0, 4, true, precise: true); _tracking.VirtualMemoryEvent(0, 4, true, precise: true);
Assert.IsNull(readTrackingTriggered); // Still hasn't been triggered. Assert.Null(readTrackingTriggered); // Still hasn't been triggered.
bool dirtyAfterPreciseActionTrue = handle.Dirty; bool dirtyAfterPreciseActionTrue = handle.Dirty;
Assert.False(dirtyAfterPreciseActionTrue); // Not dirtied - precise action returned true. Assert.False(dirtyAfterPreciseActionTrue); // Not dirtied - precise action returned true.
Assert.AreEqual(preciseTriggered, (0UL, 4UL, true)); // Precise action was triggered. Assert.Equal(preciseTriggered, (0UL, 4UL, true)); // Precise action was triggered.
// Handle is now dirty. // Handle is now dirty.
handle.Reprotect(true); handle.Reprotect(true);
preciseTriggered = null; preciseTriggered = null;
_tracking.VirtualMemoryEvent(4, 4, true, precise: true); _tracking.VirtualMemoryEvent(4, 4, true, precise: true);
Assert.AreEqual(preciseTriggered, (4UL, 4UL, true)); // Precise action was triggered even though handle was dirty. Assert.Equal(preciseTriggered, (4UL, 4UL, true)); // Precise action was triggered even though handle was dirty.
handle.Reprotect(); handle.Reprotect();
handle.RegisterPreciseAction((address, size, write) => handle.RegisterPreciseAction((address, size, write) =>
@ -503,10 +509,10 @@ namespace Ryujinx.Tests.Memory
_tracking.VirtualMemoryEvent(8, 4, true, precise: true); _tracking.VirtualMemoryEvent(8, 4, true, precise: true);
Assert.AreEqual(readTrackingTriggered, (8UL, 4UL)); // Read action triggered, as precise action returned false. Assert.Equal(readTrackingTriggered, (8UL, 4UL)); // Read action triggered, as precise action returned false.
bool dirtyAfterPreciseActionFalse = handle.Dirty; bool dirtyAfterPreciseActionFalse = handle.Dirty;
Assert.True(dirtyAfterPreciseActionFalse); // Dirtied, as precise action returned false. Assert.True(dirtyAfterPreciseActionFalse); // Dirtied, as precise action returned false.
Assert.AreEqual(preciseTriggered, (8UL, 4UL, true)); // Precise action was triggered. Assert.Equal(preciseTriggered, (8UL, 4UL, true)); // Precise action was triggered.
} }
} }
} }