acdream/tests/AcDream.App.Tests/Audio/OpenAlResourceLifetimeTests.cs
Erik 60a1698ce7 refactor(app): compose content effects and audio startup
Move Phase-2 startup behind typed composition/publication boundaries, replace the GameWindow-capturing PhysicsScript gate with a focused deferred source, and own animation-hook registrations reversibly. Make OpenAL construction and teardown transactional so every device, context, source, and buffer prefix is retryable without replay.
2026-07-22 15:36:05 +02:00

168 lines
5.3 KiB
C#

using AcDream.App.Audio;
using Silk.NET.OpenAL;
namespace AcDream.App.Tests.Audio;
public sealed class OpenAlResourceLifetimeTests
{
[Fact]
public void SuccessfulEngineConstructionOwnsAndReleasesEveryNativePrefixOnce()
{
var api = new RecordingApi();
var engine = new OpenAlAudioEngine(new Factory(api));
Assert.True(engine.IsAvailable);
Assert.Equal(20, api.GeneratedSources.Count);
Assert.Equal(16, api.Configured3D.Count);
Assert.Equal(4, api.ConfiguredUi.Count);
engine.Dispose();
engine.Dispose();
Assert.True(engine.IsDisposalComplete);
Assert.Equal(20, api.DeletedSources.Count);
Assert.Equal(
Enumerable.Range(1, 20).Reverse().Select(value => (uint)value),
api.DeletedSources);
Assert.Equal(1, api.ClearCurrentCalls);
Assert.Equal(1, api.DestroyContextCalls);
Assert.Equal(1, api.CloseDeviceCalls);
}
[Fact]
public void ConfigurationFailureRollsBackTheExactGeneratedSourcePrefix()
{
var api = new RecordingApi
{
ConfigureFailureSource = 3,
};
var engine = new OpenAlAudioEngine(new Factory(api));
Assert.False(engine.IsAvailable);
Assert.True(engine.IsDisposalComplete);
Assert.Equal([3u, 2u, 1u], api.DeletedSources);
Assert.Equal(1, api.DestroyContextCalls);
Assert.Equal(1, api.CloseDeviceCalls);
}
[Fact]
public void IncompleteInitializationCleanupRemainsRetryableWithoutReplay()
{
var api = new RecordingApi
{
ConfigureFailureSource = 3,
DeleteFailureSource = 2,
};
OpenAlInitializationException failure = Assert.Throws<OpenAlInitializationException>(
() => new OpenAlAudioEngine(new Factory(api)));
Assert.False(failure.IsCleanupComplete);
Assert.Equal([3u, 1u], api.DeletedSources);
Assert.Equal(0, api.DestroyContextCalls);
Assert.Equal(0, api.CloseDeviceCalls);
api.DeleteFailureSource = null;
failure.RetryCleanup();
failure.RetryCleanup();
Assert.True(failure.IsCleanupComplete);
Assert.Equal([3u, 1u, 2u], api.DeletedSources);
Assert.Equal(1, api.DeletedSources.Count(source => source == 3u));
Assert.Equal(1, api.DeletedSources.Count(source => source == 1u));
Assert.Equal(1, api.DestroyContextCalls);
Assert.Equal(1, api.CloseDeviceCalls);
}
[Fact]
public void ContextCreationFailureClosesTheDeviceBeforeReturningUnavailable()
{
var api = new RecordingApi { ContextResult = 0 };
var engine = new OpenAlAudioEngine(new Factory(api));
Assert.False(engine.IsAvailable);
Assert.True(engine.IsDisposalComplete);
Assert.Empty(api.GeneratedSources);
Assert.Equal(0, api.DestroyContextCalls);
Assert.Equal(1, api.CloseDeviceCalls);
}
private sealed class Factory(IOpenAlResourceApi api) : IOpenAlResourceApiFactory
{
public IOpenAlResourceApi Create() => api;
}
private sealed class RecordingApi : IOpenAlResourceApi
{
private uint _nextSource = 1;
public AL? AudioApi => null;
public ALContext? ContextApi => null;
public nint DeviceResult { get; set; } = 101;
public nint ContextResult { get; set; } = 202;
public uint? ConfigureFailureSource { get; set; }
public uint? DeleteFailureSource { get; set; }
public List<uint> GeneratedSources { get; } = [];
public List<uint> Configured3D { get; } = [];
public List<uint> ConfiguredUi { get; } = [];
public List<uint> DeletedSources { get; } = [];
public int ClearCurrentCalls { get; private set; }
public int DestroyContextCalls { get; private set; }
public int CloseDeviceCalls { get; private set; }
public nint OpenDevice() => DeviceResult;
public nint CreateContext(nint device) => ContextResult;
public bool MakeContextCurrent(nint context)
{
if (context == 0)
ClearCurrentCalls++;
return true;
}
public uint GenerateSource()
{
uint source = _nextSource++;
GeneratedSources.Add(source);
return source;
}
public void Configure3DSource(uint source)
{
Configured3D.Add(source);
ThrowIfConfiguredFailure(source);
}
public void ConfigureUiSource(uint source)
{
ConfiguredUi.Add(source);
ThrowIfConfiguredFailure(source);
}
public void SelectRetailDistanceModel() { }
public void StopSource(uint source) { }
public void DeleteSource(uint source)
{
if (DeleteFailureSource == source)
throw new InvalidOperationException("synthetic delete failure");
DeletedSources.Add(source);
}
public void DeleteBuffer(uint buffer) { }
public void DestroyContext(nint context) => DestroyContextCalls++;
public void CloseDevice(nint device) => CloseDeviceCalls++;
private void ThrowIfConfiguredFailure(uint source)
{
if (ConfigureFailureSource == source)
throw new InvalidOperationException("synthetic configure failure");
}
}
}