Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2b726d2
docs(809): add active bug feature folder for UiThread init contract r…
drmoisan Sep 8, 2026
194b848
docs(809): add research artifact and correct the #784 root-cause note
drmoisan Sep 8, 2026
6e67276
docs(809): author the bug specification
drmoisan Sep 8, 2026
27cb7a3
docs(809): author the atomic implementation plan
drmoisan Sep 8, 2026
7c99ce4
docs(809): extend the spec Write Set with three DoNotParallelize-only…
drmoisan Sep 8, 2026
b13bb0d
docs(809): apply preflight round 1 delta to the plan
drmoisan Sep 8, 2026
06e4a97
docs(809): apply preflight round 2 delta to the plan
drmoisan Sep 8, 2026
c1abcfc
docs(809): apply preflight round 3 delta to the plan
drmoisan Sep 8, 2026
8dbceba
docs(809): apply preflight round 4 delta to the plan
drmoisan Sep 8, 2026
e23a036
chore(809): capture Phase 0 baseline, bootstrap and policy-read evidence
drmoisan Sep 8, 2026
09bb952
test(809): add the UiThread capture-object factory seam and state-res…
drmoisan Sep 8, 2026
f7294d7
test(809): add failing regression tests for the three UiThread defects
drmoisan Sep 8, 2026
2112679
fix(809): reject non-STA callers in UiThread.Init and repair retry an…
drmoisan Sep 8, 2026
fd22abf
test(809): reconcile the MTA UiThread.Init caller with a pumping disp…
drmoisan Sep 8, 2026
0db2cf0
chore(809): record the final QC toolchain loop evidence
drmoisan Sep 8, 2026
01a467b
docs(809): record coverage comparison, acceptance-criteria check-off …
drmoisan Sep 8, 2026
fd51d79
docs(809): check off the executed plan tasks
drmoisan Sep 8, 2026
ef431e6
docs(809): check off the final plan task
drmoisan Sep 8, 2026
b1c211c
docs(809): add feature review artifacts and withdraw the unverified a…
drmoisan Sep 8, 2026
3b4a0c9
docs(809): correct the apartment mechanism recorded for future planners
drmoisan Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 62 additions & 28 deletions QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,38 +323,72 @@ public async Task RunAsync_ExecutesCorrectly()
}

