mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 17:57:57 +01:00
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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Terminal.Gui.Examples;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
@@ -35,7 +36,130 @@ public class FakeComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
|
||||
/// <inheritdoc/>
|
||||
public override IInput<ConsoleKeyInfo> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
22
Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
Normal file
22
Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a sequence of keystrokes to inject during example demonstration or testing.
|
||||
/// </summary>
|
||||
public class DemoKeyStrokeSequence
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the array of keystroke names to inject.
|
||||
/// </summary>
|
||||
public string [] KeyStrokes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay in milliseconds before injecting these keystrokes.
|
||||
/// </summary>
|
||||
public int DelayMs { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the order in which this sequence should be executed.
|
||||
/// </summary>
|
||||
public int Order { get; set; } = 0;
|
||||
}
|
||||
35
Terminal.Gui/Examples/ExampleCategoryAttribute.cs
Normal file
35
Terminal.Gui/Examples/ExampleCategoryAttribute.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Multiple instances of this attribute can be applied to a single assembly to associate the example
|
||||
/// with multiple categories.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// [assembly: ExampleCategory("Text and Formatting")]
|
||||
/// [assembly: ExampleCategory("Controls")]
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class ExampleCategoryAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExampleCategoryAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="category">The category name.</param>
|
||||
public ExampleCategoryAttribute (string category)
|
||||
{
|
||||
Category = category;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category name.
|
||||
/// </summary>
|
||||
public string Category { get; set; }
|
||||
}
|
||||
76
Terminal.Gui/Examples/ExampleContext.cs
Normal file
76
Terminal.Gui/Examples/ExampleContext.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ExampleContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the driver to use (e.g., "FakeDriver", "DotnetDriver").
|
||||
/// If <see langword="null"/>, the default driver for the platform is used.
|
||||
/// </summary>
|
||||
public string? DriverName { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Input.Key.TryParse"/>.
|
||||
/// </summary>
|
||||
public List<string> KeysToInject { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time in milliseconds to allow the example to run before forcibly terminating it.
|
||||
/// </summary>
|
||||
public int TimeoutMs { get; set; } = 30000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of iterations to allow before stopping the example.
|
||||
/// If set to -1, no iteration limit is enforced.
|
||||
/// </summary>
|
||||
public int MaxIterations { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to collect and report performance metrics during execution.
|
||||
/// </summary>
|
||||
public bool CollectMetrics { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution mode for the example.
|
||||
/// </summary>
|
||||
public ExecutionMode Mode { get; set; } = ExecutionMode.OutOfProcess;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the environment variable used to pass the serialized <see cref="ExampleContext"/>
|
||||
/// to example applications.
|
||||
/// </summary>
|
||||
public const string EnvironmentVariableName = "TERMGUI_TEST_CONTEXT";
|
||||
|
||||
/// <summary>
|
||||
/// Serializes this context to a JSON string for passing via environment variables.
|
||||
/// </summary>
|
||||
/// <returns>A JSON string representation of this context.</returns>
|
||||
public string ToJson ()
|
||||
{
|
||||
return JsonSerializer.Serialize (this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a <see cref="ExampleContext"/> from a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to deserialize.</param>
|
||||
/// <returns>The deserialized context, or <see langword="null"/> if deserialization fails.</returns>
|
||||
public static ExampleContext? FromJson (string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<ExampleContext> (json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
Normal file
50
Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Multiple instances of this attribute can be applied to a single assembly to define a sequence
|
||||
/// of keystroke injections. The <see cref="Order"/> property controls the execution sequence.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1, DelayMs = 100)]
|
||||
/// [assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Enter" }, Order = 2, DelayMs = 200)]
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class ExampleDemoKeyStrokesAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an array of keystroke names to inject.
|
||||
/// Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>.
|
||||
/// </summary>
|
||||
public string []? KeyStrokes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of a single key to repeat multiple times.
|
||||
/// This is a convenience for repeating the same keystroke.
|
||||
/// </summary>
|
||||
public string? RepeatKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times to repeat <see cref="RepeatKey"/>.
|
||||
/// Only used when <see cref="RepeatKey"/> is specified.
|
||||
/// </summary>
|
||||
public int RepeatCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay in milliseconds before injecting these keystrokes.
|
||||
/// </summary>
|
||||
public int DelayMs { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the order in which this keystroke sequence should be executed
|
||||
/// relative to other <see cref="ExampleDemoKeyStrokesAttribute"/> instances.
|
||||
/// </summary>
|
||||
public int Order { get; set; } = 0;
|
||||
}
|
||||
121
Terminal.Gui/Examples/ExampleDiscovery.cs
Normal file
121
Terminal.Gui/Examples/ExampleDiscovery.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for discovering example applications by scanning assemblies for example metadata attributes.
|
||||
/// </summary>
|
||||
public static class ExampleDiscovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovers examples from the specified assembly file paths.
|
||||
/// </summary>
|
||||
/// <param name="assemblyPaths">The paths to assembly files to scan for examples.</param>
|
||||
/// <returns>An enumerable of <see cref="ExampleInfo"/> objects for each discovered example.</returns>
|
||||
[RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
|
||||
[RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
|
||||
public static IEnumerable<ExampleInfo> 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<ExampleMetadataAttribute> ();
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ExampleInfo info = new ()
|
||||
{
|
||||
Name = metadata.Name,
|
||||
Description = metadata.Description,
|
||||
AssemblyPath = path,
|
||||
Categories = asm.GetCustomAttributes<ExampleCategoryAttribute> ()
|
||||
.Select (c => c.Category)
|
||||
.ToList (),
|
||||
DemoKeyStrokes = ParseDemoKeyStrokes (asm)
|
||||
};
|
||||
|
||||
yield return info;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers examples from assemblies in the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory to search for assembly files.</param>
|
||||
/// <param name="searchPattern">The search pattern for assembly files (default is "*.dll").</param>
|
||||
/// <param name="searchOption">The search option for traversing subdirectories.</param>
|
||||
/// <returns>An enumerable of <see cref="ExampleInfo"/> objects for each discovered example.</returns>
|
||||
[RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
|
||||
[RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
|
||||
public static IEnumerable<ExampleInfo> 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<DemoKeyStrokeSequence> ParseDemoKeyStrokes (Assembly assembly)
|
||||
{
|
||||
List<DemoKeyStrokeSequence> sequences = new ();
|
||||
|
||||
foreach (ExampleDemoKeyStrokesAttribute attr in assembly.GetCustomAttributes<ExampleDemoKeyStrokesAttribute> ())
|
||||
{
|
||||
List<string> 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 ();
|
||||
}
|
||||
}
|
||||
41
Terminal.Gui/Examples/ExampleInfo.cs
Normal file
41
Terminal.Gui/Examples/ExampleInfo.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about a discovered example application.
|
||||
/// </summary>
|
||||
public class ExampleInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the example.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a description of what the example demonstrates.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full path to the example's assembly file.
|
||||
/// </summary>
|
||||
public string AssemblyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of categories this example belongs to.
|
||||
/// </summary>
|
||||
public List<string> Categories { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the demo keystroke sequences defined for this example.
|
||||
/// </summary>
|
||||
public List<DemoKeyStrokeSequence> DemoKeyStrokes { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this example info.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the name and description.</returns>
|
||||
public override string ToString ()
|
||||
{
|
||||
return $"{Name}: {Description}";
|
||||
}
|
||||
}
|
||||
41
Terminal.Gui/Examples/ExampleMetadataAttribute.cs
Normal file
41
Terminal.Gui/Examples/ExampleMetadataAttribute.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This attribute is used by the example discovery system to identify and describe standalone example programs.
|
||||
/// Each example should have exactly one <see cref="ExampleMetadataAttribute"/> applied to its assembly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// [assembly: ExampleMetadata("Character Map", "Unicode character viewer and selector")]
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage (AttributeTargets.Assembly)]
|
||||
public class ExampleMetadataAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExampleMetadataAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The display name of the example.</param>
|
||||
/// <param name="description">A brief description of what the example demonstrates.</param>
|
||||
public ExampleMetadataAttribute (string name, string description)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the example.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brief description of what the example demonstrates.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
52
Terminal.Gui/Examples/ExampleMetrics.cs
Normal file
52
Terminal.Gui/Examples/ExampleMetrics.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Contains performance and execution metrics collected during an example's execution.
|
||||
/// </summary>
|
||||
public class ExampleMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the time when the example started.
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time when initialization completed.
|
||||
/// </summary>
|
||||
public DateTime? InitializedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether initialization completed successfully.
|
||||
/// </summary>
|
||||
public bool InitializedSuccessfully { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of iterations executed.
|
||||
/// </summary>
|
||||
public int IterationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time when shutdown began.
|
||||
/// </summary>
|
||||
public DateTime? ShutdownAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether shutdown completed gracefully.
|
||||
/// </summary>
|
||||
public bool ShutdownGracefully { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times the screen was cleared.
|
||||
/// </summary>
|
||||
public int ClearedContentCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times views were drawn.
|
||||
/// </summary>
|
||||
public int DrawCompleteCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times views were laid out.
|
||||
/// </summary>
|
||||
public int LaidOutCount { get; set; }
|
||||
}
|
||||
42
Terminal.Gui/Examples/ExampleResult.cs
Normal file
42
Terminal.Gui/Examples/ExampleResult.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the result of running an example application.
|
||||
/// </summary>
|
||||
public class ExampleResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the example completed successfully.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exit code of the example process (for out-of-process execution).
|
||||
/// </summary>
|
||||
public int? ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the example timed out.
|
||||
/// </summary>
|
||||
public bool TimedOut { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any error message that occurred during execution.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the performance metrics collected during execution.
|
||||
/// </summary>
|
||||
public ExampleMetrics? Metrics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the standard output captured during execution.
|
||||
/// </summary>
|
||||
public string? StandardOutput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the standard error captured during execution.
|
||||
/// </summary>
|
||||
public string? StandardError { get; set; }
|
||||
}
|
||||
176
Terminal.Gui/Examples/ExampleRunner.cs
Normal file
176
Terminal.Gui/Examples/ExampleRunner.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for running example applications in various execution modes.
|
||||
/// </summary>
|
||||
public static class ExampleRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs an example with the specified context.
|
||||
/// </summary>
|
||||
/// <param name="example">The example information.</param>
|
||||
/// <param name="context">The execution context.</param>
|
||||
/// <returns>The result of running the example.</returns>
|
||||
[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<string> () });
|
||||
}
|
||||
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<ExampleMetrics> (match.Groups [1].Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Terminal.Gui/Examples/ExecutionMode.cs
Normal file
19
Terminal.Gui/Examples/ExecutionMode.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Terminal.Gui.Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the execution mode for running an example application.
|
||||
/// </summary>
|
||||
public enum ExecutionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Run the example in a separate process.
|
||||
/// This provides full isolation but makes debugging more difficult.
|
||||
/// </summary>
|
||||
OutOfProcess,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
InProcess
|
||||
}
|
||||
Reference in New Issue
Block a user