Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions Containers.Test/DelayLineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Containers.Tests;

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class DelayLineTests
{
[TestMethod]
public void Constructor_NonPositiveCapacity_Throws()
{
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new DelayLine(0));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new DelayLine(-4));
}

[TestMethod]
public void Capacity_MatchesRequestedMaxDelay()
{
DelayLine line = new(100);
Assert.AreEqual(100, line.Capacity);
}

[TestMethod]
public void NewDelayLine_ReadsZero()
{
DelayLine line = new(8);
for (int delay = 0; delay <= line.Capacity; delay++)
{
Assert.AreEqual(0f, line.Read(delay));
}
}

[TestMethod]
public void Read_ReturnsSampleNSamplesInThePast()
{
DelayLine line = new(8);
line.Write(1f);
line.Write(2f);
line.Write(3f);

// Delay 0 == most recent, delay 2 == oldest of the three writes.
Assert.AreEqual(3f, line.Read(0));
Assert.AreEqual(2f, line.Read(1));
Assert.AreEqual(1f, line.Read(2));
}

[TestMethod]
public void Process_OutputsInputFromExactlyDelaySamplesEarlier()
{
DelayLine line = new(4);
// Process(x, 4) yields x[n - 4]: zero until the line has been primed with 4 samples.
Assert.AreEqual(0f, line.Process(10f, 4));
Assert.AreEqual(0f, line.Process(20f, 4));
Assert.AreEqual(0f, line.Process(30f, 4));
Assert.AreEqual(0f, line.Process(40f, 4));
Assert.AreEqual(10f, line.Process(50f, 4));
Assert.AreEqual(20f, line.Process(60f, 4));
}

[TestMethod]
public void Process_ZeroDelay_ReturnsInput()
{
DelayLine line = new(4);
Assert.AreEqual(7f, line.Process(7f, 0));
Assert.AreEqual(9f, line.Process(9f, 0));
}

[TestMethod]
public void Read_OutOfRange_Throws()
{
DelayLine line = new(8);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => line.Read(-1));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => line.Read(9));
}

[TestMethod]
public void ReadInterpolated_HalfwayBetweenSamples_AveragesNeighbours()
{
DelayLine line = new(8);
line.Write(0f);
line.Write(10f);

// delay 0 -> 10 (newest), delay 1 -> 0 (older). 0.5 interpolates halfway.
Assert.AreEqual(5f, line.ReadInterpolated(0.5f), 1e-6f);
}

[TestMethod]
public void ReadInterpolated_IntegerDelay_MatchesRead()
{
DelayLine line = new(8);
line.Write(3f);
line.Write(7f);
line.Write(11f);

Assert.AreEqual(line.Read(1), line.ReadInterpolated(1f), 1e-6f);
}

[TestMethod]
public void ReadInterpolated_OutOfRange_Throws()
{
DelayLine line = new(8);
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => line.ReadInterpolated(-0.5f));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => line.ReadInterpolated(8.5f));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => line.ReadInterpolated(float.NaN));
}

[TestMethod]
public void Write_FlushesDenormalsToZero()
{
DelayLine line = new(4);
float denormal = float.Epsilon; // smallest subnormal float, well below the threshold
line.Write(denormal);
Assert.AreEqual(0f, line.Read(0), "Denormal input must be flushed to zero.");
}

[TestMethod]
public void Write_KeepsNormalSmallValues()
{
DelayLine line = new(4);
const float audible = 1e-6f; // ~ -120 dBFS, a normal float that must be preserved
line.Write(audible);
Assert.AreEqual(audible, line.Read(0));
}

[TestMethod]
public void Clear_ZeroesAllSamples()
{
DelayLine line = new(4);
line.Write(1f);
line.Write(2f);
line.Clear();

for (int delay = 0; delay <= line.Capacity; delay++)
{
Assert.AreEqual(0f, line.Read(delay));
}
}

