Add unit test for pattern replacement

The previous method was fine, not sure what the underlying issue is. The unit tests assert that the sequence is fully replaced.
This commit is contained in:
Kurt
2026-01-14 18:59:38 -06:00
parent ef4703d3b3
commit fd37fa4397
3 changed files with 142 additions and 57 deletions

View File

@@ -41,8 +41,6 @@ private static void ChangeToHostTown(this IVillagerOrigin visit, IVillagerOrigin
{
var hostTown = host.GetTownIdentity();
var visitTown = visit.GetTownIdentity();
if (hostTown.SequenceEqual(visitTown))
return;
visitData.ReplaceOccurrences(visitTown, hostTown);
}
@@ -50,8 +48,6 @@ private static void ChangeToHostPlayer(this IVillagerOrigin visit, IVillagerOrig
{
var hostPlayer = host.GetPlayerIdentity();
var visitPlayer = visit.GetPlayerIdentity();
if (hostPlayer.SequenceEqual(visitPlayer))
return;
visitData.ReplaceOccurrences(visitPlayer, hostPlayer);
}
}

View File

@@ -7,67 +7,25 @@ namespace NHSE.Core;
/// </summary>
public static class ArrayUtil
{
//public static int ReplaceOccurrences(this Span<byte> array, ReadOnlySpan<byte> pattern, ReadOnlySpan<byte> swap)
//{
// int count = 0;
// int ofs = 0;
// while (true)
// {
// var index = array[ofs..].IndexOf(pattern);
// if (index == -1)
// return count;
// ofs += index;
//
// swap.CopyTo(array[ofs..]);
// ofs += swap.Length; // skip past swapped data
// ++count;
// }
//}
public static int ReplaceOccurrences(this Span<byte> array, ReadOnlySpan<byte> pattern, ReadOnlySpan<byte> swap)
{
if (pattern.Length != swap.Length)
return -1;
if (pattern.SequenceEqual(swap))
return 0;
int count = 0;
int ofs = 0;
while (true)
{
int ofs = IndexOfBytes(array, pattern);
if (ofs == -1)
var index = array[ofs..].IndexOf(pattern);
if (index == -1)
return count;
ofs += index;
swap.CopyTo(array[ofs..]);
ofs += swap.Length; // skip past swapped data
++count;
}
}
/// <summary>
/// Finds a provided <see cref="pattern"/> within the supplied <see cref="array"/>.
/// </summary>
/// <param name="array">Array to look in</param>
/// <param name="pattern">Pattern to look for</param>
/// <param name="startIndex">Starting offset to look from</param>
/// <param name="length">Amount of entries to look through</param>
/// <returns>Index the pattern occurs at; if not found, returns -1.</returns>
public static int IndexOfBytes(ReadOnlySpan<byte> array, ReadOnlySpan<byte> pattern, int startIndex = 0, int length = -1)
{
int len = pattern.Length;
int endIndex = length > 0
? startIndex + length
: array.Length - len - startIndex;
endIndex = Math.Min(array.Length - pattern.Length, endIndex);
int i = startIndex;
int j = 0;
while (true)
{
if (pattern[j] != array[i + j])
{
if (++i == endIndex)
return -1;
j = 0;
}
else if (++j == len)
{
return i;
}
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using NHSE.Core;
using Xunit;
namespace NHSE.Tests;
public class ArrayUtilTests
{
[Fact]
public void ReplaceOccurrences_WhenPatternNotFound_ReturnsZero()
{
byte[] array = [0x01, 0x02, 0x03, 0x04, 0x05];
byte[] pattern = [0xAA, 0xBB];
byte[] swap = [0xCC, 0xDD];
var count = array.ReplaceOccurrences(pattern, swap);
count.Should().Be(0);
array.Should().BeEquivalentTo([0x01, 0x02, 0x03, 0x04, 0x05]);
}
[Fact]
public void ReplaceOccurrences_WhenSingleOccurrence_ReplacesAndReturnsOne()
{
byte[] array = [0x01, 0xAA, 0xBB, 0x04, 0x05];
byte[] pattern = [0xAA, 0xBB];
byte[] swap = [0xCC, 0xDD];
var count = array.ReplaceOccurrences(pattern, swap);
count.Should().Be(1);
array.Should().BeEquivalentTo([0x01, 0xCC, 0xDD, 0x04, 0x05]);
}
[Fact]
public void ReplaceOccurrences_WhenMultipleOccurrences_ReplacesAllAndReturnsCount()
{
byte[] array = [0xAA, 0xBB, 0x03, 0xAA, 0xBB, 0x06, 0xAA, 0xBB];
byte[] pattern = [0xAA, 0xBB];
byte[] swap = [0xCC, 0xDD];
var count = array.ReplaceOccurrences(pattern, swap);
count.Should().Be(3);
array.Should().BeEquivalentTo([0xCC, 0xDD, 0x03, 0xCC, 0xDD, 0x06, 0xCC, 0xDD]);
}
[Fact]
public void ReplaceOccurrences_WhenConsecutiveOccurrences_ReplacesAll()
{
byte[] array = [0xAA, 0xBB, 0xAA, 0xBB, 0xAA, 0xBB];
byte[] pattern = [0xAA, 0xBB];
byte[] swap = [0xCC, 0xDD];
var count = array.ReplaceOccurrences(pattern, swap);
count.Should().Be(3);
array.Should().BeEquivalentTo([0xCC, 0xDD, 0xCC, 0xDD, 0xCC, 0xDD]);
}
[Fact]
public void ReplaceOccurrences_WhenSwapContainsPattern_DoesNotCauseInfiniteLoop()
{
// Swap contains the original pattern - must skip past swapped data
byte[] array = [0x01, 0xAA, 0xBB, 0x04];
byte[] pattern = [0xAA, 0xBB];
byte[] swap = [0xAA, 0xBB]; // Same as pattern
var count = array.ReplaceOccurrences(pattern, swap);
count.Should().Be(0);
array.Should().BeEquivalentTo([0x01, 0xAA, 0xBB, 0x04]);
}
[Fact]
public void ReplaceOccurrences_WhenLargeFileWithRandomPlacements_ReplacesAllOccurrences()
{
const int fileSize = 1024 * 1024; // 1 MB
const int sequenceLength = 0x13; // 19 bytes
const int seed = 42;
var random = new Random(seed);
Span<byte> file = new byte[fileSize];
random.NextBytes(file);
// Create two distinct random sequences
Span<byte> patternA = stackalloc byte[sequenceLength];
Span<byte> patternB = stackalloc byte[sequenceLength];
random.NextBytes(patternA);
random.NextBytes(patternB);
// Ensure patterns are different
patternB[0] = (byte)(patternA[0] ^ 0xFF);
// Insert pattern A at random non-overlapping offsets
var insertedOffsets = new List<int>();
int currentPosition = 0;
while (currentPosition + sequenceLength <= fileSize)
{
// Random gap between 0 and 1000 bytes, then add sequence length to avoid overlap
int gap = random.Next(0, 1000);
int nextOffset = currentPosition + gap;
if (nextOffset + sequenceLength > fileSize)
break;
// Insert pattern A at this offset
patternA.CopyTo(file[nextOffset..]);
insertedOffsets.Add(nextOffset);
// Move past the inserted sequence
currentPosition = nextOffset + sequenceLength;
}
// Run the replacement
var count = file.ReplaceOccurrences(patternA, patternB);
// Verify count matches
count.Should().Be(insertedOffsets.Count);
// Verify each offset now contains pattern B
foreach (var offset in insertedOffsets)
{
var actual = file.Slice(offset, sequenceLength);
actual.SequenceEqual(patternB).Should().BeTrue(because: $"offset {offset} should have been replaced");
}
}
}