mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* Initial plan * Add IRunnable interface, Runnable base class, and RunnableSessionToken Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add comprehensive parallelizable unit tests for IRunnable Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add 41 more unit tests for comprehensive IRunnable coverage - Added ApplicationRunnableIntegrationTests with 29 integration tests covering Begin/End/Run lifecycle - Added RunnableEdgeCasesTests with 24 edge case and error condition tests - Tests cover event propagation, cancellation scenarios, nested runnables, result handling - Fixed App property not being set in Begin() method - Total test count increased from 23 to 64 tests for IRunnable functionality Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix parallel test failures in CI/CD - Fixed IsModal property to check RunnableSessionStack instead of just TopRunnable - Added "fake" driver parameter to Application.Init() in integration tests - Fixed Begin() to capture old IsModal value before pushing to stack - Moved App property assignment before stack operations to ensure proper state - Skipped 2 tests that use Run() with main loop (not suitable for parallel tests) - All 11,654 parallelizable tests now pass (4 skipped) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor Application with IRunnable and session management Modernized `Application` and `ApplicationImpl` by introducing `IRunnable` and `RunnableSessionToken` for improved session management. Deprecated legacy methods and added `Obsolete` attributes to indicate their removal. Simplified method bodies using expression-bodied members and null-coalescing assignments. Enhanced lifecycle management in `ApplicationImpl` by removing redundant code and improving `SessionStack` iteration. Introduced `IToplevelTransitionManager` to handle top-level state changes. Updated `Runnable<TResult>` to implement `IRunnable<TResult>` with lifecycle event handling for `IsRunning` and `IsModal` states. Improved result management during lifecycle transitions. Removed legacy classes like `SessionToken` and consolidated their functionality into the new constructs. Updated and expanded the test suite to cover `IRunnable` lifecycle events, `RunnableSessionToken` behavior, and integration with `Application`. Performed code cleanup, improved readability, and updated documentation with detailed remarks and examples. Added new unit tests for edge cases and lifecycle behavior. * Implement fluent API for Init/Run/Shutdown with automatic disposal - Changed Init() to return IApplication for fluent chaining - Changed Run<TRunnable>() to return IApplication (breaking change from TRunnable) - Changed Shutdown() to return object? (extracts and returns result from last Run<T>()) - Added FrameworkOwnedRunnable property to track runnable created by Run<T>() - Shutdown() automatically disposes framework-owned runnables - Created FluentExample demonstrating: Application.Create().Init().Run<ColorPickerView>().Shutdown() - Disposal semantics: framework creates → framework disposes; caller creates → caller disposes Co-authored-by: tig <585482+tig@users.noreply.github.com> * New Example: Demonstrates new Fluent API using ColorPicker Conditional compilation (`#if POST_4148`) to support both a new Fluent API and a traditional approach for running `ColorPickerView`. The Fluent API simplifies the application lifecycle with method chaining and automatic disposal, while the traditional approach retains explicit lifecycle management. Refactor `ColorPickerView` to support both approaches: - Add an `instructions` label for user guidance. - Replace `_okButton` and `_cancelButton` with local `Button` instances. - Use a new `ColorPicker` with enhanced styling options. Add a warning log for WIP issue (#4148) in `ApplicationImpl.Run.cs` to highlight limitations with non-`Toplevel` views as runnables. Update `Terminal.sln` to include the new `FluentExample` project with appropriate build configurations. Improve code readability with verbatim string literals and better alignment/indentation. * Introduce `RunnableWrapper` for making any View runnable Added the `RunnableWrapper<TView, TResult>` pattern to enable any `View` to be run as a blocking session with typed results, without requiring inheritance from `Runnable<TResult>` or implementation of `IRunnable<TResult>`. - Added `RunnableWrapperExample` project to demonstrate usage. - Introduced `ApplicationRunnableExtensions` and `ViewRunnableExtensions` for clean, type-safe APIs to run views with or without result extraction. - Updated `CodeSharingStrategy.md` to document reduced duplication using `#if POST_4148` directives. - Added `RunnableWrapper.md` with detailed documentation and examples. - Created runnable examples in `Program.cs` showcasing various use cases. - Improved maintainability by reducing code duplication by 86% and increasing shared code by 264%. - Gated all new functionality behind the `POST_4148` feature flag for backward compatibility. * Simplified `#if POST_4148` usage to reduce duplication and improve clarity. Refactored `RunnableWrapper` to use a parameterless constructor with `required` properties, ensuring type safety and better lifecycle management. Updated `AllViewsView` with new commands, improved generic handling, and enhanced logging. Refactored `ApplicationRunnableExtensions` and `ViewRunnableExtensions` for cleaner initialization and event handling. Enhanced `TestsAllViews` to handle required properties and constraints dynamically. Updated documentation to reflect new designs and provide clearer examples. Improved overall code readability, consistency, and maintainability while leveraging modern C# features. * Update docfx documentation for IRunnable architecture - Updated View.md with comprehensive IRunnable section - Interface-based architecture explanation - Fluent API patterns and examples - Disposal semantics ("whoever creates it, owns it") - Result extraction patterns - Lifecycle properties and CWP-compliant events - Marked legacy Modal Views section for clarity - Updated application.md with IRunnable deep dive - Key features and benefits - Fluent API patterns with method chaining - Disposal semantics table - Creating runnable views with examples - Lifecycle properties and events - RunnableSessionStack management - Updated IApplication interface documentation - Updated runnable-architecture-proposal.md - Marked Phase 1 as COMPLETE ✅ - Updated status to "Phase 1 Complete - Phase 2 In Progress" - Documented all implemented features - Added bonus features (fluent API, automatic disposal) - Included migration examples All documentation is now clear, concise, and complete relative to Phase 1 implementation. Co-authored-by: tig <585482+tig@users.noreply.github.com> --------- Co-authored-by: Tig <tig@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com>
219 lines
6.9 KiB
C#
219 lines
6.9 KiB
C#
#nullable enable
|
|
using System.Reflection;
|
|
|
|
namespace UnitTests;
|
|
|
|
/// <summary>
|
|
/// Base class for tests that need to test all views.
|
|
/// </summary>
|
|
public class TestsAllViews : FakeDriverBase
|
|
{
|
|
/// <summary>
|
|
/// Gets all view types.
|
|
/// </summary>
|
|
public static IEnumerable<object []> AllViewTypes =>
|
|
typeof (View).Assembly
|
|
.GetTypes ()
|
|
.Where (type => type is { IsClass: true, IsAbstract: false, IsPublic: true }
|
|
&& (type.IsSubclassOf (typeof (View)) || type == typeof (View)))
|
|
.Select (type => new object [] { type });
|
|
|
|
/// <summary>
|
|
/// Creates an instance of a view if it is not a generic type.
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static View? CreateInstanceIfNotGeneric (Type type)
|
|
{
|
|
if (type.IsGenericType)
|
|
{
|
|
// Return null for generic types
|
|
return null;
|
|
}
|
|
|
|
return Activator.CreateInstance (type) as View;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of all view classes.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static List<Type> GetAllViewClasses ()
|
|
{
|
|
return typeof (View).Assembly.GetTypes ()
|
|
.Where (myType => myType is { IsClass: true, IsAbstract: false, IsPublic: true }
|
|
&& myType.IsSubclassOf (typeof (View))
|
|
)
|
|
.ToList ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a view from a type.
|
|
/// </summary>
|
|
/// <param name="type">The type.</param>
|
|
/// <param name="ctor">The constructor to call.</param>
|
|
/// <returns></returns>
|
|
public static View? CreateViewFromType (Type type, ConstructorInfo ctor)
|
|
{
|
|
View? viewType = null;
|
|
|
|
if (type is { IsGenericType: true, IsTypeDefinition: true })
|
|
{
|
|
List<Type> typeArguments = new ();
|
|
|
|
// use <object> or the original type if applicable
|
|
foreach (Type arg in type.GetGenericArguments ())
|
|
{
|
|
// Check if this type parameter has constraints that object can't satisfy
|
|
Type [] constraints = arg.GetGenericParameterConstraints ();
|
|
|
|
// If there's a View constraint, use View instead of object
|
|
if (constraints.Any (c => c == typeof (View) || c.IsSubclassOf (typeof (View))))
|
|
{
|
|
typeArguments.Add (typeof (View));
|
|
}
|
|
else if (arg.IsValueType && Nullable.GetUnderlyingType (arg) == null)
|
|
{
|
|
typeArguments.Add (arg);
|
|
}
|
|
else
|
|
{
|
|
typeArguments.Add (typeof (object));
|
|
}
|
|
}
|
|
|
|
type = type.MakeGenericType (typeArguments.ToArray ());
|
|
|
|
// Ensure the type does not contain any generic parameters
|
|
if (type.ContainsGenericParameters)
|
|
{
|
|
Logging.Warning ($"Cannot create an instance of {type} because it contains generic parameters.");
|
|
|
|
//throw new ArgumentException ($"Cannot create an instance of {type} because it contains generic parameters.");
|
|
return null;
|
|
}
|
|
|
|
// Check if the type has required properties that can't be satisfied by Activator.CreateInstance
|
|
// This handles cases like RunnableWrapper which has a required WrappedView property
|
|
if (HasRequiredProperties (type))
|
|
{
|
|
Logging.Warning ($"Cannot create an instance of {type} because it has required properties that must be set.");
|
|
return null;
|
|
}
|
|
|
|
Assert.IsType (type, (View)Activator.CreateInstance (type)!);
|
|
}
|
|
else
|
|
{
|
|
ParameterInfo [] paramsInfo = ctor.GetParameters ();
|
|
Type paramType;
|
|
List<object> pTypes = new ();
|
|
|
|
if (type.IsGenericType)
|
|
{
|
|
foreach (Type args in type.GetGenericArguments ())
|
|
{
|
|
paramType = args.GetType ();
|
|
|
|
if (args.Name == "T")
|
|
{
|
|
pTypes.Add (typeof (object));
|
|
}
|
|
else
|
|
{
|
|
AddArguments (paramType, pTypes);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (ParameterInfo p in paramsInfo)
|
|
{
|
|
paramType = p.ParameterType;
|
|
|
|
if (p.HasDefaultValue)
|
|
{
|
|
pTypes.Add (p.DefaultValue!);
|
|
}
|
|
else
|
|
{
|
|
AddArguments (paramType, pTypes);
|
|
}
|
|
}
|
|
|
|
if (type is { IsGenericType: true, IsTypeDefinition: false })
|
|
{
|
|
viewType = Activator.CreateInstance (type) as View;
|
|
}
|
|
else
|
|
{
|
|
viewType = (View)ctor.Invoke (pTypes.ToArray ());
|
|
}
|
|
|
|
Assert.IsType (type, viewType);
|
|
}
|
|
|
|
return viewType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a type has required properties (C# 11 feature).
|
|
/// </summary>
|
|
private static bool HasRequiredProperties (Type type)
|
|
{
|
|
// Check all public instance properties for the RequiredMemberAttribute
|
|
return type.GetProperties (BindingFlags.Public | BindingFlags.Instance)
|
|
.Any (p => p.GetCustomAttributes (typeof (System.Runtime.CompilerServices.RequiredMemberAttribute), true).Any ());
|
|
}
|
|
|
|
private static void AddArguments (Type paramType, List<object> pTypes)
|
|
{
|
|
if (paramType == typeof (Rectangle))
|
|
{
|
|
pTypes.Add (Rectangle.Empty);
|
|
}
|
|
else if (paramType == typeof (string))
|
|
{
|
|
pTypes.Add (string.Empty);
|
|
}
|
|
else if (paramType == typeof (int))
|
|
{
|
|
pTypes.Add (0);
|
|
}
|
|
else if (paramType == typeof (bool))
|
|
{
|
|
pTypes.Add (true);
|
|
}
|
|
else if (paramType.Name == "IList")
|
|
{
|
|
pTypes.Add (new List<object> ());
|
|
}
|
|
else if (paramType.Name == "View")
|
|
{
|
|
var top = new Toplevel ();
|
|
var view = new View ();
|
|
top.Add (view);
|
|
pTypes.Add (view);
|
|
}
|
|
else if (paramType.Name == "View[]")
|
|
{
|
|
pTypes.Add (new View [] { });
|
|
}
|
|
else if (paramType.Name == "Stream")
|
|
{
|
|
pTypes.Add (new MemoryStream ());
|
|
}
|
|
else if (paramType.Name == "String")
|
|
{
|
|
pTypes.Add (string.Empty);
|
|
}
|
|
else if (paramType.Name == "TreeView`1[T]")
|
|
{
|
|
pTypes.Add (string.Empty);
|
|
}
|
|
else
|
|
{
|
|
pTypes.Add (null!);
|
|
}
|
|
}
|
|
}
|