Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ce7f4e
Fixes #4804. InjectKey Tests with Pipeline Mode hangs forever in the …
BDisp Mar 6, 2026
be016ca
Code cleanup
BDisp Mar 6, 2026
86f567a
Forced to use Console.KeyAvailable due a not reliable GetNumberOfCons…
BDisp Mar 7, 2026
f348350
Call ProcessQueue while _testSource IsAvailable is true
BDisp Mar 7, 2026
9028e4e
Check AnsiPlatform in CI pipelines
BDisp Mar 7, 2026
399370e
Add AnsiPlatform check to all pipeline mode
BDisp Mar 7, 2026
c92d71d
Add IsRunningInTest method
BDisp Mar 7, 2026
e79fcc7
Move static IsRunningInTest method to the internal DriverImpl class
BDisp Mar 7, 2026
2a9c015
Merge branch 'v2_develop' into v2_4804_readfile-hang-fix
BDisp Mar 7, 2026
30a4a3d
Increasing delay time
BDisp Mar 7, 2026
bbd4067
Merge branch 'v2_develop' into v2_4804_readfile-hang-fix
BDisp Mar 7, 2026
5b2b46c
Put delay after call ProcessQueue
BDisp Mar 7, 2026
762d304
Delay before and after call ProcessQueue
BDisp Mar 7, 2026
a906657
Remove faulty code not needed now because the options.AutoProcess is …
BDisp Mar 7, 2026
bd9da52
Increase the delay in the longer test
BDisp Mar 7, 2026
a35a38e
Delay after ProcessQueue
BDisp Mar 7, 2026
a388ac0
Add WaitUntilAsync helper method
BDisp Mar 7, 2026
ed091dc
Testing without delay
BDisp Mar 7, 2026
7657a6a
Change AutoProcess = true
BDisp Mar 7, 2026
4d97378
Rename to DisableDriverRealIO per @tig
BDisp Mar 8, 2026
ffcad71
Fix erroneous information
BDisp Mar 8, 2026
0711d00
Fix erroneous information
BDisp Mar 8, 2026
2e74d52
Revert changes
BDisp Mar 8, 2026
a1f2318
Skip tests
BDisp Mar 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
21 changes: 21 additions & 0 deletions Terminal.Gui/App/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ public static IApplication Create (ITimeProvider? timeProvider = null)
return app;
}

/// <summary>
/// Determines whether the current execution context is within a unit test project.
/// </summary>
/// <remarks>
/// This method leverages <see cref="AppContext"/> switches to ensure compatibility with
/// <b>Native AOT</b> and avoids trimming-related issues associated with reflection.
/// <para>
/// For this to return <c>true</c>, the test project (e.g., xUnit v3) must define the
/// switch in its <c>.csproj</c> file:
/// <code>
/// &lt;ItemGroup&gt;
/// &lt;RuntimeHostConfigurationOption Include="Runtime.IsTestProject" Value="true" Trim="false" /&gt;
/// &lt;/ItemGroup&gt;
/// </code>
/// </para>
/// </remarks>
/// <returns>
/// <see langword="true"/> if the test execution flag is set; otherwise, <see langword="false"/>.
/// </returns>
public static bool IsRunningInTest () => AppContext.TryGetSwitch ("Runtime.IsTestProject", out bool isTest) && isTest;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd much prefer this to be on IDriver. I worked hard to ensure there was no coupling from drivers to IApplication and this undoes that.

Can you refactor this such that it's a property on IDriver instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I was just testing and I really thought it wasn't the proper way. So I'll include it in IDriver as a property and implement it in DriverImpl. Does that sound good to you?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, this method isn't characteristic of IDriver because it refers to the type of application being executed. I think it's better to add it as a property in IApplication and implement it in ApplicationImpl. I think that would be the right way. What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. Forget my previous comment. The reason it's included in IDriver is that practically all the classes that use it are also used by IDriver. So you were right in your reasoning. Thanks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that besides creating a property in IDriver and implementing it in DriverImpl, I'll also have to create the same property in IInput and IOutput. Don't you think that's too much work for just one property, which could even be static because when one test is running, all the others will run as well? I await your opinion and will work on the flaws in Linux and Mac.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's also used in several places. See the image below. If the IInput and IOutput interfaces had the IDriver property, then in their implementations it would be enough to call something like if (Driver.IsRunningInTest). Then you also have the case of the static class ClipboardProcessRunner, which adds even more complexity. The UnixTerminalHelper class would be easier because it's only called by AnsiOutput and UnixOutput, and you would just need to add a new parameter bool isRunningInTest to its Suspend method. The most complicated to solve would be ClipboardProcessRunner because it doesn't have any reference to IDriver, IInput, and IOutput. With this explanation, see if you can find the best solution.

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this only impacts windows.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this only impacts windows.

