Pārlūkot izejas kodu

Fixes #3109. AOT support with .Net 8. (#3638)

* Add a native AOT project.

* Fixes Text.Json to work with native AOT.

* Fix silent errors on unit tests when testing the Red color which has a length of 3.

* Allowing test custom configuration without the config.json file match the unit tests configurations.

* Fix unit test if tested alone.

* Add native project into solution.

* Fix merge errors.

* Setting ConfigurationManager.ThrowOnJsonErrors as true to throw any serialization issue when published file runs.

* Remove unnecessary using's.

* Added unit test to ensure all serialization is properly configured.

* Fix warnings.

* Remove ThrowOnJsonErrors.

* Fix warnings.

---------

Co-authored-by: Tig <[email protected]>
BDisp 11 mēneši atpakaļ
vecāks
revīzija
63e75b7413

+ 22 - 0
NativeAot/NativeAot.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <PublishAot>true</PublishAot>
+    <InvariantGlobalization>false</InvariantGlobalization>
+  </PropertyGroup>
+
+  <ItemGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
+    <TrimmerRootAssembly Include="Terminal.Gui" />
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <PackageReference Include="Terminal.Gui" Version="[2.0.0-pre.1788,3)" />
+    <TrimmerRootAssembly Include="Terminal.Gui" />
+  </ItemGroup>
+
+</Project>

+ 113 - 0
NativeAot/Program.cs

@@ -0,0 +1,113 @@
+// This is a test application for a native Aot file.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Terminal.Gui;
+
+namespace NativeAot;
+
+public static class Program
+{
+    [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")]
+    [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")]
+    private static void Main (string [] args)
+    {
+        Application.Init ();
+
+        #region The code in this region is not intended for use in a native Aot self-contained. It's just here to make sure there is no functionality break with localization in Terminal.Gui using self-contained
+
+        if (Equals(Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures.Count == 0)
+        {
+            // Only happens if the project has <InvariantGlobalization>true</InvariantGlobalization>
+            Debug.Assert (Application.SupportedCultures.Count == 0);
+        }
+        else
+        {
+            Debug.Assert (Application.SupportedCultures.Count > 0);
+            Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture));
+        }
+
+        #endregion
+
+        ExampleWindow app = new ();
+        Application.Run (app);
+
+        // Dispose the app object before shutdown
+        app.Dispose ();
+
+        // Before the application exits, reset Terminal.Gui for clean shutdown
+        Application.Shutdown ();
+
+        // To see this output on the screen it must be done after shutdown,
+        // which restores the previous screen.
+        Console.WriteLine ($@"Username: {ExampleWindow.UserName}");
+    }
+}
+
+// Defines a top-level window with border and title
+public class ExampleWindow : Window
+{
+    public static string? UserName;
+
+    public ExampleWindow ()
+    {
+        Title = $"Example App ({Application.QuitKey} to quit)";
+
+        // Create input components and labels
+        var usernameLabel = new Label { Text = "Username:" };
+
+        var userNameText = new TextField
+        {
+            // Position text field adjacent to the label
+            X = Pos.Right (usernameLabel) + 1,
+
+            // Fill remaining horizontal space
+            Width = Dim.Fill ()
+        };
+
+        var passwordLabel = new Label
+        {
+            Text = "Password:", X = Pos.Left (usernameLabel), Y = Pos.Bottom (usernameLabel) + 1
+        };
+
+        var passwordText = new TextField
+        {
+            Secret = true,
+
+            // align with the text box above
+            X = Pos.Left (userNameText),
+            Y = Pos.Top (passwordLabel),
+            Width = Dim.Fill ()
+        };
+
+        // Create login button
+        var btnLogin = new Button
+        {
+            Text = "Login",
+            Y = Pos.Bottom (passwordLabel) + 1,
+
+            // center the login button horizontally
+            X = Pos.Center (),
+            IsDefault = true
+        };
+
+        // When login button is clicked display a message popup
+        btnLogin.Accept += (s, e) =>
+        {
+            if (userNameText.Text == "admin" && passwordText.Text == "password")
+            {
+                MessageBox.Query ("Logging In", "Login Successful", "Ok");
+                UserName = userNameText.Text;
+                Application.RequestStop ();
+            }
+            else
+            {
+                MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
+            }
+        };
+
+        // Add the views to the Window
+        Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin);
+    }
+}

