• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DemoBytom / DemoEngine / 21911211831

11 Feb 2026 03:15PM UTC coverage: 29.787% (+3.5%) from 26.251%
21911211831

Pull #499

coveralls.net

DemoBytom
Unit test for `StaThreadService`

Unit test for `StaThreadService` that tests, that the requests send via `Channel<T>` to `StaThreadService`, are indeed executed on it's main thread.
Producer task sends 5 consecutive requests, ensuring that they are sent from a thread different to the STA main thread. Then, when invoked, an assertion is performed to validate that invocation occurs on STA main thread.
Test takes into account that STA thread apartment can only be enforced on Windows, so it validates that the thread name is `"Main STA thread"` on Windows, and `"Main thread"` on other platforms.
Pull Request #499: Feature/sta thread service refactor

630 of 2115 relevant lines covered (29.79%)

0.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.5
/src/Demo.Engine.Core/Features/StaThread/StaThreadService.cs
1
// Copyright © Michał Dembski and contributors.
2
// Distributed under MIT license. See LICENSE file in the root for more information.
3

4
using System.Runtime.InteropServices;
5
using System.Threading.Channels;
6
using Demo.Engine.Core.Interfaces;
7
using Demo.Engine.Core.Interfaces.Platform;
8
using Demo.Engine.Core.Interfaces.Rendering;
9
using Microsoft.Extensions.Hosting;
10

11
namespace Demo.Engine.Core.Features.StaThread;
12

13
internal sealed class StaThreadService
14
    : IStaThreadService,
15
      IDisposable
