-
Notifications
You must be signed in to change notification settings - Fork 735
Open
Milestone
Description
Problems
- Testing locked to legacy static API: Current tests can't determine if examples use
Application(legacy) orIApplication(modern), blocking migration - Not copy/paste ready: Examples wrapped in
Scenario.Main()with artificial inheritance - Class-based architecture unnecessary: All scenarios can be standalone programs with a
Proposal: Restructure Scenarios as Standalone Programs
Summary
Transform Terminal.Gui examples from class-based Scenarios into standalone programs with:
- Zero cruft: No test-specific code in examples
- Copy/paste ready: Complete, runnable programs
- Hybrid execution: In-process (debugging) or out-of-process (isolation)
- Declarative metadata: Assembly attributes for discovery and testing
Solution Architecture
1. Example Metadata Attributes
Location: Terminal.Gui library, Terminal.Gui.Examples namespace
[AttributeUsage(AttributeTargets.Assembly)]
public class ExampleMetadataAttribute : Attribute
{
public ExampleMetadataAttribute(string name, string description);
public string Name { get; }
public string Description { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class ExampleCategoryAttribute : Attribute
{
public ExampleCategoryAttribute(string category);
public string Category { get; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class ExampleDemoKeyStrokesAttribute : Attribute
{
public string[]? KeyStrokes { get; set; }
public string? RepeatKey { get; set; }
public int RepeatCount { get; set; } = 1;
public int DelayMs { get; set; } = 0;
public int Order { get; set; } = 0;
}Usage:
[assembly: ExampleMetadata("Character Map", "Unicode viewer")]
[assembly: ExampleCategory("Text and Formatting")]
[assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 200, Order = 1)]
[assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Shift+Tab", "B", "L" }, Order = 2)]
// Pure example code - no Scenario wrapper
Application.Init();
var top = new Window();
// ... example code ...
Application.Run(top);
Application.Shutdown();2. Test Context Injection (Zero Cruft)
Key Insight: Framework detects environment variable during Init() and auto-wires monitoring.
public class ExampleContext
{
public string? DriverName { get; set; } = null;
public List<string> KeysToInject { get; set; } = new();
public int TimeoutMs { get; set; } = 30000;
public int MaxIterations { get; set; } = -1;
public bool CollectMetrics { get; set; } = false;
public ExecutionMode Mode { get; set; } = ExecutionMode.OutOfProcess;
public const string EnvironmentVariableName = "TERMGUI_TEST_CONTEXT";
}
public enum ExecutionMode { OutOfProcess, InProcess }Implementation in FakeComponentFactory.CreateInput():
public override IInput<ConsoleKeyInfo> CreateInput()
{
var fakeInput = new FakeInput();
string? contextJson = Environment.GetEnvironmentVariable(ExampleContext.EnvironmentVariableName);
if (contextJson != null)
{
var context = JsonSerializer.Deserialize<ExampleContext>(contextJson);
foreach (string keyStr in context?.KeysToInject ?? [])
{
if (Key.TryParse(keyStr, out Key key))
fakeInput.AddInput(ConvertKeyToConsoleKeyInfo(key));
}
}
return fakeInput;
}Implementation in ApplicationImpl.Init():
private void SetupMetricsCollection()
{
var metrics = new ExampleMetrics { StartTime = DateTime.UtcNow };
InitializedChanged += (s, e) => {
if (e.NewState) {
metrics.InitializedAt = DateTime.UtcNow;
metrics.InitializedSuccessfully = true;
}
};
Iteration += (s, e) => metrics.IterationCount++;
Exiting += (s, e) => {
metrics.ShutdownAt = DateTime.UtcNow;
metrics.ShutdownGracefully = true;
Console.WriteLine($"###TERMGUI_METRICS:{JsonSerializer.Serialize(metrics)}###");
};
}3. Example Runner
public static class ExampleRunner
{
public static ExampleResult Run(ExampleInfo example, ExampleContext context)
{
return context.Mode == ExecutionMode.InProcess
? RunInProcess(example, context)
: RunOutOfProcess(example, context);
}
private static ExampleResult RunInProcess(ExampleInfo example, ExampleContext context)
{
Environment.SetEnvironmentVariable(
ExampleContext.EnvironmentVariableName,
JsonSerializer.Serialize(context));
try
{
Assembly asm = Assembly.LoadFrom(example.AssemblyPath);
asm.EntryPoint?.Invoke(null,
asm.EntryPoint.GetParameters().Length == 0 ? null : new[] { Array.Empty<string>() });
return new ExampleResult { Success = true };
}
finally
{
Environment.SetEnvironmentVariable(ExampleContext.EnvironmentVariableName, null);
}
}
private static ExampleResult RunOutOfProcess(ExampleInfo example, ExampleContext context)
{
var psi = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"\"{example.AssemblyPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
Environment = { [ExampleContext.EnvironmentVariableName] = JsonSerializer.Serialize(context) }
};
using var process = Process.Start(psi);
bool exited = process.WaitForExit(context.TimeoutMs);
if (!exited)
{
process.Kill();
return new ExampleResult { Success = false, TimedOut = true };
}
return new ExampleResult
{
Success = process.ExitCode == 0,
ExitCode = process.ExitCode
};
}
}4. Example Discovery
public static class ExampleDiscovery
{
public static IEnumerable<ExampleInfo> DiscoverFromFiles(params string[] assemblyPaths)
{
foreach (string path in assemblyPaths)
{
Assembly asm = Assembly.LoadFrom(path);
var metadata = asm.GetCustomAttribute<ExampleMetadataAttribute>();
if (metadata == null) continue;
yield return new ExampleInfo
{
Name = metadata.Name,
Description = metadata.Description,
AssemblyPath = path,
Categories = asm.GetCustomAttributes<ExampleCategoryAttribute>()
.Select(c => c.Category).ToList(),
DemoKeyStrokes = ParseDemoKeyStrokes(asm)
};
}
}
}5. Updated Test Infrastructure
public class ExampleTests
{
[Theory]
[MemberData(nameof(AllExamples))]
public void All_Examples_Quit_And_Init_Shutdown_Properly(ExampleInfo example)
{
var result = ExampleRunner.Run(example, new ExampleContext
{
DriverName = "FakeDriver",
KeysToInject = new() { "Esc" },
TimeoutMs = 5000,
CollectMetrics = true,
Mode = ExecutionMode.OutOfProcess
});
Assert.True(result.Success);
Assert.True(result.Metrics?.InitializedSuccessfully);
Assert.True(result.Metrics?.ShutdownGracefully);
}
public static IEnumerable<object[]> AllExamples =>
ExampleDiscovery.DiscoverFromFiles(Directory.GetFiles("Examples", "*.dll", SearchOption.AllDirectories))
.Select(e => new object[] { e });
}File Organization
Examples/
TUIExplorer/ # New browser app
FluentExample/ # Existing, add attributes
CharacterMap/ # Converted from Scenario
Program.cs # Standalone program
CharMap.cs # Custom view (if needed)
Migration Path
Phase 1: Infrastructure
- Create
Terminal.Gui.Examplesnamespace - Add attributes, discovery, runner, context classes
- Update
FakeComponentFactory.CreateInput()andApplicationImpl.Init()
Phase 2: Proof of Concept
- Update 3 existing examples (FluentExample, RunnableWrapperExample, Example)
- Convert 5 Scenarios (Buttons, Selectors, CharacterMap, AllViewsTester, Wizards)
- Create test infrastructure
- Validate both execution modes
Phase 3: TUIExplorer
- Create browser app with discovery and launching
Phase 4: Mass Migration
- Convert ~100 remaining Scenarios
- Update all tests
Phase 5: Deprecation
- Remove UICatalog and Scenario
Key Benefits
For Users:
- Run directly:
dotnet run --project Examples/CharacterMap - Copy/paste entire
Program.cs- works immediately
For Testing:
- Works with both legacy and modern APIs
- Zero test code in examples
- In-process (debugging) or out-of-process (isolation)
For Maintenance:
- Examples independently buildable
- No tight coupling to browser app
- Supports simple and complex scenarios
Technical Risks
| Risk | Mitigation |
|---|---|
| Environment variables with async/await | Process-scoped, works fine. In-process tests must cleanup. |
| Assembly.EntryPoint invocation | Well-understood pattern. Handle all Main() signatures. |
| Performance of loading assemblies | Load on-demand. Out-of-process doesn't load in test process. |
| Breaking changes for contributors | Scenario remains during transition. Clear migration guide. |
| Debugging out-of-process | In-process mode available. Debugger can attach to processes. |
Success Criteria
- All examples converted to standalone programs
- TUIExplorer provides equivalent browsing
- All tests pass with new infrastructure
- Both execution modes work
- Zero test-specific code in examples
- Examples are copy/paste ready
- Supports both legacy and modern APIs
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
No status