[TestMethod]
public void WrapAround_LongStreamReadsCorrectDelay()
{
DelayLine line = new(3);
float previous = 0f;
for (int i = 1; i <= 1000; i++)
{
// Process(i, 1) returns the value written on the previous call (i - 1), 0 on the first.
float output = line.Process(i, 1);
Assert.AreEqual(i - 1, output);
previous = output;
}

Assert.AreEqual(999f, previous);
}
}
158 changes: 158 additions & 0 deletions Containers.Test/SpscRingBufferTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Containers.Tests;

using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class SpscRingBufferTests
{
[TestMethod]
public void Constructor_NonPositiveCapacity_Throws()
{
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new SpscRingBuffer<int>(0));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new SpscRingBuffer<int>(-1));
}

[TestMethod]
public void Capacity_IsAtLeastRequested()
{
SpscRingBuffer<int> buffer = new(5);
Assert.IsTrue(buffer.Capacity >= 5, "Usable capacity must be at least the requested amount.");
}

[TestMethod]
public void NewBuffer_IsEmpty()
{
SpscRingBuffer<int> buffer = new(4);
Assert.IsTrue(buffer.IsEmpty);
Assert.AreEqual(0, buffer.Count);
Assert.IsFalse(buffer.TryDequeue(out _));
}

[TestMethod]
public void EnqueueDequeue_PreservesFifoOrder()
{
SpscRingBuffer<int> buffer = new(8);
for (int i = 0; i < 5; i++)
{
Assert.IsTrue(buffer.TryEnqueue(i));
}

for (int i = 0; i < 5; i++)
{
Assert.IsTrue(buffer.TryDequeue(out int value));
Assert.AreEqual(i, value);
}

Assert.IsTrue(buffer.IsEmpty);
}

[TestMethod]
public void TryEnqueue_WhenFull_ReturnsFalse()
{
SpscRingBuffer<int> buffer = new(4);
int enqueued = 0;
while (buffer.TryEnqueue(enqueued))
{
enqueued++;
}

Assert.IsTrue(enqueued >= 4, "Should accept at least the requested capacity before reporting full.");
Assert.IsFalse(buffer.TryEnqueue(999));
}

[TestMethod]
public void TryPeek_DoesNotRemoveElement()
{
SpscRingBuffer<int> buffer = new(4);
buffer.TryEnqueue(42);

Assert.IsTrue(buffer.TryPeek(out int peeked));
Assert.AreEqual(42, peeked);
Assert.AreEqual(1, buffer.Count);

Assert.IsTrue(buffer.TryDequeue(out int dequeued));
Assert.AreEqual(42, dequeued);
}

[TestMethod]
public void WrapAround_ReusesSlotsCorrectly()
{
SpscRingBuffer<int> buffer = new(4);

// Cycle through many more items than capacity to force repeated wraparound.
for (int i = 0; i < 1000; i++)
{
Assert.IsTrue(buffer.TryEnqueue(i));
Assert.IsTrue(buffer.TryDequeue(out int value));
Assert.AreEqual(i, value);
}

Assert.IsTrue(buffer.IsEmpty);
}

[TestMethod]
public async Task ConcurrentProducerConsumer_TransfersAllItemsInOrder()
{
const int itemCount = 1_000_000;
SpscRingBuffer<int> buffer = new(1024);

Task producer = Task.Run(() =>
{
int produced = 0;
while (produced < itemCount)
{
if (buffer.TryEnqueue(produced))
{
produced++;
}
else
{
Thread.SpinWait(1);
}
}
});

Task<bool> consumer = Task.Run(() =>
{
int expected = 0;
while (expected < itemCount)
{
if (buffer.TryDequeue(out int value))
{
if (value != expected)
{
return false;
}

expected++;
}
else
{
Thread.SpinWait(1);
}
}

return true;
});

await Task.WhenAll(producer, consumer).ConfigureAwait(false);
Assert.IsTrue(await consumer.ConfigureAwait(false), "All items must be received exactly once and in order.");
Assert.IsTrue(buffer.IsEmpty);
}

[TestMethod]
public void Dequeue_ReferenceType_ReleasesReference()
{
SpscRingBuffer<string> buffer = new(4);
buffer.TryEnqueue("hello");
Assert.IsTrue(buffer.TryDequeue(out string? value));
Assert.AreEqual("hello", value);
Assert.IsTrue(buffer.IsEmpty);
}
}
Loading
Loading