+ 18 - 0
NativeAot/Properties/PublishProfiles/FolderProfile_net8.0_win-x64_Debug.pubxml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+https://go.microsoft.com/fwlink/?LinkID=208121.
+-->
+<Project>
+  <PropertyGroup>
+    <Configuration>Debug</Configuration>
+    <Platform>Any CPU</Platform>
+    <PublishDir>bin\Debug\net8.0\publish\win-x64\</PublishDir>
+    <PublishProtocol>FileSystem</PublishProtocol>
+    <_TargetId>Folder</_TargetId>
+    <TargetFramework>net8.0</TargetFramework>
+    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+    <SelfContained>true</SelfContained>
+    <PublishSingleFile>false</PublishSingleFile>
+    <PublishReadyToRun>false</PublishReadyToRun>
+  </PropertyGroup>
+</Project>

+ 18 - 0
NativeAot/Properties/PublishProfiles/FolderProfile_net8.0_win-x64_Release.pubxml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+https://go.microsoft.com/fwlink/?LinkID=208121.
+-->
+<Project>
+  <PropertyGroup>
+    <Configuration>Release</Configuration>
+    <Platform>Any CPU</Platform>
+    <PublishDir>bin\Release\net8.0\publish\win-x64\</PublishDir>
+    <PublishProtocol>FileSystem</PublishProtocol>
+    <_TargetId>Folder</_TargetId>
+    <TargetFramework>net8.0</TargetFramework>
+    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+    <SelfContained>true</SelfContained>
+    <PublishSingleFile>false</PublishSingleFile>
+    <PublishReadyToRun>false</PublishReadyToRun>
+  </PropertyGroup>
+</Project>

+ 5 - 0
NativeAot/Publish_linux-x64_Debug.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+dotnet clean
+dotnet build
+dotnet publish -c Debug -r linux-x64 --self-contained

+ 5 - 0
NativeAot/Publish_linux-x64_Release.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+dotnet clean
+dotnet build
+dotnet publish -c Release -r linux-x64 --self-contained

+ 5 - 0
NativeAot/Publish_osx-x64_Debug.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+dotnet clean
+dotnet build
+dotnet publish -c Debug -r osx-x64 --self-contained

+ 5 - 0
NativeAot/Publish_osx-x64_Release.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+dotnet clean
+dotnet build
+dotnet publish -c Release -r osx-x64 --self-contained

+ 10 - 0
NativeAot/README.md

