From 175a6439bb1d27e0343b3dffc1aa93fa5c3bf307 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Dec 2025 21:42:47 +0000
Subject: [PATCH 01/14] Initial plan
From ea5eabf6e3a31005587f361d7de2e7be71d6b947 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Dec 2025 21:54:57 +0000
Subject: [PATCH 02/14] Phase 1: Add example infrastructure attributes and
classes
- Added ExampleMetadataAttribute, ExampleCategoryAttribute, ExampleDemoKeyStrokesAttribute
- Added ExampleContext, ExecutionMode, ExampleInfo, ExampleResult, ExampleMetrics classes
- Added ExampleDiscovery and ExampleRunner static classes
- Updated FakeComponentFactory to support context injection via environment variable
- Built successfully with no errors
Co-authored-by: tig <585482+tig@users.noreply.github.com>
---
.../FakeDriver/FakeComponentFactory.cs | 126 ++++++++++++-
.../Examples/DemoKeyStrokeSequence.cs | 22 +++
.../Examples/ExampleCategoryAttribute.cs | 35 ++++
Terminal.Gui/Examples/ExampleContext.cs | 76 ++++++++
.../ExampleDemoKeyStrokesAttribute.cs | 50 +++++
Terminal.Gui/Examples/ExampleDiscovery.cs | 121 ++++++++++++
Terminal.Gui/Examples/ExampleInfo.cs | 41 ++++
.../Examples/ExampleMetadataAttribute.cs | 41 ++++
Terminal.Gui/Examples/ExampleMetrics.cs | 52 ++++++
Terminal.Gui/Examples/ExampleResult.cs | 42 +++++
Terminal.Gui/Examples/ExampleRunner.cs | 176 ++++++++++++++++++
Terminal.Gui/Examples/ExecutionMode.cs | 19 ++
12 files changed, 800 insertions(+), 1 deletion(-)
create mode 100644 Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
create mode 100644 Terminal.Gui/Examples/ExampleCategoryAttribute.cs
create mode 100644 Terminal.Gui/Examples/ExampleContext.cs
create mode 100644 Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
create mode 100644 Terminal.Gui/Examples/ExampleDiscovery.cs
create mode 100644 Terminal.Gui/Examples/ExampleInfo.cs
create mode 100644 Terminal.Gui/Examples/ExampleMetadataAttribute.cs
create mode 100644 Terminal.Gui/Examples/ExampleMetrics.cs
create mode 100644 Terminal.Gui/Examples/ExampleResult.cs
create mode 100644 Terminal.Gui/Examples/ExampleRunner.cs
create mode 100644 Terminal.Gui/Examples/ExecutionMode.cs
diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
index 5f4284bdc5..db5be73a0c 100644
--- a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
+++ b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
+using Terminal.Gui.Examples;
namespace Terminal.Gui.Drivers;
@@ -35,7 +36,130 @@ public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBu
///
public override IInput CreateInput ()
{
- return _input ?? new FakeInput ();
+ FakeInput fakeInput = _input ?? new FakeInput ();
+
+ // Check for test context in environment variable
+ string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.EnvironmentVariableName);
+
+ if (!string.IsNullOrEmpty (contextJson))
+ {
+ ExampleContext? context = ExampleContext.FromJson (contextJson);
+
+ if (context is { })
+ {
+ foreach (string keyStr in context.KeysToInject)
+ {
+ if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { })
+ {
+ ConsoleKeyInfo consoleKeyInfo = ConvertKeyToConsoleKeyInfo (key);
+ fakeInput.AddInput (consoleKeyInfo);
+ }
+ }
+ }
+ }
+
+ return fakeInput;
+ }
+
+ private static ConsoleKeyInfo ConvertKeyToConsoleKeyInfo (Input.Key key)
+ {
+ ConsoleModifiers modifiers = 0;
+
+ if (key.IsShift)
+ {
+ modifiers |= ConsoleModifiers.Shift;
+ }
+
+ if (key.IsAlt)
+ {
+ modifiers |= ConsoleModifiers.Alt;
+ }
+
+ if (key.IsCtrl)
+ {
+ modifiers |= ConsoleModifiers.Control;
+ }
+
+ // Remove the modifier masks to get the base key code
+ KeyCode baseKeyCode = key.KeyCode & KeyCode.CharMask;
+
+ // Map KeyCode to ConsoleKey
+ ConsoleKey consoleKey = baseKeyCode switch
+ {
+ KeyCode.A => ConsoleKey.A,
+ KeyCode.B => ConsoleKey.B,
+ KeyCode.C => ConsoleKey.C,
+ KeyCode.D => ConsoleKey.D,
+ KeyCode.E => ConsoleKey.E,
+ KeyCode.F => ConsoleKey.F,
+ KeyCode.G => ConsoleKey.G,
+ KeyCode.H => ConsoleKey.H,
+ KeyCode.I => ConsoleKey.I,
+ KeyCode.J => ConsoleKey.J,
+ KeyCode.K => ConsoleKey.K,
+ KeyCode.L => ConsoleKey.L,
+ KeyCode.M => ConsoleKey.M,
+ KeyCode.N => ConsoleKey.N,
+ KeyCode.O => ConsoleKey.O,
+ KeyCode.P => ConsoleKey.P,
+ KeyCode.Q => ConsoleKey.Q,
+ KeyCode.R => ConsoleKey.R,
+ KeyCode.S => ConsoleKey.S,
+ KeyCode.T => ConsoleKey.T,
+ KeyCode.U => ConsoleKey.U,
+ KeyCode.V => ConsoleKey.V,
+ KeyCode.W => ConsoleKey.W,
+ KeyCode.X => ConsoleKey.X,
+ KeyCode.Y => ConsoleKey.Y,
+ KeyCode.Z => ConsoleKey.Z,
+ KeyCode.D0 => ConsoleKey.D0,
+ KeyCode.D1 => ConsoleKey.D1,
+ KeyCode.D2 => ConsoleKey.D2,
+ KeyCode.D3 => ConsoleKey.D3,
+ KeyCode.D4 => ConsoleKey.D4,
+ KeyCode.D5 => ConsoleKey.D5,
+ KeyCode.D6 => ConsoleKey.D6,
+ KeyCode.D7 => ConsoleKey.D7,
+ KeyCode.D8 => ConsoleKey.D8,
+ KeyCode.D9 => ConsoleKey.D9,
+ KeyCode.Enter => ConsoleKey.Enter,
+ KeyCode.Esc => ConsoleKey.Escape,
+ KeyCode.Space => ConsoleKey.Spacebar,
+ KeyCode.Tab => ConsoleKey.Tab,
+ KeyCode.Backspace => ConsoleKey.Backspace,
+ KeyCode.Delete => ConsoleKey.Delete,
+ KeyCode.Home => ConsoleKey.Home,
+ KeyCode.End => ConsoleKey.End,
+ KeyCode.PageUp => ConsoleKey.PageUp,
+ KeyCode.PageDown => ConsoleKey.PageDown,
+ KeyCode.CursorUp => ConsoleKey.UpArrow,
+ KeyCode.CursorDown => ConsoleKey.DownArrow,
+ KeyCode.CursorLeft => ConsoleKey.LeftArrow,
+ KeyCode.CursorRight => ConsoleKey.RightArrow,
+ KeyCode.F1 => ConsoleKey.F1,
+ KeyCode.F2 => ConsoleKey.F2,
+ KeyCode.F3 => ConsoleKey.F3,
+ KeyCode.F4 => ConsoleKey.F4,
+ KeyCode.F5 => ConsoleKey.F5,
+ KeyCode.F6 => ConsoleKey.F6,
+ KeyCode.F7 => ConsoleKey.F7,
+ KeyCode.F8 => ConsoleKey.F8,
+ KeyCode.F9 => ConsoleKey.F9,
+ KeyCode.F10 => ConsoleKey.F10,
+ KeyCode.F11 => ConsoleKey.F11,
+ KeyCode.F12 => ConsoleKey.F12,
+ _ => (ConsoleKey)0
+ };
+
+ var keyChar = '\0';
+ Rune rune = key.AsRune;
+
+ if (Rune.IsValid (rune.Value))
+ {
+ keyChar = (char)rune.Value;
+ }
+
+ return new (keyChar, consoleKey, key.IsShift, key.IsAlt, key.IsCtrl);
}
///
diff --git a/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
new file mode 100644
index 0000000000..6c73508f77
--- /dev/null
+++ b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
@@ -0,0 +1,22 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Represents a sequence of keystrokes to inject during example demonstration or testing.
+///
+public class DemoKeyStrokeSequence
+{
+ ///
+ /// Gets or sets the array of keystroke names to inject.
+ ///
+ public string [] KeyStrokes { get; set; } = [];
+
+ ///
+ /// Gets or sets the delay in milliseconds before injecting these keystrokes.
+ ///
+ public int DelayMs { get; set; } = 0;
+
+ ///
+ /// Gets or sets the order in which this sequence should be executed.
+ ///
+ public int Order { get; set; } = 0;
+}
diff --git a/Terminal.Gui/Examples/ExampleCategoryAttribute.cs b/Terminal.Gui/Examples/ExampleCategoryAttribute.cs
new file mode 100644
index 0000000000..f22ce8fbfa
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleCategoryAttribute.cs
@@ -0,0 +1,35 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines a category for an example application.
+/// Apply this attribute to an assembly to associate it with one or more categories for organization and filtering.
+///
+///
+///
+/// Multiple instances of this attribute can be applied to a single assembly to associate the example
+/// with multiple categories.
+///
+///
+///
+///
+/// [assembly: ExampleCategory("Text and Formatting")]
+/// [assembly: ExampleCategory("Controls")]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+public class ExampleCategoryAttribute : System.Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The category name.
+ public ExampleCategoryAttribute (string category)
+ {
+ Category = category;
+ }
+
+ ///
+ /// Gets or sets the category name.
+ ///
+ public string Category { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleContext.cs b/Terminal.Gui/Examples/ExampleContext.cs
new file mode 100644
index 0000000000..8a29909d1b
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleContext.cs
@@ -0,0 +1,76 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines the execution context for running an example application.
+/// This context is used to configure how an example should be executed, including driver selection,
+/// keystroke injection, timeouts, and metrics collection.
+///
+public class ExampleContext
+{
+ ///
+ /// Gets or sets the name of the driver to use (e.g., "FakeDriver", "DotnetDriver").
+ /// If , the default driver for the platform is used.
+ ///
+ public string? DriverName { get; set; } = null;
+
+ ///
+ /// Gets or sets the list of key names to inject into the example during execution.
+ /// Each string should be a valid key name that can be parsed by .
+ ///
+ public List KeysToInject { get; set; } = new ();
+
+ ///
+ /// Gets or sets the maximum time in milliseconds to allow the example to run before forcibly terminating it.
+ ///
+ public int TimeoutMs { get; set; } = 30000;
+
+ ///
+ /// Gets or sets the maximum number of iterations to allow before stopping the example.
+ /// If set to -1, no iteration limit is enforced.
+ ///
+ public int MaxIterations { get; set; } = -1;
+
+ ///
+ /// Gets or sets a value indicating whether to collect and report performance metrics during execution.
+ ///
+ public bool CollectMetrics { get; set; } = false;
+
+ ///
+ /// Gets or sets the execution mode for the example.
+ ///
+ public ExecutionMode Mode { get; set; } = ExecutionMode.OutOfProcess;
+
+ ///
+ /// The name of the environment variable used to pass the serialized
+ /// to example applications.
+ ///
+ public const string EnvironmentVariableName = "TERMGUI_TEST_CONTEXT";
+
+ ///
+ /// Serializes this context to a JSON string for passing via environment variables.
+ ///
+ /// A JSON string representation of this context.
+ public string ToJson ()
+ {
+ return JsonSerializer.Serialize (this);
+ }
+
+ ///
+ /// Deserializes a from a JSON string.
+ ///
+ /// The JSON string to deserialize.
+ /// The deserialized context, or if deserialization fails.
+ public static ExampleContext? FromJson (string json)
+ {
+ try
+ {
+ return JsonSerializer.Deserialize (json);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
new file mode 100644
index 0000000000..2bdf23cccb
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
@@ -0,0 +1,50 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines keystrokes to be automatically injected when the example is run in demo or test mode.
+/// Apply this attribute to an assembly to specify automated input sequences for demonstration or testing purposes.
+///
+///
+///
+/// Multiple instances of this attribute can be applied to a single assembly to define a sequence
+/// of keystroke injections. The property controls the execution sequence.
+///
+///
+///
+///
+/// [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1, DelayMs = 100)]
+/// [assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Enter" }, Order = 2, DelayMs = 200)]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+public class ExampleDemoKeyStrokesAttribute : System.Attribute
+{
+ ///
+ /// Gets or sets an array of keystroke names to inject.
+ /// Each string should be a valid key name that can be parsed by .
+ ///
+ public string []? KeyStrokes { get; set; }
+
+ ///
+ /// Gets or sets the name of a single key to repeat multiple times.
+ /// This is a convenience for repeating the same keystroke.
+ ///
+ public string? RepeatKey { get; set; }
+
+ ///
+ /// Gets or sets the number of times to repeat .
+ /// Only used when is specified.
+ ///
+ public int RepeatCount { get; set; } = 1;
+
+ ///
+ /// Gets or sets the delay in milliseconds before injecting these keystrokes.
+ ///
+ public int DelayMs { get; set; } = 0;
+
+ ///
+ /// Gets or sets the order in which this keystroke sequence should be executed
+ /// relative to other instances.
+ ///
+ public int Order { get; set; } = 0;
+}
diff --git a/Terminal.Gui/Examples/ExampleDiscovery.cs b/Terminal.Gui/Examples/ExampleDiscovery.cs
new file mode 100644
index 0000000000..8bcce2a480
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleDiscovery.cs
@@ -0,0 +1,121 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Provides methods for discovering example applications by scanning assemblies for example metadata attributes.
+///
+public static class ExampleDiscovery
+{
+ ///
+ /// Discovers examples from the specified assembly file paths.
+ ///
+ /// The paths to assembly files to scan for examples.
+ /// An enumerable of objects for each discovered example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static IEnumerable DiscoverFromFiles (params string [] assemblyPaths)
+ {
+ foreach (string path in assemblyPaths)
+ {
+ if (!File.Exists (path))
+ {
+ continue;
+ }
+
+ Assembly? asm = null;
+
+ try
+ {
+ asm = Assembly.LoadFrom (path);
+ }
+ catch
+ {
+ // Skip assemblies that can't be loaded
+ continue;
+ }
+
+ ExampleMetadataAttribute? metadata = asm.GetCustomAttribute ();
+
+ if (metadata is null)
+ {
+ continue;
+ }
+
+ ExampleInfo info = new ()
+ {
+ Name = metadata.Name,
+ Description = metadata.Description,
+ AssemblyPath = path,
+ Categories = asm.GetCustomAttributes ()
+ .Select (c => c.Category)
+ .ToList (),
+ DemoKeyStrokes = ParseDemoKeyStrokes (asm)
+ };
+
+ yield return info;
+ }
+ }
+
+ ///
+ /// Discovers examples from assemblies in the specified directory.
+ ///
+ /// The directory to search for assembly files.
+ /// The search pattern for assembly files (default is "*.dll").
+ /// The search option for traversing subdirectories.
+ /// An enumerable of objects for each discovered example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static IEnumerable DiscoverFromDirectory (
+ string directory,
+ string searchPattern = "*.dll",
+ SearchOption searchOption = SearchOption.AllDirectories
+ )
+ {
+ if (!Directory.Exists (directory))
+ {
+ return [];
+ }
+
+ string [] assemblyPaths = Directory.GetFiles (directory, searchPattern, searchOption);
+
+ return DiscoverFromFiles (assemblyPaths);
+ }
+
+ private static List ParseDemoKeyStrokes (Assembly assembly)
+ {
+ List sequences = new ();
+
+ foreach (ExampleDemoKeyStrokesAttribute attr in assembly.GetCustomAttributes ())
+ {
+ List keys = new ();
+
+ if (attr.KeyStrokes is { Length: > 0 })
+ {
+ keys.AddRange (attr.KeyStrokes);
+ }
+
+ if (!string.IsNullOrEmpty (attr.RepeatKey))
+ {
+ for (var i = 0; i < attr.RepeatCount; i++)
+ {
+ keys.Add (attr.RepeatKey);
+ }
+ }
+
+ if (keys.Count > 0)
+ {
+ sequences.Add (
+ new ()
+ {
+ KeyStrokes = keys.ToArray (),
+ DelayMs = attr.DelayMs,
+ Order = attr.Order
+ });
+ }
+ }
+
+ return sequences.OrderBy (s => s.Order).ToList ();
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleInfo.cs b/Terminal.Gui/Examples/ExampleInfo.cs
new file mode 100644
index 0000000000..40fd868663
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleInfo.cs
@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains information about a discovered example application.
+///
+public class ExampleInfo
+{
+ ///
+ /// Gets or sets the display name of the example.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a description of what the example demonstrates.
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the full path to the example's assembly file.
+ ///
+ public string AssemblyPath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the list of categories this example belongs to.
+ ///
+ public List Categories { get; set; } = new ();
+
+ ///
+ /// Gets or sets the demo keystroke sequences defined for this example.
+ ///
+ public List DemoKeyStrokes { get; set; } = new ();
+
+ ///
+ /// Returns a string representation of this example info.
+ ///
+ /// A string containing the name and description.
+ public override string ToString ()
+ {
+ return $"{Name}: {Description}";
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleMetadataAttribute.cs b/Terminal.Gui/Examples/ExampleMetadataAttribute.cs
new file mode 100644
index 0000000000..6416cbdda0
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleMetadataAttribute.cs
@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines metadata (Name and Description) for an example application.
+/// Apply this attribute to an assembly to mark it as an example that can be discovered and run.
+///
+///
+///
+/// This attribute is used by the example discovery system to identify and describe standalone example programs.
+/// Each example should have exactly one applied to its assembly.
+///
+///
+///
+///
+/// [assembly: ExampleMetadata("Character Map", "Unicode character viewer and selector")]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly)]
+public class ExampleMetadataAttribute : System.Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The display name of the example.
+ /// A brief description of what the example demonstrates.
+ public ExampleMetadataAttribute (string name, string description)
+ {
+ Name = name;
+ Description = description;
+ }
+
+ ///
+ /// Gets or sets the display name of the example.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets a brief description of what the example demonstrates.
+ ///
+ public string Description { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleMetrics.cs b/Terminal.Gui/Examples/ExampleMetrics.cs
new file mode 100644
index 0000000000..bf8f2069bb
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleMetrics.cs
@@ -0,0 +1,52 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains performance and execution metrics collected during an example's execution.
+///
+public class ExampleMetrics
+{
+ ///
+ /// Gets or sets the time when the example started.
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// Gets or sets the time when initialization completed.
+ ///
+ public DateTime? InitializedAt { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether initialization completed successfully.
+ ///
+ public bool InitializedSuccessfully { get; set; }
+
+ ///
+ /// Gets or sets the number of iterations executed.
+ ///
+ public int IterationCount { get; set; }
+
+ ///
+ /// Gets or sets the time when shutdown began.
+ ///
+ public DateTime? ShutdownAt { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether shutdown completed gracefully.
+ ///
+ public bool ShutdownGracefully { get; set; }
+
+ ///
+ /// Gets or sets the number of times the screen was cleared.
+ ///
+ public int ClearedContentCount { get; set; }
+
+ ///
+ /// Gets or sets the number of times views were drawn.
+ ///
+ public int DrawCompleteCount { get; set; }
+
+ ///
+ /// Gets or sets the number of times views were laid out.
+ ///
+ public int LaidOutCount { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleResult.cs b/Terminal.Gui/Examples/ExampleResult.cs
new file mode 100644
index 0000000000..32049d0b88
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleResult.cs
@@ -0,0 +1,42 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains the result of running an example application.
+///
+public class ExampleResult
+{
+ ///
+ /// Gets or sets a value indicating whether the example completed successfully.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Gets or sets the exit code of the example process (for out-of-process execution).
+ ///
+ public int? ExitCode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the example timed out.
+ ///
+ public bool TimedOut { get; set; }
+
+ ///
+ /// Gets or sets any error message that occurred during execution.
+ ///
+ public string? ErrorMessage { get; set; }
+
+ ///
+ /// Gets or sets the performance metrics collected during execution.
+ ///
+ public ExampleMetrics? Metrics { get; set; }
+
+ ///
+ /// Gets or sets the standard output captured during execution.
+ ///
+ public string? StandardOutput { get; set; }
+
+ ///
+ /// Gets or sets the standard error captured during execution.
+ ///
+ public string? StandardError { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleRunner.cs b/Terminal.Gui/Examples/ExampleRunner.cs
new file mode 100644
index 0000000000..005714c89b
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleRunner.cs
@@ -0,0 +1,176 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Provides methods for running example applications in various execution modes.
+///
+public static class ExampleRunner
+{
+ ///
+ /// Runs an example with the specified context.
+ ///
+ /// The example information.
+ /// The execution context.
+ /// The result of running the example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static ExampleResult Run (ExampleInfo example, ExampleContext context)
+ {
+ return context.Mode == ExecutionMode.InProcess
+ ? RunInProcess (example, context)
+ : RunOutOfProcess (example, context);
+ }
+
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ private static ExampleResult RunInProcess (ExampleInfo example, ExampleContext context)
+ {
+ Environment.SetEnvironmentVariable (
+ ExampleContext.EnvironmentVariableName,
+ context.ToJson ());
+
+ try
+ {
+ Assembly asm = Assembly.LoadFrom (example.AssemblyPath);
+ MethodInfo? entryPoint = asm.EntryPoint;
+
+ if (entryPoint is null)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = "Assembly does not have an entry point"
+ };
+ }
+
+ ParameterInfo [] parameters = entryPoint.GetParameters ();
+ object? result = null;
+
+ if (parameters.Length == 0)
+ {
+ result = entryPoint.Invoke (null, null);
+ }
+ else if (parameters.Length == 1 && parameters [0].ParameterType == typeof (string []))
+ {
+ result = entryPoint.Invoke (null, new object [] { Array.Empty () });
+ }
+ else
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = "Entry point has unsupported signature"
+ };
+ }
+
+ // If entry point returns Task, wait for it
+ if (result is Task task)
+ {
+ task.Wait ();
+ }
+
+ return new ()
+ {
+ Success = true
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = ex.ToString ()
+ };
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable (ExampleContext.EnvironmentVariableName, null);
+ }
+ }
+
+ private static ExampleResult RunOutOfProcess (ExampleInfo example, ExampleContext context)
+ {
+ ProcessStartInfo psi = new ()
+ {
+ FileName = "dotnet",
+ Arguments = $"\"{example.AssemblyPath}\"",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ psi.Environment [ExampleContext.EnvironmentVariableName] = context.ToJson ();
+
+ using Process? process = Process.Start (psi);
+
+ if (process is null)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = "Failed to start process"
+ };
+ }
+
+ bool exited = process.WaitForExit (context.TimeoutMs);
+ string stdout = process.StandardOutput.ReadToEnd ();
+ string stderr = process.StandardError.ReadToEnd ();
+
+ if (!exited)
+ {
+ try
+ {
+ process.Kill (true);
+ }
+ catch
+ {
+ // Ignore errors killing the process
+ }
+
+ return new ()
+ {
+ Success = false,
+ TimedOut = true,
+ StandardOutput = stdout,
+ StandardError = stderr
+ };
+ }
+
+ ExampleMetrics? metrics = ExtractMetricsFromOutput (stdout);
+
+ return new ()
+ {
+ Success = process.ExitCode == 0,
+ ExitCode = process.ExitCode,
+ StandardOutput = stdout,
+ StandardError = stderr,
+ Metrics = metrics
+ };
+ }
+
+ private static ExampleMetrics? ExtractMetricsFromOutput (string output)
+ {
+ // Look for the metrics marker in the output
+ Match match = Regex.Match (output, @"###TERMGUI_METRICS:(.+?)###");
+
+ if (!match.Success)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize (match.Groups [1].Value);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/Terminal.Gui/Examples/ExecutionMode.cs b/Terminal.Gui/Examples/ExecutionMode.cs
new file mode 100644
index 0000000000..42cd7ff47b
--- /dev/null
+++ b/Terminal.Gui/Examples/ExecutionMode.cs
@@ -0,0 +1,19 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines the execution mode for running an example application.
+///
+public enum ExecutionMode
+{
+ ///
+ /// Run the example in a separate process.
+ /// This provides full isolation but makes debugging more difficult.
+ ///
+ OutOfProcess,
+
+ ///
+ /// Run the example in the same process by loading its assembly and invoking its entry point.
+ /// This allows for easier debugging but may have side effects from shared process state.
+ ///
+ InProcess
+}
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 03/14] 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 9d3fd863f0..de89c45a97 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 026a981345..de7d8aaa33 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 1eb5e9e119..e0d423819b 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 0000000000..123f65374d
--- /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