16
{
17
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
18
    private readonly ChannelReader<StaThreadRequests> _channelReader;
19
    private readonly IMainLoopLifetime _mainLoopLifetime;
20
    private bool _disposedValue;
21

22
    public Task ExecutingTask { get; }
1✔
23
    public bool IsRunning { get; private set; }
1✔
24

25
    public StaThreadService(
1✔
26
        IHostApplicationLifetime hostApplicationLifetime,
1✔
27
        IRenderingEngine renderingEngine,
1✔
28
        IOSMessageHandler osMessageHandler,
1✔
29
        ChannelReader<StaThreadRequests> channelReader,
1✔
30
        IMainLoopLifetime mainLoopLifetime)
1✔
31
    {
32
        _hostApplicationLifetime = hostApplicationLifetime;
1✔
33
        _channelReader = channelReader;
1✔
34
        _mainLoopLifetime = mainLoopLifetime;
1✔
35
        IsRunning = true;
1✔
36
        ExecutingTask = RunSTAThread(
1✔
37
            renderingEngine,
1✔
38
            osMessageHandler);
1✔
39
    }
1✔
40

41
    private Task RunSTAThread(
42
        IRenderingEngine renderingEngine,
43
        IOSMessageHandler osMessageHandler)
44
    {
45
        var tcs = new TaskCompletionSource();
1✔
46
        var thread = new Thread(()
1✔
47
            =>
1✔
48
        {
1✔
49
            try
1✔
50
            {
1✔
51
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(
1✔
52
                    _hostApplicationLifetime.ApplicationStopping,
1✔
53
                    _mainLoopLifetime.Token);
1✔
54

1✔
55
                SingleThreadedSynchronizationContextChannel.Await(async ()
1✔
56
                    => await STAThread(
1✔
57
                        renderingEngine: renderingEngine,
1✔
58
                        osMessageHandler: osMessageHandler,
1✔
59
                        cancellationToken: cts.Token));
1✔
60

1✔
61
                tcs.SetResult();
62
            }
63
            catch (OperationCanceledException)
1✔
64
            {
1✔
65
                tcs.SetResult();
1✔
66
            }
1✔
67
            catch (Exception ex)
68
            {
1✔
69
                tcs.SetException(ex);
70
            }
71
            IsRunning = false;
1✔
72
            _mainLoopLifetime.Cancel();
1✔
73
        });
1✔
74
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
75
        {
76
            //Can only by set on the Windows machine. Doesn't work on Linux/MacOS
77
            thread.SetApartmentState(ApartmentState.STA);
1✔
78
            thread.Name = "Main STA thread";
1✔
79
        }
80
        else
81
        {
82
            thread.Name = "Main thread";
83
        }
84

85
        thread.Start();
1✔
86

87
        return tcs.Task;
1✔
88
    }
89

90
    private async Task STAThread(
91
        IRenderingEngine renderingEngine,
92
        IOSMessageHandler osMessageHandler,
93
        CancellationToken cancellationToken)
94
    {
95
        var doEventsOk = true;
1✔
96

97
        await foreach (var staAction in _channelReader
98
            .ReadAllAsync(cancellationToken)
1✔
99
            .WithCancellation(cancellationToken))
1✔
100
        {
101
            switch (staAction)
102
            {
103
                case StaThreadRequests.DoEventsOkRequest doEventsOkRequest:
104
                    doEventsOk &= doEventsOkRequest.Invoke(renderingEngine, osMessageHandler);
105
                    break;
106

107
                default:
108
                    _ = staAction.Invoke(renderingEngine, osMessageHandler);
1✔
109
                    break;
110
            }
111

112
            if (!doEventsOk || !IsRunning || cancellationToken.IsCancellationRequested)
113
            {
114
                break;
115
            }
116
        }
117
    }
118

119
    private void Dispose(bool disposing)
120
    {
121
        if (!_disposedValue)
1✔
122
        {
123
            if (disposing)
1✔
124
            {
125
            }
126

127
            _disposedValue = true;
1✔
128
        }
129
    }
1✔
130

131
    public void Dispose()
132
    {
133
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
134
        Dispose(disposing: true);
1✔
135
        GC.SuppressFinalize(this);
1✔
136
    }
1✔
137

138
    private sealed class SingleThreadedSynchronizationContextChannel
139
        : SynchronizationContext
140
    {
141
        private readonly Channel<(SendOrPostCallback d, object? state)> _channel =
2✔
142
            Channel.CreateUnbounded<(SendOrPostCallback d, object? state)>(
2✔
143
                new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
2✔
144

145
        public override void Post(SendOrPostCallback d, object? state)
146
            => _channel.Writer.TryWrite((d, state));
2✔
147

148
        public override void Send(SendOrPostCallback d, object? state)
149
            => throw new InvalidOperationException("Synchronous operations are not supported!");
×
150

151
        public static void Await(Func<Task> taskInvoker)
152
        {
153
            var originalContext = Current;
2✔
154
            try
155
            {
156
                var context = new SingleThreadedSynchronizationContextChannel();
2✔
157
                SetSynchronizationContext(context);
2✔
158

159
                Task task;
160
                try
161
                {
162
                    task = taskInvoker.Invoke();
2✔
163
                }
2✔
164
                catch (Exception ex)
×
165
                {
166
                    // If the invoker throws synchronously, complete the channel so the pump can exit.
167
                    context._channel.Writer.Complete(ex);
×
168
                    throw;
×
169
                }
170

171
                _ = task.ContinueWith(t
2✔
172
                    => context._channel.Writer.Complete(t.Exception),
2✔
173
                    TaskScheduler.Default);
2✔
174

175
                // Pump loop: block synchronously until items are available or the writer completes.
176
                while (context._channel.Reader.WaitToReadAsync().Preserve().GetAwaiter().GetResult())
2✔
177
                {
178
                    while (context._channel.Reader.TryRead(out var work))
2✔
179
                    {
180
                        work.d.Invoke(work.state);
2✔
181
                    }
2✔
182
                }
183

184
                task.GetAwaiter().GetResult();
2✔
185
            }
×
186
            finally
187
            {
188
                SetSynchronizationContext(originalContext);
2✔
189
            }
2✔
190
        }
×
191
    }
192
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc