Browse Source

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 <[email protected]>
copilot-swe-agent[bot] 1 week ago
parent
commit
cd392456ca

+ 18 - 1
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<ExampleWindow> ();
 

+ 18 - 1
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<ColorPickerView> ();
 
 // Run the application with fluent API - automatically creates, runs, and disposes the runnable

+ 21 - 1
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" };

+ 154 - 0
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;
+
+/// <summary>
+///     Tests for the example discovery and execution infrastructure.
+/// </summary>
+public class ExampleTests
+{
+    private readonly ITestOutputHelper _output;
+
+    public ExampleTests (ITestOutputHelper output)
+    {
+        _output = output;
+    }
+
+    /// <summary>
+    ///     Discovers all examples by looking for assemblies with ExampleMetadata attributes.
+    /// </summary>
+    /// <returns>Test data for all discovered examples.</returns>
+    [RequiresUnreferencedCode ("Calls ExampleDiscovery.DiscoverFromDirectory")]
+    [RequiresDynamicCode ("Calls ExampleDiscovery.DiscoverFromDirectory")]
+    public static IEnumerable<object []> AllExamples ()
+    {
+        string examplesDir = Path.GetFullPath (Path.Combine (AppContext.BaseDirectory, "..", "..", "..", "..", "..", "Examples"));
+
+        if (!Directory.Exists (examplesDir))
+        {
+            return [];
+        }
+
+        List<ExampleInfo> 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);
+    }
+}