sharpemu/tests/SharpEmu.Libs.Tests/Pthread/PthreadCondSemanticsTests.cs

83 lines
3.3 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.Pthread;
// POSIX condition variables are edges, not semaphore credits. A signal with no waiter
// must have no effect. This was violated by the previous implementation which persisted
// signals via PendingSignals, causing lock inversions and predicate bypasses.
// See issue #113.
public sealed class PthreadCondSemanticsTests
{
[Fact]
public void PthreadCondState_DoesNotHavePendingSignals()
{
// Verify that PthreadCondState no longer has the PendingSignals property.
// This is a regression test to ensure the POSIX-correct behavior is maintained.
var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic);
Assert.NotNull(stateType);
var pendingSignalsProp = stateType.GetProperty("PendingSignals");
Assert.Null(pendingSignalsProp);
var tryConsumeMethod = stateType.GetMethod("TryConsumePendingSignal");
Assert.Null(tryConsumeMethod);
}
[Fact]
public void PthreadCondSignal_WithNoWaiter_DoesNotPersist()
{
// This test verifies the semantic contract: signal without waiter is a no-op.
// We can't easily test the full pthread flow without the scheduler, but we can
// verify the code path by checking that SignalEpoch advances but no state persists.
var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic);
Assert.NotNull(stateType);
// Create an instance via reflection
var state = Activator.CreateInstance(stateType);
Assert.NotNull(state);
var syncRootProp = stateType.GetProperty("SyncRoot");
var signalEpochProp = stateType.GetProperty("SignalEpoch");
var waitersProp = stateType.GetProperty("Waiters");
Assert.NotNull(syncRootProp);
Assert.NotNull(signalEpochProp);
Assert.NotNull(waitersProp);
var syncRoot = syncRootProp.GetValue(state);
Assert.NotNull(syncRoot);
// Initial state
Assert.Equal(0UL, (ulong)signalEpochProp.GetValue(state)!);
Assert.Equal(0, (int)waitersProp.GetValue(state)!);
// Simulate signal with no waiter (this would have incremented PendingSignals before)
lock (syncRoot)
{
signalEpochProp.SetValue(state, (ulong)signalEpochProp.GetValue(state)! + 1);
// Note: we don't increment PendingSignals because it doesn't exist
}
// Verify epoch advanced but no persistent signal state
Assert.Equal(1UL, (ulong)signalEpochProp.GetValue(state)!);
// A new waiter arriving should see the new epoch but not consume any "pending" signal
// (because there's no such concept anymore)
lock (syncRoot)
{
var observedEpoch = (ulong)signalEpochProp.GetValue(state)!;
waitersProp.SetValue(state, (int)waitersProp.GetValue(state)! + 1);
// Waiter sees epoch=1, will block until epoch changes again
Assert.Equal(1UL, observedEpoch);
Assert.Equal(1, (int)waitersProp.GetValue(state)!);
}
}
}