mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-28 08:47:59 +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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user