[TestMethod]
public void Worker_RunWorkerCompleted_HandlesCompletionCorrectly()
public async System.Threading.Tasks.Task Worker_RunWorkerCompleted_HandlesCompletionCorrectly()
{
// Arrange
UiThread.Init(false);
var mockFormViewer = new Mock<IQfcFormViewer>();
mockFormViewer.SetupAllProperties();
mockFormViewer.SetupProperty(m => m.ItemsPerLoadEnabled, false);
mockFormViewer.SetupProperty(m => m.SkipButtonEnabled, false);
_controller
.GetType()
.GetField(
"_formViewer",
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
)
.SetValue(_controller, mockFormViewer.Object);
// Arrange: install a dispatcher belonging to a thread that actually pumps, instead of
// calling UiThread.Init() on the MSTest worker. Init() now rejects a non-STA caller,
// and the old arrangement additionally left the test order-dependent on whichever
// thread had consumed the initialization latch first.
var host = new QuickFiler.Test.TestSupport.WinFormsPumpHost();
UiThreadDispatcherTransaction transaction = null;
try
{
// Dispatcher.FromThread is a lookup that never creates a dispatcher, so the
// dispatcher must first be created on the pump thread itself; resolving it before
// that returns null and Install(null) would leave UiThread._dispatcher unset.
Thread pumpThread = await host.InvokeAsync(() =>
{
System.Windows.Threading.Dispatcher.CurrentDispatcher.Should().NotBeNull();
return System.Threading.Thread.CurrentThread;
})
.ConfigureAwait(false);

System.Windows.Threading.Dispatcher pumpDispatcher =
System.Windows.Threading.Dispatcher.FromThread(pumpThread);
pumpDispatcher
.Should()
.NotBeNull(
because: "the dispatcher was created on the pump thread by the call above"
);

transaction =
await QuickFiler.Controllers.Tests.UiThreadDispatcherFixture.BeginTransactionAsync();
transaction.Install(pumpDispatcher);

var mockFormViewer = new Mock<IQfcFormViewer>();
mockFormViewer.SetupAllProperties();
mockFormViewer.SetupProperty(m => m.ItemsPerLoadEnabled, false);
mockFormViewer.SetupProperty(m => m.SkipButtonEnabled, false);
_controller
.GetType()
.GetField(
"_formViewer",
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
)
.SetValue(_controller, mockFormViewer.Object);

var eventArgs = new RunWorkerCompletedEventArgs(null, null, false);
var eventArgs = new RunWorkerCompletedEventArgs(null, null, false);

// Act
_controller
.GetType()
.GetMethod(
"Worker_RunWorkerCompleted",
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
)
.Invoke(_controller, new object[] { null, eventArgs });
// Act
_controller
.GetType()
.GetMethod(
"Worker_RunWorkerCompleted",
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
)
.Invoke(_controller, new object[] { null, eventArgs });

// Assert
Assert.IsTrue(mockFormViewer.Object.ItemsPerLoadEnabled);
Assert.IsTrue(mockFormViewer.Object.SkipButtonEnabled);
// Assert
Assert.IsTrue(mockFormViewer.Object.ItemsPerLoadEnabled);
Assert.IsTrue(mockFormViewer.Object.SkipButtonEnabled);
}
finally
{
transaction?.Dispose();
await host.StopAsync().ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace UtilitiesCS.Test.EmailIntelligence
/// is set to a synthetic snapshot so that SetupTree() does not hit COM.
/// </summary>
[STATestClass]
[DoNotParallelize]
public class FilterOlFoldersViewer_Tests
{
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace UtilitiesCS.Test.EmailIntelligence
/// and _mappings2 are set via reflection to avoid COM access.
/// </summary>
[STATestClass]
[DoNotParallelize]
public class FolderRemapViewer_Tests
{
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
namespace UtilitiesCS.Test.OutlookObjects.Folder
{
[TestClass]
[DoNotParallelize]
public class FolderPredictorTests
{
[TestMethod]
Expand Down
209 changes: 209 additions & 0 deletions UtilitiesCS.Test/TestHelpers/UiThreadStateScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
using System;
using System.Reflection;
using System.Threading;
using System.Windows.Threading;
using FluentAssertions;
using UtilitiesCS;
using UtilitiesCS.Threading;

namespace UtilitiesCS.Test
{
/// <summary>
/// Snapshots every process-global static <c>UiThread</c> owns, resets them all through that
/// type's internal <c>ResetForTesting</c> hook, and restores the captured values on disposal.
/// </summary>
/// <remarks>
/// This type is deliberately <b>not</b> internally synchronized. It performs an unguarded
/// read-then-write against process-global statics, so two tests entering a scope concurrently
/// would interleave and one would restore values the other had already replaced. Serialization
/// of writers is provided instead by <c>[DoNotParallelize]</c> on every consuming test class. A
/// future caller must not assume this type is thread-safe: adding a new consuming test class
/// requires adding that attribute to the class as well.
///
/// Reflection is required because <c>InternalsVisibleTo</c> exposes internal members only; it
/// does not expose private ones, and every controlled field is private. Centralising the
/// reflection here means each field name appears in exactly one place in this assembly.
/// </remarks>
#nullable enable annotations
internal sealed class UiThreadStateScope : IDisposable
{
private static readonly FieldInfo InitializedInfo = Resolve("_initialized");
private static readonly FieldInfo UiSyncContextInfo = Resolve("_uiSyncContext");
private static readonly FieldInfo AutoScaleFactorInfo = Resolve("_autoScaleFactor");
private static readonly FieldInfo UiThreadIdInfo = Resolve("_uiThreadId");
private static readonly FieldInfo DispatcherInfo = Resolve("_dispatcher");
private static readonly FieldInfo SyncContextFormInfo = Resolve("_syncContextForm");
private static readonly FieldInfo ThreadMonitorInfo = Resolve("_threadMonitor");
private static readonly FieldInfo MonitorUiThreadInfo = Resolve("_monitorUiThread");
private static readonly FieldInfo OnLockupDetectedInfo = Resolve("_onLockupDetected");
private static readonly FieldInfo MonitorTimeProviderInfo = Resolve("_monitorTimeProvider");
private static readonly FieldInfo LockupThresholdInfo = Resolve(
"_lockupAttributionThresholdMs"
);

private readonly object?[] _priorValues;
private readonly Func<IUiCaptureSource> _priorFactory;
private bool _disposed;

private UiThreadStateScope(object?[] priorValues, Func<IUiCaptureSource> priorFactory)
{
_priorValues = priorValues;
_priorFactory = priorFactory;
}

/// <summary>
/// The field-info objects this scope controls, in the order their prior values are
/// captured and restored.
/// </summary>
private static FieldInfo[] ControlledFields =>
new[]
{
InitializedInfo,
UiSyncContextInfo,
AutoScaleFactorInfo,
UiThreadIdInfo,
DispatcherInfo,
SyncContextFormInfo,
ThreadMonitorInfo,
MonitorUiThreadInfo,
OnLockupDetectedInfo,
MonitorTimeProviderInfo,
LockupThresholdInfo,
};

/// <summary>
/// Captures every controlled field plus <c>UiThread.SyncContextFormFactory</c>, resets the
/// statics, and returns a scope that restores the captured values when disposed.
/// </summary>
/// <returns>A scope whose disposal restores the captured prior state.</returns>
internal static UiThreadStateScope Enter()
{
FieldInfo[] fields = ControlledFields;
var prior = new object?[fields.Length];
for (int i = 0; i < fields.Length; i++)
{
prior[i] = fields[i].GetValue(null);
}

Func<IUiCaptureSource> priorFactory = UiThread.SyncContextFormFactory;
UiThread.ResetForTesting();
return new UiThreadStateScope(prior, priorFactory);
}

/// <summary>Reads <c>UiThread._monitorUiThread</c> without going through a property.</summary>
internal static bool MonitorUiThread => (bool)MonitorUiThreadInfo.GetValue(null);

/// <summary>Reads <c>UiThread._onLockupDetected</c> without going through a property.</summary>
internal static Action<LockupAttribution>? OnLockupDetected =>
(Action<LockupAttribution>?)OnLockupDetectedInfo.GetValue(null);

/// <summary>Reads <c>UiThread._monitorTimeProvider</c> without going through a property.</summary>
internal static TimeProvider? MonitorTimeProvider =>
(TimeProvider?)MonitorTimeProviderInfo.GetValue(null);

/// <summary>Reads <c>UiThread._lockupAttributionThresholdMs</c> without going through a property.</summary>
internal static int LockupAttributionThresholdMs => (int)LockupThresholdInfo.GetValue(null);

/// <summary>
/// Reads <c>UiThread._uiSyncContext</c> directly.
/// </summary>
/// <remarks>
/// The <c>UiThread.UiSyncContext</c> property lazily calls <c>Init()</c> when the field is
/// null, so a test that needs to observe the uninitialized state cannot use the property.
/// </remarks>
internal static SynchronizationContext? UiSyncContextField =>
(SynchronizationContext?)UiSyncContextInfo.GetValue(null);

/// <summary>
/// Reads <c>UiThread._autoScaleFactor</c> directly, for the same reason as
/// <see cref="UiSyncContextField"/>.
/// </summary>
internal static System.Drawing.SizeF? AutoScaleFactorField =>
(System.Drawing.SizeF?)AutoScaleFactorInfo.GetValue(null);

/// <summary>Reads <c>UiThread._uiThreadId</c> directly.</summary>
internal static int UiThreadIdField => (int)UiThreadIdInfo.GetValue(null);

/// <summary>
/// Reads <c>UiThread._dispatcher</c> directly.
/// </summary>
/// <remarks>
/// The <c>UiThread.Dispatcher</c> property throws when the field is unset, so a test that
/// needs to observe the uninitialized state cannot use the property.
/// </remarks>
internal static Dispatcher? DispatcherField => (Dispatcher?)DispatcherInfo.GetValue(null);

/// <summary>Reads <c>UiThread._syncContextForm</c> directly.</summary>
internal static IUiCaptureSource? SyncContextFormField =>
(IUiCaptureSource?)SyncContextFormInfo.GetValue(null);

/// <summary>Reads <c>UiThread._threadMonitor</c> directly.</summary>
internal static ThreadMonitor? ThreadMonitorField =>
(ThreadMonitor?)ThreadMonitorInfo.GetValue(null);

/// <summary>
/// Installs a value into <c>UiThread._uiSyncContext</c> for the remainder of this scope.
/// </summary>
/// <param name="value">The value to install; may be null.</param>
internal static void SetUiSyncContext(SynchronizationContext? value) =>
UiSyncContextInfo.SetValue(null, value);

/// <summary>
/// Installs a value into <c>UiThread._uiThreadId</c> for the remainder of this scope.
/// </summary>
/// <param name="value">The managed thread id to install.</param>
internal static void SetUiThreadId(int value) => UiThreadIdInfo.SetValue(null, value);

/// <summary>
/// Installs a value into <c>UiThread._dispatcher</c> for the remainder of this scope.
/// </summary>
/// <param name="value">The dispatcher to install; may be null.</param>
internal static void SetDispatcher(Dispatcher? value) =>
DispatcherInfo.SetValue(null, value);

/// <summary>
/// Restores every captured value, including a captured null, and restores the factory.
/// </summary>
/// <remarks>
/// Each captured prior is written back unconditionally rather than being tested for null
/// first: a null prior is a real state that must be restored, and skipping the write for it
/// would leak an installed value into every later test on the same process-global static.
/// A second call is a no-op.
/// </remarks>
public void Dispose()
{
if (_disposed)
{
return;
}

FieldInfo[] fields = ControlledFields;
for (int i = 0; i < fields.Length; i++)
{
fields[i].SetValue(null, _priorValues[i]);
}

UiThread.SyncContextFormFactory = _priorFactory;
_disposed = true;
}

private static FieldInfo Resolve(string fieldName)
{
FieldInfo field = typeof(UiThread).GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Static
);
field
.Should()
.NotBeNull(
because: "UiThread.{0} backing field must exist for UiThreadStateScope to "
+ "control it; a rename must fail loudly here rather than degrade to a "
+ "silent no-op that restores nothing and still passes",
fieldName
);
return field;
}
}

#nullable restore annotations
}
Loading
Loading