Skip to content

Commit df627e1

Browse files
committed
fix(tests): run NLogViewerFilterTests on shared STA thread to fix batch failures
NLogViewerFilterTests passed individually but failed when run together because later tests used Application.Current.Dispatcher from the first test's (dead) STA thread. Use a single STA thread and WPF Application for all filter tests via WpfStaContextFixture and IClassFixture. Simplify WpfTestHelper (remove RunOnStaThread; use StartListen in CreateViewerWithTestData). Remove unused usings in test files.
1 parent 7606dba commit df627e1

File tree

6 files changed

+144
-161
lines changed

6 files changed

+144
-161
lines changed

tests/Sentinel.NLogViewer.App.Tests/NLogViewerFilterTests.cs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ namespace Sentinel.NLogViewer.App.Tests
1111
/// <summary>
1212
/// Unit tests for NLogViewer filter commands (AddRegexSearchTerm and AddRegexSearchTermExclude)
1313
/// </summary>
14-
public class NLogViewerFilterTests : IDisposable
14+
public class NLogViewerFilterTests : IClassFixture<WpfStaContextFixture>, IDisposable
1515
{
16-
public NLogViewerFilterTests()
16+
private readonly WpfStaContextFixture _context;
17+
18+
public NLogViewerFilterTests(WpfStaContextFixture fixture)
1719
{
18-
// No viewer initialization here - each test creates its own
20+
_context = fixture ?? throw new ArgumentNullException(nameof(fixture));
1921
}
2022

2123
[Fact]
2224
public void AddRegexSearchTerm_EscapesSpecialCharacters_CreatesLiteralPattern()
2325
{
24-
WpfTestHelper.RunOnStaThread(() =>
26+
_context.RunOnSta(() =>
2527
{
2628
// Arrange
2729
var viewer = new WpfNLogViewer();
@@ -44,7 +46,7 @@ public void AddRegexSearchTerm_EscapesSpecialCharacters_CreatesLiteralPattern()
4446
[Fact]
4547
public void AddRegexSearchTerm_LoggerNameWithDot_MatchesExactLoggerName()
4648
{
47-
WpfTestHelper.RunOnStaThread(() =>
49+
_context.RunOnSta(() =>
4850
{
4951
// Arrange
5052
var testData = new ObservableCollection<LogEventInfo>
@@ -71,7 +73,7 @@ public void AddRegexSearchTerm_LoggerNameWithDot_MatchesExactLoggerName()
7173
[Fact]
7274
public void AddRegexSearchTermExclude_CreatesNegativeLookaheadPattern()
7375
{
74-
WpfTestHelper.RunOnStaThread(() =>
76+
_context.RunOnSta(() =>
7577
{
7678
// Arrange
7779
var viewer = new WpfNLogViewer();
@@ -93,7 +95,7 @@ public void AddRegexSearchTermExclude_CreatesNegativeLookaheadPattern()
9395
[Fact]
9496
public void AddRegexSearchTermExclude_ExcludesMatchingEntries()
9597
{
96-
WpfTestHelper.RunOnStaThread(() =>
98+
_context.RunOnSta(() =>
9799
{
98100
// Arrange
99101
var testData = new ObservableCollection<LogEventInfo>
@@ -122,7 +124,7 @@ public void AddRegexSearchTermExclude_ExcludesMatchingEntries()
122124
[Fact]
123125
public void AddRegexSearchTermExclude_ShowsNonMatchingEntries()
124126
{
125-
WpfTestHelper.RunOnStaThread(() =>
127+
_context.RunOnSta(() =>
126128
{
127129
// Arrange
128130
var testData = new ObservableCollection<LogEventInfo>
@@ -148,7 +150,7 @@ public void AddRegexSearchTermExclude_ShowsNonMatchingEntries()
148150
[Fact]
149151
public void Filter_IncludeAndExcludeTerms_AppliesBothCorrectly()
150152
{
151-
WpfTestHelper.RunOnStaThread(() =>
153+
_context.RunOnSta(() =>
152154
{
153155
// Arrange
154156
var testData = new ObservableCollection<LogEventInfo>
@@ -178,7 +180,7 @@ public void Filter_IncludeAndExcludeTerms_AppliesBothCorrectly()
178180
[Fact]
179181
public void Filter_MultipleIncludeTerms_RequiresAllToMatch()
180182
{
181-
WpfTestHelper.RunOnStaThread(() =>
183+
_context.RunOnSta(() =>
182184
{
183185
// Arrange
184186
var testData = new ObservableCollection<LogEventInfo>
@@ -208,7 +210,7 @@ public void Filter_MultipleIncludeTerms_RequiresAllToMatch()
208210
[Fact]
209211
public void Filter_MultipleExcludeTerms_HidesIfAnyMatches()
210212
{
211-
WpfTestHelper.RunOnStaThread(() =>
213+
_context.RunOnSta(() =>
212214
{
213215
// Arrange
214216
var testData = new ObservableCollection<LogEventInfo>
@@ -236,7 +238,7 @@ public void Filter_MultipleExcludeTerms_HidesIfAnyMatches()
236238
[Fact]
237239
public void Filter_EmptySearchTerms_ShowsAllEntries()
238240
{
239-
WpfTestHelper.RunOnStaThread(() =>
241+
_context.RunOnSta(() =>
240242
{
241243
// Arrange
242244
var testData = new ObservableCollection<LogEventInfo>
@@ -260,7 +262,7 @@ public void Filter_EmptySearchTerms_ShowsAllEntries()
260262
[Fact]
261263
public void AddRegexSearchTerm_SpecialRegexCharacters_AreEscaped()
262264
{
263-
WpfTestHelper.RunOnStaThread(() =>
265+
_context.RunOnSta(() =>
264266
{
265267
// Arrange
266268
var viewer = new WpfNLogViewer();
@@ -285,7 +287,7 @@ public void AddRegexSearchTerm_SpecialRegexCharacters_AreEscaped()
285287
[Fact]
286288
public void AddRegexSearchTermExclude_SpecialRegexCharacters_AreEscapedInNegativeLookahead()
287289
{
288-
WpfTestHelper.RunOnStaThread(() =>
290+
_context.RunOnSta(() =>
289291
{
290292
// Arrange
291293
var viewer = new WpfNLogViewer();
@@ -308,7 +310,7 @@ public void AddRegexSearchTermExclude_SpecialRegexCharacters_AreEscapedInNegativ
308310
[Fact]
309311
public void Filter_ExcludePatternMatches_EntryIsHidden()
310312
{
311-
WpfTestHelper.RunOnStaThread(() =>
313+
_context.RunOnSta(() =>
312314
{
313315
// Arrange
314316
var testData = new ObservableCollection<LogEventInfo>
@@ -332,7 +334,7 @@ public void Filter_ExcludePatternMatches_EntryIsHidden()
332334
[Fact]
333335
public void Filter_ExcludePatternDoesNotMatch_EntryIsShown()
334336
{
335-
WpfTestHelper.RunOnStaThread(() =>
337+
_context.RunOnSta(() =>
336338
{
337339
// Arrange
338340
var testData = new ObservableCollection<LogEventInfo>
@@ -357,7 +359,7 @@ public void Filter_ExcludePatternDoesNotMatch_EntryIsShown()
357359
[Fact]
358360
public void Filter_MessageField_IsAlsoFiltered()
359361
{
360-
WpfTestHelper.RunOnStaThread(() =>
362+
_context.RunOnSta(() =>
361363
{
362364
// Arrange
363365
var testData = new ObservableCollection<LogEventInfo>
@@ -383,7 +385,7 @@ public void Filter_MessageField_IsAlsoFiltered()
383385
[Fact]
384386
public void Filter_ExcludeMessageField_WorksCorrectly()
385387
{
386-
WpfTestHelper.RunOnStaThread(() =>
388+
_context.RunOnSta(() =>
387389
{
388390
// Arrange
389391
var testData = new ObservableCollection<LogEventInfo>

tests/Sentinel.NLogViewer.App.Tests/Parsers/JsonLogParserTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System;
2-
using System.Linq;
31
using NLog;
42
using Sentinel.NLogViewer.App.Parsers;
53
using Xunit;

tests/Sentinel.NLogViewer.App.Tests/Parsers/PlainTextParserTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System;
2-
using System.Linq;
31
using NLog;
42
using Sentinel.NLogViewer.App.Parsers;
53
using Xunit;

tests/Sentinel.NLogViewer.App.Tests/TestCacheTarget.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System;
2-
using System.Collections.Generic;
31
using System.Reactive.Linq;
42
using System.Reactive.Subjects;
53
using NLog;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Windows;
2+
using System.Windows.Threading;
3+
4+
namespace Sentinel.NLogViewer.App.Tests
5+
{
6+
/// <summary>
7+
/// xUnit class fixture that provides a single STA thread with a WPF Application
8+
/// so that all tests run in the same STA/Application context and share one Dispatcher.
9+
/// Use for WPF tests that would otherwise see a stale Application.Current when run together.
10+
/// </summary>
11+
public class WpfStaContextFixture : IDisposable
12+
{
13+
private Dispatcher _dispatcher;
14+
private readonly Thread _thread;
15+
private readonly ManualResetEvent _ready = new ManualResetEvent(false);
16+
private const int StaThreadReadyTimeoutMs = 5000;
17+
18+
public WpfStaContextFixture()
19+
{
20+
_thread = new Thread(StaThreadProc)
21+
{
22+
IsBackground = true
23+
};
24+
_thread.SetApartmentState(ApartmentState.STA);
25+
_thread.Start();
26+
if (!_ready.WaitOne(StaThreadReadyTimeoutMs))
27+
throw new InvalidOperationException("STA thread did not signal ready within timeout.");
28+
if (_dispatcher == null)
29+
throw new InvalidOperationException("STA thread did not set Dispatcher.");
30+
}
31+
32+
private void StaThreadProc()
33+
{
34+
var app = new Application();
35+
app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
36+
_dispatcher = Dispatcher.CurrentDispatcher;
37+
_ready.Set();
38+
Dispatcher.Run();
39+
}
40+
41+
/// <summary>
42+
/// Runs the given action on the shared STA thread (same thread as Application.Current).
43+
/// Blocks until the action completes.
44+
/// </summary>
45+
public void RunOnSta(Action action)
46+
{
47+
if (action == null)
48+
throw new ArgumentNullException(nameof(action));
49+
_dispatcher.Invoke(action);
50+
}
51+
52+
/// <summary>
53+
/// Runs the given function on the shared STA thread and returns its result.
54+
/// Blocks until the function completes.
55+
/// </summary>
56+
public T RunOnSta<T>(Func<T> func)
57+
{
58+
if (func == null)
59+
throw new ArgumentNullException(nameof(func));
60+
return _dispatcher.Invoke(func);
61+
}
62+
63+
/// <summary>
64+
/// Shuts down the WPF Application on the STA thread and joins the thread.
65+
/// </summary>
66+
public void Dispose()
67+
{
68+
if (_dispatcher != null && _dispatcher.Thread.IsAlive)
69+
{
70+
try
71+
{
72+
_dispatcher.Invoke(() => Application.Current?.Shutdown());
73+
}
74+
catch (Exception)
75+
{
76+
// Best effort; thread may already be shutting down
77+
}
78+
}
79+
_thread.Join(TimeSpan.FromSeconds(5));
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)