83 lines
3.3 KiB
C#
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)!);
|
|
}
|
|
}
|
|
}
|