@@ -0,0 +1,10 @@
+# Terminal.Gui C# SelfContained
+
+This project aims to test the `Terminal.Gui` library to create a simple `native AOT` `self-container` GUI application in C#, ensuring that all its features are available.
+
+With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish` or in the `Publish_linux-x64` or in the `Publish_osx-x64` files.
+Unlike self-contained single-file publishing, native AOT publishing must be generated on the same platform as the target execution version. Therefore, if the target execution is Linux, then the publishing must be generated on a Linux operating system. Attempting to generate on Windows for the Linux target will throw an exception.
+
+To publish the `native AOT` file in `Debug` or `Release` mode, it is not necessary to select it in the `Solution Configurations`, just choose the `Debug` or `Release` configuration in the `Publish Profile` or the `*.sh` files.
+
+When executing the file directly from the `native AOT` file and needing to debug it, it will be necessary to attach it to the debugger, just like any other standalone application and selecting `Native Code`.

+ 6 - 6
Terminal.Gui/Configuration/SourceGenerationContext.cs

@@ -7,17 +7,17 @@ namespace Terminal.Gui;
 /// </summary>
 [JsonSerializable (typeof (Attribute))]
 [JsonSerializable (typeof (Color))]
-[JsonSerializable (typeof (ThemeScope))]
-[JsonSerializable (typeof (ColorScheme))]
-[JsonSerializable (typeof (SettingsScope))]
 [JsonSerializable (typeof (AppScope))]
+[JsonSerializable (typeof (SettingsScope))]
 [JsonSerializable (typeof (Key))]
 [JsonSerializable (typeof (GlyphDefinitions))]
-[JsonSerializable (typeof (ConfigProperty))]
+[JsonSerializable (typeof (Alignment))]
+[JsonSerializable (typeof (AlignmentModes))]
+[JsonSerializable (typeof (LineStyle))]
 [JsonSerializable (typeof (ShadowStyle))]
-[JsonSerializable (typeof (string))]
-[JsonSerializable (typeof (bool))]
 [JsonSerializable (typeof (bool?))]
 [JsonSerializable (typeof (Dictionary<ColorName, string>))]
+[JsonSerializable (typeof (Dictionary<string, ThemeScope>))]
+[JsonSerializable (typeof (Dictionary<string, ColorScheme>))]
 internal partial class SourceGenerationContext : JsonSerializerContext
 { }

+ 2 - 1
Terminal.Gui/Drawing/Alignment.cs

@@ -1,10 +1,11 @@
-
+using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
 /// <summary>
 ///     Determines the position of items when arranged in a container.
 /// </summary>
+[JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
 public enum Alignment
 {
     /// <summary>

+ 2 - 1
Terminal.Gui/Drawing/AlignmentModes.cs

@@ -1,10 +1,11 @@
-
+using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
 /// <summary>
 ///     Determines alignment modes for <see cref="Alignment"/>.
 /// </summary>
+[JsonConverter (typeof (JsonStringEnumConverter<AlignmentModes>))]
 [Flags]
 public enum AlignmentModes
 {

+ 1 - 1
Terminal.Gui/Drawing/Color.Formatting.cs

@@ -284,7 +284,7 @@ public readonly partial record struct Color
                                                                                               ),
 
                    // Any string too short to possibly be any supported format.
-                   { Length: > 0 and < 4 } => throw new ColorParseException (
+                   { Length: > 0 and < 3 } => throw new ColorParseException (
                                                                              in text,
                                                                              "Text was too short to be any possible supported format.",
                                                                              in text

+ 3 - 0
Terminal.Gui/Drawing/LineStyle.cs

@@ -1,7 +1,10 @@
 #nullable enable
+using System.Text.Json.Serialization;
+
 namespace Terminal.Gui;
 
 /// <summary>Defines the style of lines for a <see cref="LineCanvas"/>.</summary>
+[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
 public enum LineStyle
 {
     /// <summary>No border is drawn.</summary>

+ 4 - 1
Terminal.Gui/View/Adornment/ShadowStyle.cs

@@ -1,8 +1,11 @@
-namespace Terminal.Gui;
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
 
 /// <summary>
 ///     Defines the style of shadow to be drawn on the right and bottom sides of the <see cref="View"/>.
 /// </summary>
+[JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
 public enum ShadowStyle
 {
     /// <summary>

+ 0 - 4
Terminal.Gui/Views/Button.cs

@@ -5,8 +5,6 @@
 //   Miguel de Icaza ([email protected])
 //
 
-using System.Text.Json.Serialization;
-
 namespace Terminal.Gui;
 
 /// <summary>Button is a <see cref="View"/> that provides an item that invokes raises the <see cref="View.Accept"/> event.</summary>
@@ -39,8 +37,6 @@ public class Button : View, IDesignable
     /// Gets or sets whether <see cref="Button"/>s are shown with a shadow effect by default.
     /// </summary>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
-
     public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None;
 
     /// <summary>Initializes a new instance of <see cref="Button"/>.</summary>

+ 2 - 8
Terminal.Gui/Views/Dialog.cs

@@ -1,6 +1,4 @@
-using System.Text.Json.Serialization;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     The <see cref="Dialog"/> <see cref="View"/> is a <see cref="Window"/> that by default is centered and contains
@@ -19,13 +17,11 @@ public class Dialog : Window
     /// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
     /// <remarks>This property can be set in a Theme.</remarks>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
     public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; // Default is set in config.json
 
-    /// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
+    /// <summary>The default <see cref="AlignmentModes"/> for <see cref="Dialog"/>.</summary>
     /// <remarks>This property can be set in a Theme.</remarks>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<AlignmentModes>))]
     public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems;
 
     /// <summary>
@@ -47,7 +43,6 @@ public class Dialog : Window
     /// Gets or sets whether all <see cref="Window"/>s are shown with a shadow effect by default.
     /// </summary>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
     public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; // Default is set in config.json
 
     /// <summary>
@@ -56,7 +51,6 @@ public class Dialog : Window
     /// </summary>
 
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
     public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json
 
     private readonly List<Button> _buttons = new ();

+ 1 - 4
Terminal.Gui/Views/FrameView.cs

@@ -1,6 +1,4 @@
-using System.Text.Json.Serialization;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     The FrameView is a container View with a border around it. 
@@ -38,6 +36,5 @@ public class FrameView : View
     ///     <see cref="FrameView"/>s.
     /// </remarks>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
     public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
 }

+ 1 - 6
Terminal.Gui/Views/MessageBox.cs

@@ -1,7 +1,4 @@
-using System.Diagnostics;
-using System.Text.Json.Serialization;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     MessageBox displays a modal message to the user, with a title, a message and a series of options that the user
@@ -32,13 +29,11 @@ public static class MessageBox
     ///     <see cref="ConfigurationManager"/>.
     /// </summary>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
     public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json
 
     /// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
     /// <remarks>This property can be set in a Theme.</remarks>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
     public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; // Default is set in config.json
 
     /// <summary>

+ 1 - 4
Terminal.Gui/Views/Window.cs

@@ -1,6 +1,4 @@
-using System.Text.Json.Serialization;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
 ///     A <see cref="Toplevel"/> <see cref="View"/> with <see cref="View.BorderStyle"/> set to
@@ -75,6 +73,5 @@ public class Window : Toplevel
     ///     s.
     /// </remarks>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
     public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
 }

+ 6 - 0
Terminal.sln

@@ -46,6 +46,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{C7A51224-5
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "SelfContained\SelfContained.csproj", "{524DEA78-7E7C-474D-B42D-52ED4C04FF14}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "NativeAot\NativeAot.csproj", "{E6D716C6-AC94-4150-B10A-44AE13F79344}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -80,6 +82,10 @@ Global
 		{524DEA78-7E7C-474D-B42D-52ED4C04FF14}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{524DEA78-7E7C-474D-B42D-52ED4C04FF14}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{524DEA78-7E7C-474D-B42D-52ED4C04FF14}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E6D716C6-AC94-4150-B10A-44AE13F79344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E6D716C6-AC94-4150-B10A-44AE13F79344}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 40 - 40
UnitTests/Configuration/ConfigurationMangerTests.cs

@@ -38,7 +38,7 @@ public class ConfigurationManagerTests
         }
 
         // act
-        Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+        Settings! ["Application.QuitKey"].PropertyValue = Key.Q;
         Settings ["Application.NextTabGroupKey"].PropertyValue = Key.F;
         Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B;
 
@@ -130,7 +130,7 @@ public class ConfigurationManagerTests
             { "Disabled", new Attribute (Color.White) }, { "Normal", new Attribute (Color.Blue) }
         };
         dictCopy = (Dictionary<string, Attribute>)DeepMemberWiseCopy (dictSrc, dictDest);
-        Assert.Equal (2, dictCopy.Count);
+        Assert.Equal (2, dictCopy!.Count);
         Assert.Equal (dictSrc ["Disabled"], dictCopy ["Disabled"]);
         Assert.Equal (dictSrc ["Normal"], dictCopy ["Normal"]);
 
@@ -141,7 +141,7 @@ public class ConfigurationManagerTests
         };
         dictSrc = new Dictionary<string, Attribute> { { "Disabled", new Attribute (Color.White) } };
         dictCopy = (Dictionary<string, Attribute>)DeepMemberWiseCopy (dictSrc, dictDest);
-        Assert.Equal (2, dictCopy.Count);
+        Assert.Equal (2, dictCopy!.Count);
         Assert.Equal (dictSrc ["Disabled"], dictCopy ["Disabled"]);
         Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]);
     }
@@ -151,7 +151,7 @@ public class ConfigurationManagerTests
     {
         Reset ();
 
-        Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+        Settings! ["Application.QuitKey"].PropertyValue = Key.Q;
         Settings ["Application.NextTabGroupKey"].PropertyValue = Key.F;
         Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B;
 
@@ -163,16 +163,16 @@ public class ConfigurationManagerTests
             fired = true;
 
             // assert
-            Assert.Equal (Key.Esc, ((Key)Settings ["Application.QuitKey"].PropertyValue).KeyCode);
+            Assert.Equal (Key.Esc, (((Key)Settings! ["Application.QuitKey"].PropertyValue)!).KeyCode);
 
             Assert.Equal (
                           KeyCode.F6,
-                          ((Key)Settings ["Application.NextTabGroupKey"].PropertyValue).KeyCode
+                          (((Key)Settings ["Application.NextTabGroupKey"].PropertyValue)!).KeyCode
                          );
 
             Assert.Equal (
                           KeyCode.F6 | KeyCode.ShiftMask,
-                          ((Key)Settings ["Application.PrevTabGroupKey"].PropertyValue).KeyCode
+                          (((Key)Settings ["Application.PrevTabGroupKey"].PropertyValue)!).KeyCode
                          );
         }
 
@@ -228,7 +228,7 @@ public class ConfigurationManagerTests
 
         // arrange
         Reset ();
-        Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+        Settings! ["Application.QuitKey"].PropertyValue = Key.Q;
         Settings ["Application.NextTabGroupKey"].PropertyValue = Key.F;
         Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B;
         Settings.Apply ();
@@ -242,7 +242,7 @@ public class ConfigurationManagerTests
         Reset ();
 
         // assert
-        Assert.NotEmpty (Themes);
+        Assert.NotEmpty (Themes!);
         Assert.Equal ("Default", Themes.Theme);
         Assert.Equal (Key.Esc, Application.QuitKey);
         Assert.Equal (Key.F6, Application.NextTabGroupKey);
@@ -274,7 +274,7 @@ public class ConfigurationManagerTests
     {
         Locations = ConfigLocations.DefaultOnly;
         Reset ();
-        Assert.NotEmpty (Themes);
+        Assert.NotEmpty (Themes!);
         Assert.Equal ("Default", Themes.Theme);
     }
 
@@ -367,7 +367,7 @@ public class ConfigurationManagerTests
         // Serialize to a JSON string
         string json = ToJson ();
 
-        // Write the JSON string to the file 
+        // Write the JSON string to the file
         File.WriteAllText ("config.json", json);
     }
 
@@ -377,23 +377,23 @@ public class ConfigurationManagerTests
         Locations = ConfigLocations.All;
         Reset ();
 
-        Assert.NotEmpty (Settings);
+        Assert.NotEmpty (Settings!);
 
         // test that all ConfigProperties have our attribute
         Assert.All (
                     Settings,
                     item => Assert.NotEmpty (
-                                             item.Value.PropertyInfo.CustomAttributes.Where (
-                                                                                             a => a.AttributeType == typeof (SerializableConfigurationProperty)
-                                                                                            )
+                                             item.Value.PropertyInfo!.CustomAttributes.Where (
+                                                                                              a => a.AttributeType == typeof (SerializableConfigurationProperty)
+                                                                                             )
                                             )
                    );
 
         Assert.Empty (
                       Settings.Where (
-                                      cp => cp.Value.PropertyInfo.GetCustomAttribute (
-                                                                                      typeof (SerializableConfigurationProperty)
-                                                                                     )
+                                      cp => cp.Value.PropertyInfo!.GetCustomAttribute (
+                                                                                       typeof (SerializableConfigurationProperty)
+                                                                                      )
                                             == null
                                      )
                      );
@@ -401,12 +401,12 @@ public class ConfigurationManagerTests
         // Application is a static class
         PropertyInfo pi = typeof (Application).GetProperty ("QuitKey");
         Assert.Equal (pi, Settings ["Application.QuitKey"].PropertyInfo);
-        
 
         // FrameView is not a static class and DefaultBorderStyle is Scope.Scheme
         pi = typeof (FrameView).GetProperty ("DefaultBorderStyle");
         Assert.False (Settings.ContainsKey ("FrameView.DefaultBorderStyle"));
-        Assert.True (Themes ["Default"].ContainsKey ("FrameView.DefaultBorderStyle"));
+        Assert.True (Themes! ["Default"].ContainsKey ("FrameView.DefaultBorderStyle"));
+        Assert.Equal (pi, Themes! ["Default"] ["FrameView.DefaultBorderStyle"].PropertyInfo);
     }
 
     [Fact]
@@ -414,31 +414,31 @@ public class ConfigurationManagerTests
     {
         // Color.ColorSchemes is serialized as "ColorSchemes", not "Colors.ColorSchemes"
         PropertyInfo pi = typeof (Colors).GetProperty ("ColorSchemes");
-        var scp = (SerializableConfigurationProperty)pi.GetCustomAttribute (typeof (SerializableConfigurationProperty));
-        Assert.True (scp.Scope == typeof (ThemeScope));
+        var scp = (SerializableConfigurationProperty)pi!.GetCustomAttribute (typeof (SerializableConfigurationProperty));
+        Assert.True (scp!.Scope == typeof (ThemeScope));
         Assert.True (scp.OmitClassName);
 
         Reset ();
-        Assert.Equal (pi, Themes ["Default"] ["ColorSchemes"].PropertyInfo);
+        Assert.Equal (pi, Themes! ["Default"] ["ColorSchemes"].PropertyInfo);
     }
 
     [Fact]
     [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
     public void TestConfigurationManagerInitDriver ()
     {
-        Assert.Equal ("Default", Themes.Theme);
+        Assert.Equal ("Default", Themes!.Theme);
 
-        Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"].Normal.Foreground);
+        Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"]!.Normal.Foreground);
         Assert.Equal (new Color (Color.Blue), Colors.ColorSchemes ["Base"].Normal.Background);
 
         // Change Base
         Stream json = ToStream ();
 
-        Settings.Update (json, "TestConfigurationManagerInitDriver");
+        Settings!.Update (json, "TestConfigurationManagerInitDriver");
 
         Dictionary<string, ColorScheme> colorSchemes =
             (Dictionary<string, ColorScheme>)Themes [Themes.Theme] ["ColorSchemes"].PropertyValue;
-        Assert.Equal (Colors.ColorSchemes ["Base"], colorSchemes ["Base"]);
+        Assert.Equal (Colors.ColorSchemes ["Base"], colorSchemes! ["Base"]);
         Assert.Equal (Colors.ColorSchemes ["TopLevel"], colorSchemes ["TopLevel"]);
         Assert.Equal (Colors.ColorSchemes ["Error"], colorSchemes ["Error"]);
         Assert.Equal (Colors.ColorSchemes ["Dialog"], colorSchemes ["Dialog"]);
@@ -489,7 +489,7 @@ public class ConfigurationManagerTests
 				}
 			}";
 
-        Settings.Update (json, "test");
+        Settings!.Update (json, "test");
 
         // AbNormal is not a ColorScheme attribute
         json = @"
@@ -514,7 +514,7 @@ public class ConfigurationManagerTests
 
         Settings.Update (json, "test");
 
-        // Modify hotNormal background only 
+        // Modify hotNormal background only
         json = @"
 			{
 				""Themes"" :  [ 
@@ -572,7 +572,7 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        var jsonException = Assert.Throws<JsonException> (() => Settings.Update (json, "test"));
+        var jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
         Assert.Equal ("Unexpected color name: brown.", jsonException.Message);
 
         // AbNormal is not a ColorScheme attribute
@@ -596,10 +596,10 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
         Assert.Equal ("Unrecognized ColorScheme Attribute name: AbNormal.", jsonException.Message);
 
-        // Modify hotNormal background only 
+        // Modify hotNormal background only
         json = @"
 			{
 				""Themes"" : [ 
@@ -619,7 +619,7 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
         Assert.Equal ("Both Foreground and Background colors must be provided.", jsonException.Message);
 
         // Unknown property
@@ -628,7 +628,7 @@ public class ConfigurationManagerTests
 				""Unknown"" : ""Not known""
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
         Assert.StartsWith ("Unknown property", jsonException.Message);
 
         Assert.Equal (0, _jsonErrors.Length);
@@ -644,7 +644,7 @@ public class ConfigurationManagerTests
         GetHardCodedDefaults ();
         Stream stream = ToStream ();
 
-        Settings.Update (stream, "TestConfigurationManagerToJson");
+        Settings!.Update (stream, "TestConfigurationManagerToJson");
     }
 
     [Fact]
@@ -790,19 +790,19 @@ public class ConfigurationManagerTests
         Reset ();
         ThrowOnJsonErrors = true;
 
-        Settings.Update (json, "TestConfigurationManagerUpdateFromJson");
+        Settings!.Update (json, "TestConfigurationManagerUpdateFromJson");
 
         Assert.Equal (KeyCode.Esc, Application.QuitKey.KeyCode);
-        Assert.Equal (KeyCode.Z | KeyCode.AltMask, ((Key)Settings ["Application.QuitKey"].PropertyValue).KeyCode);
+        Assert.Equal (KeyCode.Z | KeyCode.AltMask, ((Key)Settings ["Application.QuitKey"].PropertyValue)!.KeyCode);
 
-        Assert.Equal ("Default", Themes.Theme);
+        Assert.Equal ("Default", Themes!.Theme);
 
-        Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"].Normal.Foreground);
+        Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"]!.Normal.Foreground);
         Assert.Equal (new Color (Color.Blue), Colors.ColorSchemes ["Base"].Normal.Background);
 
         Dictionary<string, ColorScheme> colorSchemes =
             (Dictionary<string, ColorScheme>)Themes.First ().Value ["ColorSchemes"].PropertyValue;
-        Assert.Equal (new Color (Color.White), colorSchemes ["Base"].Normal.Foreground);
+        Assert.Equal (new Color (Color.White), colorSchemes! ["Base"].Normal.Foreground);
         Assert.Equal (new Color (Color.Blue), colorSchemes ["Base"].Normal.Background);
 
         // Now re-apply

+ 86 - 0
UnitTests/Configuration/SerializableConfigurationPropertyTests.cs

@@ -0,0 +1,86 @@
+#nullable enable
+
+using System.Reflection;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Terminal.Gui.ConfigurationTests;
+
+public class SerializableConfigurationPropertyTests
+{
+    [Fact]
+    public void Test_SerializableConfigurationProperty_Types_Added_To_JsonSerializerContext ()
+    {
+        // The assembly containing the types to inspect
+        var assembly = Assembly.GetAssembly (typeof (SourceGenerationContext));
+
+        // Get all types from the assembly
+        var types = assembly!.GetTypes ();
+
+        // Find all properties with the SerializableConfigurationProperty attribute
+        var properties = new List<PropertyInfo> ();
+        foreach (var type in types)
+        {
+            properties.AddRange (type.GetProperties ().Where (p =>
+                p.GetCustomAttributes (typeof (SerializableConfigurationProperty), false).Any ()));
+        }
+
+        // Get the types of the properties
+        var propertyTypes = properties.Select (p => p.PropertyType).Distinct ();
+
+        // Get the types registered in the JsonSerializerContext derived class
+        var contextType = typeof (SourceGenerationContext);
+        var contextTypes = GetRegisteredTypes (contextType);
+
+        // Ensure all property types are included in the JsonSerializerContext derived class
+        IEnumerable<Type> collection = contextTypes as Type [] ?? contextTypes.ToArray ();
+
+        foreach (var type in propertyTypes)
+        {
+            Assert.Contains (type, collection);
+        }
+
+        // Ensure no property has the generic JsonStringEnumConverter<>
+        foreach (var property in properties)
+        {
+            var jsonConverterAttributes = property.GetCustomAttributes (typeof (JsonConverterAttribute), false)
+                .Cast<JsonConverterAttribute> ();
+
+            foreach (var attribute in jsonConverterAttributes)
+            {
+                Assert.False (attribute.ConverterType!.IsGenericType &&
+                             attribute.ConverterType.GetGenericTypeDefinition () == typeof (JsonStringEnumConverter<>));
+            }
+        }
+
+        // Find all classes with the JsonConverter attribute of type ScopeJsonConverter<>
+        var classesWithScopeJsonConverter = types.Where (t =>
+            t.GetCustomAttributes (typeof (JsonConverterAttribute), false)
+            .Any (attr => ((JsonConverterAttribute)attr).ConverterType!.IsGenericType &&
+                         ((JsonConverterAttribute)attr).ConverterType!.GetGenericTypeDefinition () == typeof (ScopeJsonConverter<>)));
+
+        // Ensure all these classes are included in the JsonSerializerContext derived class
+        foreach (var type in classesWithScopeJsonConverter)
+        {
+            Assert.Contains (type, collection);
+        }
+    }
+
+    private IEnumerable<Type> GetRegisteredTypes (Type contextType)
+    {
+        // Use reflection to find which types are registered in the JsonSerializerContext
+        var registeredTypes = new List<Type> ();
+
+        var properties = contextType.GetProperties (BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
+        foreach (var property in properties)
+        {
+            if (property.PropertyType.IsGenericType &&
+                property.PropertyType.GetGenericTypeDefinition () == typeof (JsonTypeInfo<>))
+            {
+                registeredTypes.Add (property.PropertyType.GetGenericArguments () [0]);
+            }
+        }
+
+        return registeredTypes.Distinct ();
+    }
+}

+ 9 - 4
UnitTests/Configuration/ThemeScopeTests.cs

@@ -29,13 +29,18 @@ public class ThemeScopeTests
     {
         Reset ();
         Assert.NotEmpty (Themes);
-        Assert.Equal (Alignment.End, Dialog.DefaultButtonAlignment);
+        Alignment savedValue = Dialog.DefaultButtonAlignment;
+        Alignment newValue = Alignment.Center != savedValue ? Alignment.Center : Alignment.Start;
 
-        Themes ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = Alignment.Center;
+        Themes ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = newValue;
 
         ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
-        Assert.Equal (Alignment.Center, Dialog.DefaultButtonAlignment);
-        Reset ();
+        Assert.Equal (newValue, Dialog.DefaultButtonAlignment);
+
+        // Replace with the savedValue to avoid failures on other unit tests that rely on the default value
+        Themes ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = savedValue;
+        ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
+        Assert.Equal (savedValue, Dialog.DefaultButtonAlignment);
     }
 
     [Fact]

+ 3 - 0
UnitTests/Configuration/ThemeTests.cs

@@ -76,6 +76,9 @@ public class ThemeTests
     [Fact]
     public void TestSerialize_RoundTrip ()
     {
+        // This is needed to test only this alone
+        Reset ();
+
         var theme = new ThemeScope ();
         theme ["Dialog.DefaultButtonAlignment"].PropertyValue = Alignment.End;
 

+ 72 - 0
UnitTests/TestHelpers.cs

@@ -86,6 +86,11 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
             }
 #endif
             ConfigurationManager.Reset ();
+
+            if (CM.Locations != CM.ConfigLocations.None)
+            {
+                SetCurrentConfig (_savedValues);
+            }
         }
     }
 
@@ -110,10 +115,77 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
             }
 #endif
             Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType));
+
+            if (CM.Locations != CM.ConfigLocations.None)
+            {
+                _savedValues = GetCurrentConfig ();
+            }
         }
     }
 
     private bool AutoInit { get; }
+
+    private List<object> _savedValues;
+
+    private List<object> GetCurrentConfig ()
+    {
+        CM.Reset ();
+
+        List<object> savedValues =
+        [
+            Dialog.DefaultButtonAlignment,
+            Dialog.DefaultButtonAlignmentModes,
+            MessageBox.DefaultBorderStyle
+        ];
+        CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = Alignment.End;
+        CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignmentModes"].PropertyValue = AlignmentModes.AddSpaceBetweenItems;
+        CM.Themes! ["Default"] ["MessageBox.DefaultBorderStyle"].PropertyValue = LineStyle.Double;
+        ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
+
+        return savedValues;
+    }
+
+    private void SetCurrentConfig (List<object> values)
+    {
+        CM.Reset ();
+        bool needApply = false;
+
+        foreach (object value in values)
+        {
+            switch (value)
+            {
+                case Alignment alignment:
+                    if ((Alignment)CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue! != alignment)
+                    {
+                        needApply = true;
+                        CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = alignment;
+                    }
+
+                    break;
+                case AlignmentModes alignmentModes:
+                    if ((AlignmentModes)CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignmentModes"].PropertyValue! != alignmentModes)
+                    {
+                        needApply = true;
+                        CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignmentModes"].PropertyValue = alignmentModes;
+                    }
+
+                    break;
+                case LineStyle lineStyle:
+                    if ((LineStyle)CM.Themes! ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue! != lineStyle)
+                    {
+                        needApply = true;
+                        CM.Themes! ["Default"] ["MessageBox.DefaultBorderStyle"].PropertyValue = lineStyle;
+                    }
+
+                    break;
+            }
+        }
+
+        if (needApply)
+        {
+            ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
+        }
+    }
 }
 
 [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]

+ 3 - 2
UnitTests/Views/NumericUpDownTests.cs

@@ -98,7 +98,8 @@ public class NumericUpDownTests (ITestOutputHelper _output)
     [Fact]
     public void WhenCreatedWithInvalidTypeObject_ShouldNotThrowInvalidOperationException ()
     {
-        NumericUpDown<object> numericUpDown = new ();
+        Exception exception = Record.Exception (() => new NumericUpDown<object> ());
+        Assert.Null (exception);
     }
 
     [Fact]
@@ -217,7 +218,7 @@ public class NumericUpDownTests (ITestOutputHelper _output)
     }
 
     [Fact]
-    public void KeDown_CursorUp_Increments ()
+    public void KeyDown_CursorUp_Increments ()
     {
         NumericUpDown<int> numericUpDown = new ();