Friday 25 November 2011

Wrapping locking structures in tests

Over the last week I’ve been writing some locking code around a task queue, and wondering generally how to test whether locks are taken.

The straightforward thing to do is to take the locking calls as a seam and introduce a proxy, behind which I can add my own behaviour. I’ve not thought about the practicality of what followed once I started – it may be supremely impractical – but out of interest’s sake I thought I’d see where it went.

I’ve been working with ReaderWriterLockSlim a fair amount, so that was a natural place to start. I added a hook that meant I could introduce a new implementation for EnterWriteLock. There are a couple of wrinkles over the work I did with static classes recently – in this case I’m explicitly proxying a class instance, so I can’t rely on static hooks into external code, and default them with a static method group.

Neither can I specify a lambda, as it would rely on calling methods on an instance that wasn’t available at compile time, leading to errors like “cannot access non-static member in static context”. This just means I need to resolve any default implementation at runtime.

namespace Threading.Abstractions
{
    public class ReaderWriterLockSlim
    {
        private readonly System.Threading.ReaderWriterLockSlim _lock = 
            new System.Threading.ReaderWriterLockSlim();

        public ReaderWriterLockSlim()
        {
            EnterWriteLockImpl = (Action)(() => _lock.EnterWriteLock());
        }

        public void EnterWriteLock()
        {
            EnterWriteLockImpl();
        }

        public Action EnterWriteLockImpl;
    }
}
Then in a test I might replace the EnterWriteLock implementation:
[TestFixture]
public class Tests
{
    [Test]
    public void SimpleLockTest()
    {
        var locker = new Threading.Abstractions.ReaderWriterLockSlim();

        var count = 0;
        locker.EnterWriteLockImpl = (Action)(() => count++);

        locker.EnterWriteLock();

        Assert.AreEqual(1, count);
    }
}

So, that's good, I can tell if the lock has been taken or not. I can take it a little bit further though. One thing I might want to do is check log which locks had been taken when – something that might notionally be useful even in production code. If I add an OnEnterWriteLock hook then I can log an entry if needed:

namespace Threading.Abstractions
{
    public class ReaderWriterLockSlim
    {
        private readonly System.Threading.ReaderWriterLockSlim _lock = 
            new System.Threading.ReaderWriterLockSlim();

        public ReaderWriterLockSlim()
        {
            EnterWriteLockImpl = (Action)(() => _lock.EnterWriteLock());
        }

        public Action OnEnterWriteLock;

        public Action AfterEnterWriteLock;

        public void EnterWriteLock()
        {
            if(OnEnterWriteLock != null) OnEnterWriteLock();

            EnterWriteLockImpl();

            if (AfterEnterWriteLock != null) AfterEnterWriteLock();
        }

        public Action EnterWriteLockImpl;
    }
}

After that I can add any logging that I might want to – I could append part of the stack trace for example; the name of the object that’s being called; or any number of things that help us track how it’s used:

[TestFixture]
public class Tests
{
    [Test]
    public void LockTest()
    {
        var locker = new Threading.Abstractions.ReaderWriterLockSlim();

        locker.OnEnterWriteLock = 
            (Action) (() => Console.WriteLine("Entering write lock") );
        locker.AfterEnterWriteLock = 
            (Action)(() => Console.WriteLine("Entered write lock"));

        locker.EnterWriteLock();
    }
}

Another interesting thing to try might be to actually create a timeline of all locks being taken and released. If I create a simple lock log:

namespace Threading.Abstractions
{
    public static class LockHistory
    {
        private static object locker = new object();

        public readonly static 
            System.Collections.Generic.List Entries = 
                new System.Collections.Generic.List();

        public static void Add(string lockEvent)
        {
            lock (locker) Entries.Add(lockEvent);
        }
    }
}

I can then add a log entry every time a lock is taken or released, and play that log back at the end of the test.

[TestFixture]
public class Tests
{
    [Test]
    public void LockTest()
    {
        var locker = new Threading.Abstractions.ReaderWriterLockSlim();

        locker.OnEnterWriteLock = 
            (Action) (() => Threading.Abstractions.LockHistory.Add("Entering Write Lock") );
        locker.AfterEnterWriteLock = 
            (Action)(() => Console.WriteLine("Entered write lock"));
        
        locker.EnterWriteLock();

        foreach (var entry in Threading.Abstractions.LockHistory.Entries)
        {
            Console.WriteLine(entry);
        }
    }
}

The log history could then be parsed or analysed in some way. All good fun, although, as I say -- I'm not sure how practical it might be.

No comments: