Skip to content

Commit 0ecece0

Browse files
committed
feat: integrate TestTerminalContext with Terminal.Instance
- TestTerminalContext.Current setter now syncs with Terminal.Instance - Saves previous Terminal.Instance and restores on clear - TestTerminal.Dispose clears context if it's the current terminal - Added Terminal property for direct access to current test terminal - Added integration tests Closes #11
1 parent 3bbff34 commit 0ecece0

3 files changed

Lines changed: 184 additions & 5 deletions

File tree

source/timewarp-terminal/test-terminal-context.cs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace TimeWarp.Terminal;
22

33
/// <summary>
44
/// Provides an ambient context for <see cref="TestTerminal"/> that enables zero-configuration testing
5-
/// of CLI applications.
5+
/// of CLI applications with automatic <see cref="TimeWarp.Terminal.Terminal.Instance"/> synchronization.
66
/// </summary>
77
/// <remarks>
88
/// <para>
@@ -11,38 +11,58 @@ namespace TimeWarp.Terminal;
1111
/// running tests in parallel.
1212
/// </para>
1313
/// <para>
14+
/// When <see cref="Current"/> is set, it automatically updates <see cref="TimeWarp.Terminal.Terminal.Instance"/>
15+
/// and restores the previous instance when cleared.
16+
/// </para>
17+
/// <para>
1418
/// Resolution order when determining which terminal to use:
1519
/// <list type="number">
1620
/// <item><description><see cref="Current"/> (if set)</description></item>
1721
/// <item><description><c>ITerminal</c> from DI (if registered)</description></item>
1822
/// <item><description><see cref="TimeWarpTerminal.Default"/> (fallback)</description></item>
1923
/// </list>
2024
/// </para>
25+
/// </remarks>
2126
/// <example>
27+
/// Simple test pattern with automatic TimeWarp.Terminal.Terminal.Instance synchronization:
2228
/// <code>
2329
/// public static async Task Should_display_greeting()
2430
/// {
2531
/// using TestTerminal terminal = new();
2632
/// TestTerminalContext.Current = terminal;
2733
///
34+
/// // TimeWarp.Terminal.Terminal.Instance is now set to terminal
35+
/// Terminal.WriteLine("Hello"); // Routes to test terminal
36+
///
2837
/// await Program.Main(["greet", "World"]);
2938
///
3039
/// terminal.OutputContains("Hello, World!").ShouldBeTrue();
40+
///
41+
/// // On dispose, TestTerminal clears context and restores previous TimeWarp.Terminal.Terminal.Instance
3142
/// }
3243
/// </code>
3344
/// </example>
34-
/// </remarks>
3545
public static class TestTerminalContext
3646
{
3747
private static readonly AsyncLocal<TestTerminal?> Context = new();
48+
private static readonly AsyncLocal<ITerminal?> PreviousInstance = new();
3849

3950
/// <summary>
4051
/// Gets or sets the current <see cref="TestTerminal"/> for the async execution context.
4152
/// </summary>
4253
/// <remarks>
4354
/// <para>
44-
/// Setting this property to a non-null value causes all terminal resolution
45-
/// to use the provided <see cref="TestTerminal"/> instead of the real terminal.
55+
/// Setting this property to a non-null value causes:
56+
/// <list type="bullet">
57+
/// <item><description>The current <see cref="TimeWarp.Terminal.Terminal.Instance"/> to be saved</description></item>
58+
/// <item><description><see cref="TimeWarp.Terminal.Terminal.Instance"/> to be set to the provided terminal</description></item>
59+
/// </list>
60+
/// </para>
61+
/// <para>
62+
/// Setting this property to <c>null</c> causes:
63+
/// <list type="bullet">
64+
/// <item><description><see cref="TimeWarp.Terminal.Terminal.Instance"/> to be restored to its previous value</description></item>
65+
/// </list>
4666
/// </para>
4767
/// <para>
4868
/// The value is scoped to the current async execution context, so parallel tests
@@ -55,14 +75,41 @@ public static class TestTerminalContext
5575
public static TestTerminal? Current
5676
{
5777
get => Context.Value;
58-
set => Context.Value = value;
78+
set
79+
{
80+
if (value is not null)
81+
{
82+
// Save current TimeWarp.Terminal.Terminal.Instance before replacing
83+
PreviousInstance.Value = TimeWarp.Terminal.Terminal.Instance;
84+
Context.Value = value;
85+
TimeWarp.Terminal.Terminal.Instance = value;
86+
}
87+
else if (Context.Value is not null)
88+
{
89+
// Restore previous TimeWarp.Terminal.Terminal.Instance
90+
Context.Value = null;
91+
if (PreviousInstance.Value is not null)
92+
{
93+
TimeWarp.Terminal.Terminal.Instance = PreviousInstance.Value;
94+
PreviousInstance.Value = null;
95+
}
96+
}
97+
}
5998
}
6099

61100
/// <summary>
62101
/// Gets a value indicating whether a <see cref="TestTerminal"/> is set for the current context.
63102
/// </summary>
64103
public static bool HasValue => Context.Value is not null;
65104

105+
/// <summary>
106+
/// Gets the <see cref="TestTerminal"/> for the current context, or throws if not set.
107+
/// </summary>
108+
/// <returns>The current <see cref="TestTerminal"/>.</returns>
109+
/// <exception cref="InvalidOperationException">Thrown when no test terminal is set.</exception>
110+
public static TestTerminal Terminal
111+
=> Context.Value ?? throw new InvalidOperationException("No TestTerminal set in current context. Set TestTerminalContext.Current first.");
112+
66113
/// <summary>
67114
/// Resolves a terminal using the standard resolution order:
68115
/// TestTerminalContext.Current → provided terminal → fallback.

source/timewarp-terminal/test-terminal.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,20 @@ public string[] GetOutputLines()
332332
/// <summary>
333333
/// Disposes the resources used by this instance.
334334
/// </summary>
335+
/// <summary>
336+
/// Disposes the resources used by this instance and clears the test context if this is the current terminal.
337+
/// </summary>
335338
public void Dispose()
336339
{
337340
if (Disposed)
338341
return;
339342

343+
// Clear context if this is the current terminal (restores previous Terminal.Instance)
344+
if (TestTerminalContext.Current == this)
345+
{
346+
TestTerminalContext.Current = null;
347+
}
348+
340349
InputReader.Dispose();
341350
OutputWriter.Dispose();
342351
ErrorWriter.Dispose();
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/dotnet --
2+
#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
3+
4+
// Tests for TestTerminalContext integration with Terminal.Instance
5+
#pragma warning disable CA1849
6+
7+
#if !JARIBU_MULTI
8+
return await RunAllTests();
9+
#endif
10+
11+
namespace TimeWarp.Terminal.Tests.Core.TestTerminalContextIntegration
12+
{
13+
14+
[TestTag("TestTerminalContext")]
15+
public class TestTerminalContextIntegrationTests
16+
{
17+
[ModuleInitializer]
18+
internal static void Register() => RegisterTests<TestTerminalContextIntegrationTests>();
19+
20+
public static async Task Should_set_terminal_instance_when_context_set()
21+
{
22+
// Arrange
23+
ITerminal original = TimeWarp.Terminal.Terminal.Instance;
24+
using TestTerminal testTerminal = new();
25+
26+
// Act
27+
TestTerminalContext.Current = testTerminal;
28+
29+
// Assert
30+
TimeWarp.Terminal.Terminal.Instance.ShouldBe(testTerminal);
31+
32+
// Cleanup
33+
TestTerminalContext.Current = null;
34+
TimeWarp.Terminal.Terminal.Instance = original;
35+
36+
await Task.CompletedTask;
37+
}
38+
39+
public static async Task Should_restore_terminal_instance_when_context_cleared()
40+
{
41+
// Arrange
42+
ITerminal original = TimeWarp.Terminal.Terminal.Instance;
43+
using TestTerminal testTerminal = new();
44+
TestTerminalContext.Current = testTerminal;
45+
46+
// Act
47+
TestTerminalContext.Current = null;
48+
49+
// Assert
50+
TimeWarp.Terminal.Terminal.Instance.ShouldBe(original);
51+
52+
await Task.CompletedTask;
53+
}
54+
55+
public static async Task Should_restore_terminal_instance_on_dispose()
56+
{
57+
// Arrange
58+
ITerminal original = TimeWarp.Terminal.Terminal.Instance;
59+
60+
// Act
61+
using (TestTerminal testTerminal = new())
62+
{
63+
TestTerminalContext.Current = testTerminal;
64+
TimeWarp.Terminal.Terminal.Instance.ShouldBe(testTerminal);
65+
}
66+
67+
// Assert - Terminal.Instance should be restored after dispose
68+
TimeWarp.Terminal.Terminal.Instance.ShouldBe(original);
69+
70+
await Task.CompletedTask;
71+
}
72+
73+
public static async Task Should_route_static_terminal_calls_to_test_terminal()
74+
{
75+
// Arrange
76+
ITerminal original = TimeWarp.Terminal.Terminal.Instance;
77+
using TestTerminal testTerminal = new();
78+
TestTerminalContext.Current = testTerminal;
79+
80+
// Act
81+
TimeWarp.Terminal.Terminal.WriteLine("Hello from static API");
82+
83+
// Assert
84+
testTerminal.OutputContains("Hello from static API").ShouldBeTrue();
85+
86+
// Cleanup
87+
TestTerminalContext.Current = null;
88+
TimeWarp.Terminal.Terminal.Instance = original;
89+
90+
await Task.CompletedTask;
91+
}
92+
93+
public static async Task Should_provide_terminal_property()
94+
{
95+
// Arrange
96+
using TestTerminal testTerminal = new();
97+
TestTerminalContext.Current = testTerminal;
98+
99+
// Act
100+
TestTerminal retrieved = TestTerminalContext.Terminal;
101+
102+
// Assert
103+
retrieved.ShouldBe(testTerminal);
104+
105+
// Cleanup
106+
TestTerminalContext.Current = null;
107+
108+
await Task.CompletedTask;
109+
}
110+
111+
public static async Task Should_throw_when_terminal_accessed_without_context()
112+
{
113+
// Arrange - ensure no context
114+
TestTerminalContext.Current = null;
115+
116+
// Act & Assert
117+
Should.Throw<InvalidOperationException>(() => _ = TestTerminalContext.Terminal);
118+
119+
await Task.CompletedTask;
120+
}
121+
}
122+
123+
}

0 commit comments

Comments
 (0)