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] 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 5f4284bdc..db5be73a0 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 class FakeComponentFactory : ComponentFactoryImpl /// 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 000000000..6c73508f7 --- /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 000000000..f22ce8fbf --- /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 000000000..8a29909d1 --- /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 000000000..2bdf23ccc --- /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 000000000..8bcce2a48 --- /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 000000000..40fd86866 --- /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 000000000..6416cbdda --- /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 000000000..bf8f2069b --- /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 000000000..32049d0b8 --- /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 000000000..005714c89 --- /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 000000000..42cd7ff47 --- /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 +}