Skip to content

Thread Example

Ian FY edited this page Sep 10, 2024 · 2 revisions

This page will eventually cover the Thread example code in detail.

using Moq;
using System;
using System.Threading;

namespace IFY.Shimr.Examples;

#region Production code

/// <summary>
/// Part of an application that wakes every interval or when needed.
/// </summary>
public class IntervalAction(IThreadingFactory threadFactory)
{
    public bool IsRunning => _runningBit > 0;
    private int _runningBit;

    private ICancellationTokenSource? _tokenSource;

    /// <summary>
    /// Start the service, if not already
    /// </summary>
    public bool Start(Action action, TimeSpan interval)
    {
        var wasRunning = Interlocked.Exchange(ref _runningBit, 1);
        if (wasRunning > 0)
        {
            return false;
        }

        _tokenSource = threadFactory.NewTokenSource();

        threadFactory.NewThread(() =>
        {
            try
            {
                while (IsRunning)
                {
                    action();

                    _tokenSource.Token.WaitHandle.WaitOne(interval);
                    if (_tokenSource.IsCancellationRequested)
                    {
                        _tokenSource = threadFactory.NewTokenSource();
                    }
                }
            }
            finally
            {
                _runningBit = 0;
            }
        }).Start();

        return true;
    }

    /// <summary>
    /// Stop the service without invoking again
    /// </summary>
    public void Stop()
    {
        _runningBit = 0;
        _tokenSource?.Cancel();
    }

    /// <summary>
    /// Stop sleeping and invoke again
    /// </summary>
    public void Interrupt()
    {
        _tokenSource?.Cancel();
    }
}

#endregion Production code

#region Shims

public interface IThreadingFactory
{
    [ConstructorShim(typeof(Thread))]
    IThread NewThread(ThreadStart action);

    [ConstructorShim(typeof(CancellationTokenSource))]
    ICancellationTokenSource NewTokenSource();
}

public interface IThread
{
    void Start();
}

public interface ICancellationTokenSource
{
    bool IsCancellationRequested { get; }

    ICancellationToken Token { get; }

    void Cancel();
}

public interface ICancellationToken
{
    IWaitHandle WaitHandle { get; }
}

public interface IWaitHandle
{
    bool WaitOne(TimeSpan timeout);
}

#endregion Shims

#region Tests

[TestClass]
public class IntervalActionExample
{
    [TestMethod]
    public void Really_works()
    {
        // Arrange
        var threadFactory = ShimBuilder.Create<IThreadingFactory>();

        var inst = new IntervalAction(threadFactory);

        var count = 0;
        void action()
        {
            ++count;
            if (count > 2)
            {
                inst.Stop();
            }
        }

        // Act
        inst.Start(action, TimeSpan.FromSeconds(1));
        while (inst.IsRunning)
        {
            // Wait
        }

        // Assert
        Assert.AreEqual(3, count);
    }

    [TestMethod]
    public void Stop__Before_Start__Noop()
    {
        // Arrange
        var threadFactoryMock = new Mock<IThreadingFactory>();

        var inst = new IntervalAction(threadFactoryMock.Object);

        // Act
        inst.Stop();
    }

    [TestMethod]
    public void Interrupt__Before_Start__Noop()
    {
        // Arrange
        var threadFactoryMock = new Mock<IThreadingFactory>();

        var inst = new IntervalAction(threadFactoryMock.Object);

        // Act
        inst.Interrupt();
    }

    [TestMethod]
    public void Interrupt__Cancels_token()
    {
        // Arrange
        var threadFactoryMock = new Mock<IThreadingFactory>();
        var threadMock = new Mock<IThread>();
        var tokenSourceMock = new Mock<ICancellationTokenSource>();
        var tokenMock = new Mock<ICancellationToken>();

        threadFactoryMock.Setup(m => m.NewThread(It.IsAny<ThreadStart>()))
            .Returns(threadMock.Object);
        threadFactoryMock.Setup(m => m.NewTokenSource())
            .Returns(tokenSourceMock.Object);
        tokenSourceMock.SetupGet(m => m.Token)
            .Returns(tokenMock.Object);

        var inst = new IntervalAction(threadFactoryMock.Object);

        // Act
        inst.Start(() => { }, TimeSpan.MinValue);
        inst.Interrupt();

        // Assert
        tokenSourceMock.Verify(m => m.Cancel(), Times.Once);
    }

    [TestMethod]
    public void Start__Invokes_action_before_sleeping()
    {
        // Arrange
        var threadFactoryMock = new Mock<IThreadingFactory>();
        var threadMock = new Mock<IThread>();
        var tokenSourceMock = new Mock<ICancellationTokenSource>();
        var tokenMock = new Mock<ICancellationToken>();
        var waitHandleMock = new Mock<IWaitHandle>();

        var inst = new IntervalAction(threadFactoryMock.Object);

        ThreadStart? threadAction = null;
        threadFactoryMock.Setup(m => m.NewThread(It.IsAny<ThreadStart>()))
            .Returns<ThreadStart>((a) =>
            {
                threadAction = a;
                return threadMock.Object;
            });

        waitHandleMock.Setup(m => m.WaitOne(TimeSpan.FromSeconds(5)))
            .Returns(() =>
            {
                inst.Stop();
                return true;
            });
        tokenMock.SetupGet(m => m.WaitHandle)
            .Returns(waitHandleMock.Object);
        tokenSourceMock.SetupGet(m => m.IsCancellationRequested)
            .Returns(false);
        tokenSourceMock.SetupGet(m => m.Token)
            .Returns(tokenMock.Object);
        threadFactoryMock.Setup(m => m.NewTokenSource())
            .Returns(tokenSourceMock.Object);

        var count = 0;
        void action()
        {
            ++count;
        }

        // Act
        inst.Start(action, TimeSpan.FromSeconds(5));
        threadAction!.Invoke();

        // Assert
        Assert.AreEqual(1, count);
    }

    [TestMethod]
    public void Start__Twice__Only_processes_once()
    {
        // Arrange
        var threadFactoryMock = new Mock<IThreadingFactory>();
        var threadMock = new Mock<IThread>();

        threadFactoryMock.Setup(m => m.NewThread(It.IsAny<ThreadStart>()))
            .Returns(threadMock.Object);

        var inst = new IntervalAction(threadFactoryMock.Object);

        // Act
        inst.Start(() => { }, TimeSpan.MinValue);
        inst.Start(() => { }, TimeSpan.MinValue);

        // Assert
        threadMock.Verify(m => m.Start(), Times.Once);
    }
}

#endregion Tests
Clone this wiki locally