private void PerformLockAction(
int expectedFailureHResult,
bool isBlockingOperation,
Action rwlAction,
Action makeStateChangesOnSuccess)
{
// Blocking operations are inherently nondeterministic in the order in which they are performed, so record a
// pending change before performing the operation. Since the state changes following some blocking operations
// may occur in any order, state verification is only done once there are no pending state changes. Non-blocking
// operations appear atomic and the relevant state changes need to occur in the requested order, so take a
// single lock over the operation and state changes.
if (isBlockingOperation)
{
lock (_rwl)
{
++_pendingStateChanges;
}
}
else
{
Monitor.Enter(_rwl);
}
try
{
ApplicationException ex = null;
bool isHandledCase = false;
try
{
rwlAction();
isHandledCase = true;
}
catch (ApplicationException ex2)
{
ex = ex2;
isHandledCase = true;
}
finally
{
if (!isHandledCase && isBlockingOperation)
{
// Some exception other than ones handled above occurred. Decrement the pending state changes. For
// handled cases, the decrement needs to occur in the same lock that also verifies the exception,
// makes state changes, and verifies the state.
lock (_rwl)
{
--_pendingStateChanges;
// This exception will cause the test to fail, so don't verify state
}
}
}
if (isBlockingOperation)
{
Monitor.Enter(_rwl);
}
try
{
if (isBlockingOperation)
{
// Decrementing the pending state changes needs to occur in the same lock that makes state changes
// and verifies state, in order to guarantee that when there are no pending state changes based on
// the count, all state changes are reflected in the fields as well.
--_pendingStateChanges;
}
Assert.Equal(expectedFailureHResult, ex == null ? 0 : ex.HResult);
if (ex == null)
{
makeStateChangesOnSuccess();
}
if (_pendingStateChanges == 0)
{
VerifyState();
}
}
finally
{
if (isBlockingOperation)
{
Monitor.Exit(_rwl);
}
}
}
finally
{
if (!isBlockingOperation)
{
Monitor.Exit(_rwl);
}
}
}