From cd392456ca826e49ca72a98e28e02b335516d2a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:06:23 +0000 Subject: [PATCH] Phase 2: Update examples with attributes and add test infrastructure - Updated Example, FluentExample, and RunnableWrapperExample with example attributes - Added support for driver name detection from test context in examples - Created ExampleTests class in UnitTestsParallelizable with tests for: - Example metadata validation - Out-of-process execution - In-process execution - Context serialization - Examples now properly detect and use FakeDriver from test context - Tests pass for metadata validation and serialization Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/Example/Example.cs | 19 ++- Examples/FluentExample/Program.cs | 19 ++- Examples/RunnableWrapperExample/Program.cs | 22 ++- .../Examples/ExampleTests.cs | 154 ++++++++++++++++++ 4 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 Tests/UnitTestsParallelizable/Examples/ExampleTests.cs diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index 9d3fd863f..de89c45a9 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -5,14 +5,31 @@ using Terminal.Gui.App; using Terminal.Gui.Configuration; +using Terminal.Gui.Examples; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; +[assembly: ExampleMetadata ("Simple Example", "A basic login form demonstrating Terminal.Gui fundamentals")] +[assembly: ExampleCategory ("Getting Started")] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter" }, Order = 1)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Enter" }, DelayMs = 500, Order = 2)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Esc" }, DelayMs = 100, Order = 3)] + // Override the default configuration for the application to use the Light theme ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; ConfigurationManager.Enable (ConfigLocations.All); -IApplication app = Application.Create (); +// Check for test context to determine driver +string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.EnvironmentVariableName); +string? driverName = null; + +if (!string.IsNullOrEmpty (contextJson)) +{ + ExampleContext? context = ExampleContext.FromJson (contextJson); + driverName = context?.DriverName; +} + +IApplication app = Application.Create ().Init (driverName); app.Run (); diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs index 026a98134..de7d8aaa3 100644 --- a/Examples/FluentExample/Program.cs +++ b/Examples/FluentExample/Program.cs @@ -2,11 +2,28 @@ using Terminal.Gui.App; using Terminal.Gui.Drawing; +using Terminal.Gui.Examples; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; +[assembly: ExampleMetadata ("Fluent API Example", "Demonstrates the fluent IApplication API with IRunnable pattern")] +[assembly: ExampleCategory ("API Patterns")] +[assembly: ExampleCategory ("Controls")] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "CursorDown", "CursorDown", "CursorRight", "Enter" }, Order = 1)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Esc" }, DelayMs = 100, Order = 2)] + +// Check for test context to determine driver +string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.EnvironmentVariableName); +string? driverName = null; + +if (!string.IsNullOrEmpty (contextJson)) +{ + ExampleContext? context = ExampleContext.FromJson (contextJson); + driverName = context?.DriverName; +} + IApplication? app = Application.Create () - .Init () + .Init (driverName) .Run (); // Run the application with fluent API - automatically creates, runs, and disposes the runnable diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 1eb5e9e11..e0d423819 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -2,11 +2,31 @@ using Terminal.Gui.App; using Terminal.Gui.Drawing; +using Terminal.Gui.Examples; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; +[assembly: ExampleMetadata ("Runnable Wrapper Example", "Shows how to wrap any View to make it runnable without implementing IRunnable")] +[assembly: ExampleCategory ("API Patterns")] +[assembly: ExampleCategory ("Views")] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "t", "e", "s", "t", "Esc" }, Order = 1)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Enter", "Esc" }, DelayMs = 100, Order = 2)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Enter", "Esc" }, DelayMs = 100, Order = 3)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Enter", "Esc" }, DelayMs = 100, Order = 4)] +[assembly: ExampleDemoKeyStrokes (KeyStrokes = new [] { "Enter", "Esc" }, DelayMs = 100, Order = 5)] + +// Check for test context to determine driver +string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.EnvironmentVariableName); +string? driverName = null; + +if (!string.IsNullOrEmpty (contextJson)) +{ + ExampleContext? context = ExampleContext.FromJson (contextJson); + driverName = context?.DriverName; +} + IApplication app = Application.Create (); -app.Init (); +app.Init (driverName); // Example 1: Use extension method with result extraction var textField = new TextField { Width = 40, Text = "Default text" }; diff --git a/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs b/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs new file mode 100644 index 000000000..123f65374 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs @@ -0,0 +1,154 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; +using Terminal.Gui.Examples; +using Xunit.Abstractions; + +namespace UnitTests.Parallelizable.Examples; + +/// +/// Tests for the example discovery and execution infrastructure. +/// +public class ExampleTests +{ + private readonly ITestOutputHelper _output; + + public ExampleTests (ITestOutputHelper output) + { + _output = output; + } + + /// + /// Discovers all examples by looking for assemblies with ExampleMetadata attributes. + /// + /// Test data for all discovered examples. + [RequiresUnreferencedCode ("Calls ExampleDiscovery.DiscoverFromDirectory")] + [RequiresDynamicCode ("Calls ExampleDiscovery.DiscoverFromDirectory")] + public static IEnumerable AllExamples () + { + string examplesDir = Path.GetFullPath (Path.Combine (AppContext.BaseDirectory, "..", "..", "..", "..", "..", "Examples")); + + if (!Directory.Exists (examplesDir)) + { + return []; + } + + List examples = ExampleDiscovery.DiscoverFromDirectory (examplesDir).ToList (); + + if (examples.Count == 0) + { + return []; + } + + return examples.Select (e => new object [] { e }); + } + + [Theory] + [MemberData (nameof (AllExamples))] + public void Example_Has_Metadata (ExampleInfo example) + { + Assert.NotNull (example); + Assert.False (string.IsNullOrWhiteSpace (example.Name), "Example name should not be empty"); + Assert.False (string.IsNullOrWhiteSpace (example.Description), "Example description should not be empty"); + Assert.True (File.Exists (example.AssemblyPath), $"Example assembly should exist: {example.AssemblyPath}"); + + _output.WriteLine ($"Example: {example.Name}"); + _output.WriteLine ($" Description: {example.Description}"); + _output.WriteLine ($" Categories: {string.Join (", ", example.Categories)}"); + _output.WriteLine ($" Assembly: {example.AssemblyPath}"); + } + + [Theory] + [MemberData (nameof (AllExamples))] + public void All_Examples_Quit_And_Init_Shutdown_Properly_OutOfProcess (ExampleInfo example) + { + _output.WriteLine ($"Running example '{example.Name}' out-of-process"); + + ExampleContext context = new () + { + DriverName = "FakeDriver", + KeysToInject = new () { "Esc" }, + TimeoutMs = 5000, + CollectMetrics = false, + Mode = ExecutionMode.OutOfProcess + }; + + ExampleResult result = ExampleRunner.Run (example, context); + + if (!result.Success) + { + _output.WriteLine ($"Example failed: {result.ErrorMessage}"); + + if (!string.IsNullOrEmpty (result.StandardOutput)) + { + _output.WriteLine ($"Standard Output:\n{result.StandardOutput}"); + } + + if (!string.IsNullOrEmpty (result.StandardError)) + { + _output.WriteLine ($"Standard Error:\n{result.StandardError}"); + } + } + + Assert.True (result.Success, $"Example '{example.Name}' should complete successfully"); + Assert.False (result.TimedOut, $"Example '{example.Name}' should not timeout"); + Assert.Equal (0, result.ExitCode); + } + + [Theory] + [MemberData (nameof (AllExamples))] + public void All_Examples_Quit_And_Init_Shutdown_Properly_InProcess (ExampleInfo example) + { + _output.WriteLine ($"Running example '{example.Name}' in-process"); + + // Force a complete reset to ensure clean state + Application.ResetState (true); + + ExampleContext context = new () + { + DriverName = "FakeDriver", + KeysToInject = new () { "Esc" }, + TimeoutMs = 5000, + CollectMetrics = false, + Mode = ExecutionMode.InProcess + }; + + ExampleResult result = ExampleRunner.Run (example, context); + + if (!result.Success) + { + _output.WriteLine ($"Example failed: {result.ErrorMessage}"); + } + + // Reset state after in-process execution + Application.ResetState (true); + + Assert.True (result.Success, $"Example '{example.Name}' should complete successfully"); + Assert.False (result.TimedOut, $"Example '{example.Name}' should not timeout"); + } + + [Fact] + public void ExampleContext_Serialization_Works () + { + ExampleContext context = new () + { + DriverName = "FakeDriver", + KeysToInject = new () { "Esc", "Enter" }, + TimeoutMs = 5000, + MaxIterations = 100, + CollectMetrics = true, + Mode = ExecutionMode.InProcess + }; + + string json = context.ToJson (); + Assert.False (string.IsNullOrWhiteSpace (json)); + + ExampleContext? deserialized = ExampleContext.FromJson (json); + Assert.NotNull (deserialized); + Assert.Equal (context.DriverName, deserialized.DriverName); + Assert.Equal (context.TimeoutMs, deserialized.TimeoutMs); + Assert.Equal (context.MaxIterations, deserialized.MaxIterations); + Assert.Equal (context.CollectMetrics, deserialized.CollectMetrics); + Assert.Equal (context.Mode, deserialized.Mode); + Assert.Equal (context.KeysToInject.Count, deserialized.KeysToInject.Count); + } +}