That was my initial perception, but after the tests failed with all the drivers, I verified that all the low-level reading functions were actually being executed.
Unfortunately, I still don't understand why the tests are still failing. The API could actually be used without the need for Task.Delay, but rather by signaling an event. Now we just need to figure out where.

@tig tig Mar 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try this: set an env variable in the yml. Have the IInput impls check for that variable.

There may already be an env variable set we can use.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try this: set an env variable in the yml. Have the IInput impls check for that variable.

There may already be an env variable set we can use.

But the error is no longer due to low-level functions because they no longer execute with the configuration in the test projects. The problem now is that not all injected keys have yet been processed by the input processor.


#region Modern Instance-Based Model Events (Thread-Local)

// Thread-local backing fields for events - each thread has its own subscribers
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static (int exitCode, string result) Process (
{
var output = string.Empty;

if (Console.IsInputRedirected || Console.IsOutputRedirected)
if (Application.IsRunningInTest ())
{
return (-1, output);
}
Expand Down
11 changes: 9 additions & 2 deletions Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,15 @@ public void Stop ()
_runCancellationTokenSource.Cancel ();
_output?.Dispose ();

// Wait for input infinite loop to exit
_inputTask?.Wait ();
try
{
// Wait for input infinite loop to exit
_inputTask?.Wait (_runCancellationTokenSource.Token);
}
catch
{
// ignored
}
}

private void BootMainLoop (IApplication? app)
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ public AnsiInput ()
try
{
// Check if we have a real console first
if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
if (Application.IsRunningInTest ())
{
Trace.Lifecycle (nameof (AnsiInput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", "Console is running unit tests. Running in degraded mode.");

return;
}
Expand Down
6 changes: 3 additions & 3 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace Terminal.Gui.Drivers;
public class AnsiOutput : OutputBase, IOutput
{
// Tracks which underlying platform APIs are in use
private readonly AnsiPlatform _platform;
internal readonly AnsiPlatform _platform;

private Size _consoleSize = new (80, 25);
private IOutputBuffer? _lastBuffer;
Expand All @@ -61,9 +61,9 @@ public AnsiOutput ()
try
{
// Check if we have a real console first
if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
if (Application.IsRunningInTest ())
{
Trace.Lifecycle (nameof (AnsiOutput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", "Console is running unit tests. Running in degraded mode.");

return;
}
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Drivers/AnsiDriver/WindowsVTInputHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public bool TryRead (byte [] buffer, out int bytesRead)
{
bytesRead = 0;

if (!IsEnabled || InputHandle == nint.Zero)
if (!IsEnabled || InputHandle == nint.Zero || !Console.KeyAvailable)
{
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public Size GetSize ()
{
try
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
if (Application.IsRunningInTest ())
{
return new Size (80, 25);
}
Expand Down Expand Up @@ -176,7 +176,7 @@ public void Suspend ()
Write (EscSeqUtils.CSI_DisableMouseEvents);

// Check if we have a real console first
if (Console.IsInputRedirected || Console.IsOutputRedirected)
if (Application.IsRunningInTest ())
{
Logging.Information ($"Console redirected (Output: {Console.IsOutputRedirected}, Input: {Console.IsInputRedirected}). Running in degraded mode.");

Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Drivers/Input/TestInputSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class TestInputSource : IInputSource
/// Initializes a new instance of the <see cref="TestInputSource"/> class.
/// </summary>
/// <param name="timeProvider">The time provider for timestamps.</param>
public TestInputSource (ITimeProvider timeProvider) { TimeProvider = timeProvider; }
public TestInputSource (ITimeProvider timeProvider) => TimeProvider = timeProvider;

/// <inheritdoc/>
public ITimeProvider TimeProvider { get; }
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/Drivers/UnixDriver/UnixTerminalHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ public static void Suspend (IOutput output)
output.Write (EscSeqUtils.CSI_DisableMouseEvents);

// Check if we have a real console first
if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
if (Application.IsRunningInTest ())
{
Trace.Lifecycle (nameof (UnixTerminalHelper), "Suspend", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", "Console is running unit tests. Running in degraded mode.");

return;
}
Expand Down
8 changes: 8 additions & 0 deletions Terminal.Gui/Testing/InputInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ public void ProcessQueue ()
{
_processor.ProcessQueue ();

if (_testSource is { IsAvailable: true })
{
foreach (InputEventRecord _ in _testSource.ReadAvailable ())
{
_processor.ProcessQueue ();
}
}

// If using virtual time and parser has stale escape sequences, advance time and process again
if (_timeProvider is not VirtualTimeProvider vtp)
{
Expand Down
9 changes: 8 additions & 1 deletion Tests/IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@
<Optimize>true</Optimize>
</PropertyGroup>

<ItemGroup>
<ItemGroup>
<RuntimeHostConfigurationOption
Include="Runtime.IsTestProject"
Value="true"
Trim="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
9 changes: 8 additions & 1 deletion Tests/StressTests/StressTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@
<ImplicitUsings>enable</ImplicitUsings>
<NoLogo>true</NoLogo>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>

</PropertyGroup>

<ItemGroup>
<RuntimeHostConfigurationOption
Include="Runtime.IsTestProject"
Value="true"
Trim="false" />
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineDebug>true</DefineDebug>
<DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
Expand Down
8 changes: 8 additions & 0 deletions Tests/UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Optimize>true</Optimize>
</PropertyGroup>

<ItemGroup>
<RuntimeHostConfigurationOption
Include="Runtime.IsTestProject"
Value="true"
Trim="false" />
</ItemGroup>

<ItemGroup>
<Compile Remove="Application\Keyboard\**" />
<Compile Remove="Drivers\**" />
<EmbeddedResource Remove="Application\Keyboard\**" />
Expand Down
19 changes: 7 additions & 12 deletions Tests/UnitTestsParallelizable/Input/InputInjectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,7 @@ public void InjectKey_DefaultOptions_UsesDirectMode ()

#region InjectKey Tests - Pipeline Mode

// BUGBUG: This test is bogus as it doesn't actually test what happens
// BUGBUG: when an accented char comes into the actual stdIn stream, only.
// BUGBUG: see https://github.com/gui-cs/Terminal.Gui/pull/4583#issuecomment-3769142085

[Fact (Skip = "Using Task.Delay (50) will cause failures in slow CI/CD runners")]
[Fact]
public async Task InjectKey_Pipeline_AccentedKeys_RaisesAllEvents ()
{
// Arrange
Expand Down Expand Up @@ -181,11 +177,12 @@ public async Task InjectKey_Pipeline_AccentedKeys_RaisesAllEvents ()
injector.InjectKey (new Key ('Ã'), options);
injector.InjectKey (new Key ('Õ'), options);

// BUGBUG: This is a hack; we need to figure out how to enable this without delay
await Task.Delay (50, TestContext.Current.CancellationToken); // Allow some time for processing
injector.ProcessQueue ();

// Assert - Should raise exactly 3 KeyDown events
Assert.Equal (AnsiPlatform.Degraded, ((AnsiOutput)app.Driver?.GetOutput ()!)._platform);

// Assert - Should raise exactly 34 KeyDown events
Assert.Equal (34, receivedKeys.Count);
Assert.Equal (new Key ('á'), receivedKeys [0]);
Assert.Equal (new Key ('é'), receivedKeys [1]);
Expand Down Expand Up @@ -223,10 +220,7 @@ public async Task InjectKey_Pipeline_AccentedKeys_RaisesAllEvents ()
Assert.Equal (new Key ('Õ'), receivedKeys [33]);
}

// BUGBUG: This test is bogus as it doesn't actually test what happens
// BUGBUG: when an accented char comes into the actual stdIn stream, only.
// BUGBUG: see https://github.com/gui-cs/Terminal.Gui/pull/4583#issuecomment-3769142085
[Fact (Skip = "Using Task.Delay (50) will cause failures in slow CI/CD runners")]
[Fact]
public async Task InjectKey_PipelineMode_MultipleKeys_RaisesAllEvents ()
{
// Arrange
Expand All @@ -250,10 +244,11 @@ public async Task InjectKey_PipelineMode_MultipleKeys_RaisesAllEvents ()
injector.InjectKey (Key.B, options);
injector.InjectKey (Key.C, options);

// BUGBUG: This is a hack; we need to figure out how to enable this without delay
await Task.Delay (50, TestContext.Current.CancellationToken); // Allow some time for processing
injector.ProcessQueue ();

Assert.Equal (AnsiPlatform.Degraded, ((AnsiOutput)app.Driver?.GetOutput ()!)._platform);

// Assert - Should raise exactly 3 KeyDown events
Assert.Equal (3, receivedKeys.Count);
Assert.Equal (Key.A, receivedKeys [0]);
Expand Down
7 changes: 7 additions & 0 deletions Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
</PropertyGroup>

<ItemGroup>
<RuntimeHostConfigurationOption
Include="Runtime.IsTestProject"
Value="true"
Trim="false" />
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineDebug>true</DefineDebug>
<DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
Expand Down
Loading