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 <tig@users.noreply.github.com>
This commit is contained in:
BDisp
2024-08-06 19:05:36 +01:00
committed by GitHub
parent 1b973eed5a
commit 63e75b7413
27 changed files with 443 additions and 82 deletions

View File

@@ -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
NativeAot/Program.cs Normal file
View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

10
NativeAot/README.md Normal file
View File

@@ -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`.

View File

@@ -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
{ }

View File

@@ -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>

View File

@@ -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
{

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -5,8 +5,6 @@
// Miguel de Icaza (miguel@gnome.org)
//
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>

View File

@@ -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 ();

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 ();
}
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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 ();