mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-28 16:58:01 +01:00
Fixes #4057 - MASSIVE! Fully implements ColorScheme->Scheme + VisualRole + Colors.->SchemeManager. (#4062)
* touching publish.yml * ColorScheme->Scheme * ColorScheme->Scheme 2 * Prototype of GetAttributeForRole * Badly broke CM * Further Badly broke CM * Refactored CM big-time. View still broken * All unit test pass again. Tons added. CM is still WIP, but Schemes is not mostly refactored and working. * Actually: All unit test pass again. Tons added. CM is still WIP, but Schemes is not mostly refactored and working. * Bug fixes. DeepMemberWiseClone cleanup * Further cleanup of Scope<T>, ConfigProperty, etc. * Made ConfigManager thread safe. * WIP: Broken * WIP: new deep clone impl * WIP: new deep clone impl is done. Now fixing CM * WIP: - config.md - Working on AOT clean up - Core CM is broken; but known. * WIP * Merged. Removed CM from Application.Init * WIP * More WIP; Less broke * All CM unit tests pass... Not sure if it actually works though * All unit tests pass... Themes are broken though in UI Cat * CM Ready for review? * Fixed failures due to TextStyles PR * Working on Scheme/Attribute * Working on Scheme/Attribute 2 * Working on Scheme/Attribute 3 * Working on Scheme/Attribute 4 * Working on Scheme/Attribute 5 * Working on Scheme/Attribute 6 * Added test to show how awful memory usage is * Improved schema. Updated config.json * Nade Scope<T> concurrentdictionary and added test to prove * Made Themes ConcrurrentDictionary. Added bunches of tests * Code cleanup * Code cleanup 2 * Code cleanup 3 * Tweaking Scheme * ClearJsonErrors * ClearJsonErrors2 * Updated Attribute API * It all (mostly) works! * Skip odd unit test * Messed with Themes * Theme tweaks * Code reorg. New .md stuff * Fixed Enabled. Added mock driver * Fixed a bunch of View.Enabled related issues * Scheme -> Get/SetScheme() * Cleanup * Cleanup2 * Broke something * Fixed everything * Made CM.Enable better * Text Style Scenario * Added comments * Fixed UI Catalog Theme Changing * Fixed more dynamic CM update stuff * Warning cleanup * New Default Theme * fixed unit test * Refactoring Scheme and Attribute to fix inheritance * more unit tests * ConfigProperty is not updating schemes correctly * All unit tests pass. Code cleanup * All unit tests pass. Code cleanup2 * Fixed unit tests * Upgraded TextField and TextView * Fixed TextView !Enabled bug * More updates to TextView. More unit tests for SchemeManager * Upgraded CharMap * API docs * Fixe HexView API * upgrade HexView * Fixed shortcut KeyView * Fixed more bugs. Added new themes * updated themes * upgraded Border * Fixed themes memory usage...mostly * Fixed themes memory usage...mostly2 * Fixed themes memory usage...2 * Fixed themes memory usage...3 * Added new colors * Fixed GetHardCodedConfig bug * Added Themes Scenario - WIP * Added Themes Scenario * Tweaked Themes Scenario * Code cleanup * Fixed json schmea * updated deepdives * updated deepdives * Tweaked Themes Scenario * Made Schemes a concurrent dict * Test cleanup * Thread safe ConfigProperty tests * trying to make things more thread safe * more trying to make things more thread safe * Fixing bugs in shadowview * Fixing bugs in shadowview 2 * Refactored GetViewsUnderMouse to GetViewsUnderLocation etc... * Fixed dupe unit tests? * Added better description of layout and coordiantes to deep dive * Added better description of layout and coordiantes to deep dive * Modified tests that call v2.AddTimeout; they were returning true which means restart the timer! This was causing mac/linux unit test failures. I think * Fixed auto scheme. Broke TextView/TextField selection * Realized Attribute.IsExplicitlySet is stupid; just use nullable * Fixed Attribute. Simplified. MOre theme testing * Updated themes again * GetViewsUnderMouse to GetViewsUnderLocation broke TransparentMouse. * Fixing mouseunder bugs * rewriting... * All working again. Shadows are now slick as snot. GetViewsUnderLocation is rewritten to actually work and be readable. Tons more low-level unit tests. Margin is now actually ViewportSettings.Transparent. * Code cleanup * Code cleanup * Code cleanup of color apis * Fixed Hover/Highlight * Update Examples/UICatalog/Scenarios/AllViewsTester.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/Clipping.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed race condition? * reverted * Simplified Attribute API by removing events from SetAttributeForRole * Removed recursion from GetViewsAtLocation * Removed unneeded code * Code clean up. Fixed Scheme bug. * reverted temporary disable * Adjusted scheme algo * Upgraded TextValidateField * Fixed TextValidate bugs * Tweaks * Frameview rounded border by default * API doc cleanup * Readme fix * Addressed tznind feeback * Fixed more unit test issues by protecting Application statics from being set if Application.Initialized is not true * Fixed more unit test issues by protecting Application statics from being set if Application.Initialized is not true 2 * cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using System.Text.Json;
|
||||
using Moq;
|
||||
using UnitTests;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class AttributeJsonConverterTests
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions = new ()
|
||||
{
|
||||
Converters = { new AttributeJsonConverter (), new ColorJsonConverter () }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void TestDeserialize ()
|
||||
{
|
||||
// Test deserializing from human-readable color names
|
||||
var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}";
|
||||
var attribute = JsonSerializer.Deserialize<Attribute> (json, JsonOptions);
|
||||
Assert.Equal (Color.Blue, attribute.Foreground.GetClosestNamedColor16 ());
|
||||
Assert.Equal (Color.Green, attribute.Background.GetClosestNamedColor16 ());
|
||||
|
||||
// Test deserializing from RGB values
|
||||
json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}";
|
||||
attribute = JsonSerializer.Deserialize<Attribute> (json, JsonOptions);
|
||||
Assert.Equal (Color.Red, attribute.Foreground.GetClosestNamedColor16 ());
|
||||
Assert.Equal (Color.BrightGreen, attribute.Background.GetClosestNamedColor16 ());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_TextStyle ()
|
||||
{
|
||||
var justStyleJson = "\"Bold\"";
|
||||
TextStyle textStyle = JsonSerializer.Deserialize<TextStyle> (justStyleJson, JsonOptions);
|
||||
Assert.Equal (TextStyle.Bold, textStyle);
|
||||
|
||||
justStyleJson = "\"Bold,Underline\"";
|
||||
textStyle = JsonSerializer.Deserialize<TextStyle> (justStyleJson, JsonOptions);
|
||||
Assert.Equal (TextStyle.Bold | TextStyle.Underline, textStyle);
|
||||
|
||||
var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\",\"Style\":\"Bold\"}";
|
||||
Attribute attribute = JsonSerializer.Deserialize<Attribute> (json, JsonOptions);
|
||||
Assert.Equal (TextStyle.Bold, attribute.Style);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void TestSerialize ()
|
||||
{
|
||||
// Test serializing to human-readable color names
|
||||
var attribute = new Attribute (Color.Blue, Color.Green);
|
||||
string json = JsonSerializer.Serialize (attribute, JsonOptions);
|
||||
Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_TextStyle ()
|
||||
{
|
||||
// Test serializing to human-readable color names
|
||||
var attribute = new Attribute (Color.Blue, Color.Green, TextStyle.Bold);
|
||||
string json = JsonSerializer.Serialize (attribute, JsonOptions);
|
||||
Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\",\"Style\":\"Bold\"}", json);
|
||||
|
||||
attribute = new Attribute (Color.Blue, Color.Green, TextStyle.Bold | TextStyle.Italic);
|
||||
json = JsonSerializer.Serialize (attribute, JsonOptions);
|
||||
Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\",\"Style\":\"Bold, Italic\"}", json);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonRoundTrip_PreservesEquality ()
|
||||
{
|
||||
Attribute original = new (Color.Red, Color.Green, TextStyle.None);
|
||||
|
||||
string json = JsonSerializer.Serialize (original, new JsonSerializerOptions
|
||||
{
|
||||
Converters = { new AttributeJsonConverter (), new ColorJsonConverter () }
|
||||
});
|
||||
|
||||
Attribute roundTripped = JsonSerializer.Deserialize<Attribute> (json, new JsonSerializerOptions
|
||||
{
|
||||
Converters = { new AttributeJsonConverter (), new ColorJsonConverter () }
|
||||
})!;
|
||||
|
||||
Assert.Equal (original, roundTripped); // ✅ This should pass if all fields are faithfully round-tripped
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ColorJsonConverterTests
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions = new ()
|
||||
{
|
||||
Converters = { new AttributeJsonConverter (), new ColorJsonConverter () }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData ("\"#000000\"", 0, 0, 0)]
|
||||
public void DeserializesFromHexCode (string hexCode, int r, int g, int b)
|
||||
{
|
||||
// Arrange
|
||||
var expected = new Color (r, g, b);
|
||||
|
||||
// Act
|
||||
var actual = JsonSerializer.Deserialize<Color> (
|
||||
hexCode,
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
//Assert
|
||||
Assert.Equal (expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData ("\"rgb(0,0,0)\"", 0, 0, 0)]
|
||||
public void DeserializesFromRgb (string rgb, int r, int g, int b)
|
||||
{
|
||||
// Arrange
|
||||
var expected = new Color (r, g, b);
|
||||
|
||||
// Act
|
||||
var actual = JsonSerializer.Deserialize<Color> (
|
||||
rgb,
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
//Assert
|
||||
Assert.Equal (expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ColorName16.Black, "Black")]
|
||||
[InlineData (ColorName16.Blue, "Blue")]
|
||||
[InlineData (ColorName16.Green, "Green")]
|
||||
[InlineData (ColorName16.Cyan, "Aqua")] // W3C+ Standard overrides
|
||||
[InlineData (ColorName16.Gray, "Gray")]
|
||||
[InlineData (ColorName16.Red, "Red")]
|
||||
[InlineData (ColorName16.Magenta, "Fuchsia")] // W3C+ Standard overrides
|
||||
[InlineData (ColorName16.Yellow, "Yellow")]
|
||||
[InlineData (ColorName16.DarkGray, "DarkGray")]
|
||||
[InlineData (ColorName16.BrightBlue, "BrightBlue")]
|
||||
[InlineData (ColorName16.BrightGreen, "BrightGreen")]
|
||||
[InlineData (ColorName16.BrightCyan, "BrightCyan")]
|
||||
[InlineData (ColorName16.BrightRed, "BrightRed")]
|
||||
[InlineData (ColorName16.BrightMagenta, "BrightMagenta")]
|
||||
[InlineData (ColorName16.BrightYellow, "BrightYellow")]
|
||||
[InlineData (ColorName16.White, "White")]
|
||||
public void SerializesColorName16ValuesAsStrings (ColorName16 colorName, string expectedJson)
|
||||
{
|
||||
var converter = new ColorJsonConverter ();
|
||||
var options = new JsonSerializerOptions { Converters = { converter } };
|
||||
|
||||
string serialized = JsonSerializer.Serialize (new Color (colorName), options);
|
||||
|
||||
Assert.Equal ($"\"{expectedJson}\"", serialized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (1, 0, 0, "\"#010000\"")]
|
||||
[InlineData (0, 0, 1, "\"#000001\"")]
|
||||
public void SerializesToHexCode (int r, int g, int b, string expected)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
string actual = JsonSerializer.Serialize (
|
||||
new Color (r, g, b),
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
//Assert
|
||||
Assert.Equal (expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData ("Black", Color.Black)]
|
||||
[InlineData ("Blue", Color.Blue)]
|
||||
[InlineData ("BrightBlue", Color.BrightBlue)]
|
||||
[InlineData ("BrightCyan", Color.BrightCyan)]
|
||||
[InlineData ("BrightGreen", Color.BrightGreen)]
|
||||
[InlineData ("BrightMagenta", Color.BrightMagenta)]
|
||||
[InlineData ("BrightRed", Color.BrightRed)]
|
||||
[InlineData ("BrightYellow", Color.BrightYellow)]
|
||||
[InlineData ("Yellow", Color.Yellow)]
|
||||
[InlineData ("Cyan", Color.Cyan)]
|
||||
[InlineData ("DarkGray", Color.DarkGray)]
|
||||
[InlineData ("Gray", Color.Gray)]
|
||||
[InlineData ("Green", Color.Green)]
|
||||
[InlineData ("Magenta", Color.Magenta)]
|
||||
[InlineData ("Red", Color.Red)]
|
||||
[InlineData ("White", Color.White)]
|
||||
public void TestColorDeserializationFromHumanReadableColorName16 (string colorName, ColorName16 expectedColor)
|
||||
{
|
||||
// Arrange
|
||||
var json = $"\"{colorName}\"";
|
||||
|
||||
// Act
|
||||
var actualColor = JsonSerializer.Deserialize<Color> (json, JsonOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (new Color (expectedColor), actualColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestDeserializeColor_Black ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "\"Black\"";
|
||||
var expectedColor = new Color ("Black");
|
||||
|
||||
// Act
|
||||
var color = JsonSerializer.Deserialize<Color> (
|
||||
json,
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedColor, color);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestDeserializeColor_BrightRed ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "\"BrightRed\"";
|
||||
var expectedColor = Color.BrightRed;
|
||||
|
||||
// Act
|
||||
var color = JsonSerializer.Deserialize<Color> (
|
||||
json,
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedColor, color);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestSerializeColor_Black ()
|
||||
{
|
||||
// Arrange
|
||||
var expectedJson = "\"Black\"";
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (
|
||||
new Color (Color.Black),
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedJson, json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestSerializeColor_BrightRed ()
|
||||
{
|
||||
// Arrange
|
||||
var expectedJson = "\"BrightRed\"";
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (
|
||||
new Color (Color.BrightRed),
|
||||
new JsonSerializerOptions { Converters = { new ColorJsonConverter () } }
|
||||
);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedJson, json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ConfigPropertyTests
|
||||
{
|
||||
private class Dummy
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyValue_CanBeSetAndReadConcurrently ()
|
||||
{
|
||||
var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value));
|
||||
var configProperty = new ConfigProperty { PropertyInfo = propertyInfo };
|
||||
|
||||
int numTasks = 20;
|
||||
var values = new string [numTasks];
|
||||
for (int i = 0; i < numTasks; i++)
|
||||
{
|
||||
values [i] = $"Value_{i}";
|
||||
}
|
||||
|
||||
Parallel.For (0, numTasks, i =>
|
||||
{
|
||||
configProperty.PropertyValue = values [i];
|
||||
// Remove the per-thread assertion, as it is not valid in a concurrent context.
|
||||
// Optionally, you can check that the value is one of the expected values:
|
||||
var currentValue = configProperty.PropertyValue as string;
|
||||
Assert.Contains (currentValue, values);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void UpdateFrom_CanBeCalledConcurrently ()
|
||||
{
|
||||
var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value));
|
||||
var configProperty = new ConfigProperty { PropertyInfo = propertyInfo, PropertyValue = "Initial" };
|
||||
|
||||
int numTasks = 20;
|
||||
Parallel.For (0, numTasks, i =>
|
||||
{
|
||||
var newValue = $"Thread_{i}";
|
||||
configProperty.UpdateFrom (newValue);
|
||||
});
|
||||
|
||||
var finalValue = configProperty.PropertyValue as string;
|
||||
Assert.StartsWith ("Thread_", finalValue);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DeepCloner_DeepClone_IsThreadSafe ()
|
||||
{
|
||||
var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value));
|
||||
var configProperty = new ConfigProperty { PropertyInfo = propertyInfo, PropertyValue = "DeepCloneValue" };
|
||||
|
||||
int numTasks = 20;
|
||||
Parallel.For (0, numTasks, i =>
|
||||
{
|
||||
var clone = DeepCloner.DeepClone (configProperty);
|
||||
Assert.NotSame (configProperty, clone);
|
||||
Assert.Equal ("DeepCloneValue", clone.PropertyValue);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentDictionary_UpdateFrom_IsThreadSafe ()
|
||||
{
|
||||
var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value));
|
||||
var destinationDict = new ConcurrentDictionary<string, ConfigProperty> (StringComparer.InvariantCultureIgnoreCase);
|
||||
destinationDict.TryAdd ("prop1", new ConfigProperty { PropertyValue = "Original", HasValue = true });
|
||||
|
||||
var configProperty = new ConfigProperty
|
||||
{
|
||||
PropertyInfo = propertyInfo,
|
||||
PropertyValue = destinationDict
|
||||
};
|
||||
|
||||
int numTasks = 20;
|
||||
Parallel.For (0, numTasks, i =>
|
||||
{
|
||||
var sourceDict = new ConcurrentDictionary<string, ConfigProperty> (StringComparer.InvariantCultureIgnoreCase);
|
||||
sourceDict.TryAdd ($"prop{i}", new ConfigProperty { PropertyValue = $"Value_{i}", HasValue = true });
|
||||
configProperty.UpdateFrom (sourceDict);
|
||||
});
|
||||
|
||||
var resultDict = configProperty.PropertyValue as ConcurrentDictionary<string, ConfigProperty>;
|
||||
Assert.NotNull (resultDict);
|
||||
for (int i = 0; i < numTasks; i++)
|
||||
{
|
||||
Assert.True (resultDict!.ContainsKey ($"prop{i}"));
|
||||
Assert.Equal ($"Value_{i}", resultDict [$"prop{i}"].PropertyValue);
|
||||
}
|
||||
Assert.True (resultDict!.ContainsKey ("prop1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ConfigurationManagerTests
|
||||
{
|
||||
[ConfigurationProperty (Scope = typeof (CMTestsScope))]
|
||||
public static bool? TestProperty { get; set; }
|
||||
|
||||
private class CMTestsScope : Scope<CMTestsScope>
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConfigPropertiesByScope_Gets ()
|
||||
{
|
||||
var props = ConfigurationManager.GetUninitializedConfigPropertiesByScope ("CMTestsScope");
|
||||
|
||||
Assert.NotNull (props);
|
||||
Assert.NotEmpty (props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ConfigurationPropertyAttributeTests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// If this test fails, you need to add a new property to <see cref="SourceGenerationContext"/> to support serialization of the new property type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Verify_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 [ConfigurationProperty] attribute
|
||||
var properties = new List<PropertyInfo> ();
|
||||
foreach (var type in types)
|
||||
{
|
||||
properties.AddRange (type.GetProperties ().Where (p =>
|
||||
p.GetCustomAttributes (typeof (ConfigurationPropertyAttribute), 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<>
|
||||
EnsureNoSpecifiedConverters (properties, new [] { typeof (JsonStringEnumConverter<>) });
|
||||
// Ensure no property has the type RuneJsonConverter
|
||||
EnsureNoSpecifiedConverters (properties, new [] { typeof (RuneJsonConverter) });
|
||||
// Ensure no property has the type KeyJsonConverter
|
||||
EnsureNoSpecifiedConverters (properties, new [] { typeof (KeyJsonConverter) });
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void OmitClassName_Omits ()
|
||||
{
|
||||
// Color.Schemes is serialized as "Schemes", not "Colors.Schemes"
|
||||
PropertyInfo pi = typeof (SchemeManager).GetProperty ("Schemes")!;
|
||||
var scp = (ConfigurationPropertyAttribute)pi!.GetCustomAttribute (typeof (ConfigurationPropertyAttribute))!;
|
||||
Assert.True (scp!.Scope == typeof (ThemeScope));
|
||||
Assert.True (scp.OmitClassName);
|
||||
}
|
||||
|
||||
private static 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 ();
|
||||
}
|
||||
|
||||
private static void EnsureNoSpecifiedConverters (List<PropertyInfo> properties, IEnumerable<Type> converterTypes)
|
||||
{
|
||||
// Ensure no property has any of the specified converter types
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var jsonConverterAttributes = property.GetCustomAttributes (typeof (JsonConverterAttribute), false)
|
||||
.Cast<JsonConverterAttribute> ();
|
||||
|
||||
foreach (var attribute in jsonConverterAttributes)
|
||||
{
|
||||
foreach (var converterType in converterTypes)
|
||||
{
|
||||
if (attribute.ConverterType!.IsGenericType &&
|
||||
attribute.ConverterType.GetGenericTypeDefinition () == converterType)
|
||||
{
|
||||
Assert.Fail ($"Property '{property.Name}' should not use the converter '{converterType.Name}'.");
|
||||
}
|
||||
|
||||
if (!attribute.ConverterType!.IsGenericType &&
|
||||
attribute.ConverterType == converterType)
|
||||
{
|
||||
Assert.Fail ($"Property '{property.Name}' should not use the converter '{converterType.Name}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
673
Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs
Normal file
673
Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs
Normal file
@@ -0,0 +1,673 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="DeepCloner"/> class, ensuring robust deep cloning for
|
||||
/// Terminal.Gui's configuration system.
|
||||
/// </summary>
|
||||
public class DeepClonerTests
|
||||
{
|
||||
// Test classes for complex scenarios
|
||||
private class SimpleValueType
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public bool Flag { get; init; }
|
||||
}
|
||||
|
||||
private class SimpleReferenceType
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public int Count { get; set; }
|
||||
|
||||
public override bool Equals (object? obj) { return obj is SimpleReferenceType other && Name == other.Name && Count == other.Count; }
|
||||
|
||||
// ReSharper disable twice NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode () { return HashCode.Combine (Name, Count); }
|
||||
}
|
||||
|
||||
private class CollectionContainer
|
||||
{
|
||||
public List<string>? Strings { get; init; }
|
||||
public Dictionary<string, int>? Counts { get; init; }
|
||||
public int []? Numbers { get; init; }
|
||||
}
|
||||
|
||||
private class NestedObject
|
||||
{
|
||||
public SimpleReferenceType? Inner { get; init; }
|
||||
public List<SimpleValueType>? Values { get; init; }
|
||||
}
|
||||
|
||||
private class CircularReference
|
||||
{
|
||||
public CircularReference? Self { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
private class ConfigPropertyMock
|
||||
{
|
||||
public object? PropertyValue { get; init; }
|
||||
public bool Immutable { get; init; }
|
||||
}
|
||||
|
||||
private class SettingsScopeMock : Dictionary<string, ConfigPropertyMock>
|
||||
{
|
||||
public string? Theme { get; set; }
|
||||
}
|
||||
|
||||
private class ComplexKey
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public override bool Equals (object? obj) { return obj is ComplexKey key && Id == key.Id; }
|
||||
|
||||
public override int GetHashCode () { return Id.GetHashCode (); }
|
||||
}
|
||||
|
||||
private class KeyEqualityComparer : IEqualityComparer<Key>
|
||||
{
|
||||
public bool Equals (Key? x, Key? y) { return x?.KeyCode == y?.KeyCode; }
|
||||
|
||||
public int GetHashCode (Key obj) { return obj.KeyCode.GetHashCode (); }
|
||||
}
|
||||
|
||||
// Fundamentals
|
||||
|
||||
[Fact]
|
||||
public void Null_ReturnsNull ()
|
||||
{
|
||||
object? source = null;
|
||||
object? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.Null (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SimpleValueType_ReturnsEqualValue ()
|
||||
{
|
||||
var source = 42;
|
||||
int result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void String_ReturnsSameString ()
|
||||
{
|
||||
var source = "Hello";
|
||||
string? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.Equal (source, result);
|
||||
Assert.Same (source, result); // Strings are immutable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rune_ReturnsEqualRune ()
|
||||
{
|
||||
Rune source = new ('A');
|
||||
Rune result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Key_CreatesDeepCopy ()
|
||||
{
|
||||
Key? source = new (KeyCode.A);
|
||||
source.Handled = true;
|
||||
Key? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source.KeyCode, result.KeyCode);
|
||||
Assert.Equal (source.Handled, result.Handled);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Handled = false;
|
||||
Assert.True (source.Handled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enum_ReturnsEqualEnum ()
|
||||
{
|
||||
var source = DayOfWeek.Monday;
|
||||
DayOfWeek result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Boolean_ReturnsEqualValue ()
|
||||
{
|
||||
var source = true;
|
||||
bool result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
source = false;
|
||||
result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attribute_ReturnsEqualValue ()
|
||||
{
|
||||
var source = new Attribute (Color.Black);
|
||||
Attribute result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
source = new (Color.White);
|
||||
result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scheme_Normal_Set_ReturnsEqualValue ()
|
||||
{
|
||||
var source = new Scheme (new Scheme (new Attribute (Color.Red, Color.Green, TextStyle.Bold)));
|
||||
Scheme? result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
source = new Scheme (new Scheme ());
|
||||
result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scheme_All_Set_ReturnsEqualValue ()
|
||||
{
|
||||
Scheme? source = new ()
|
||||
{
|
||||
Normal = new ("LightGray", "RaisinBlack", TextStyle.None),
|
||||
Focus = new ("White", "DarkGray", TextStyle.None),
|
||||
HotNormal = new ("Silver", "RaisinBlack", TextStyle.Underline),
|
||||
Disabled = new ("DarkGray", "RaisinBlack", TextStyle.Faint),
|
||||
HotFocus = new ("White", "Green", TextStyle.Underline),
|
||||
Active = new ("White", "Charcoal", TextStyle.Bold),
|
||||
HotActive = new ("White", "Charcoal", TextStyle.Underline | TextStyle.Bold),
|
||||
Highlight = new ("White", "Onyx", TextStyle.None),
|
||||
Editable = new ("LightYellow", "RaisinBlack", TextStyle.None),
|
||||
ReadOnly = new ("Gray", "RaisinBlack", TextStyle.Italic)
|
||||
};
|
||||
Scheme? result = DeepCloner.DeepClone (source);
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Normal, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotNormal, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Focus, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotFocus, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotActive, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Highlight, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Editable, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.ReadOnly, out _));
|
||||
Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Disabled, out _));
|
||||
|
||||
Assert.Equal (source, result);
|
||||
|
||||
source = new Scheme (new Scheme ());
|
||||
result = DeepCloner.DeepClone (source);
|
||||
Assert.Equal (source, result);
|
||||
}
|
||||
|
||||
// Simple Reference Types
|
||||
|
||||
[Fact]
|
||||
public void SimpleReferenceType_CreatesDeepCopy ()
|
||||
{
|
||||
SimpleReferenceType? source = new () { Name = "Test", Count = 10 };
|
||||
SimpleReferenceType? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source.Name, result!.Name);
|
||||
Assert.Equal (source.Count, result.Count);
|
||||
Assert.True (source.Equals (result)); // Verify Equals method
|
||||
Assert.Equal (source.GetHashCode (), result.GetHashCode ()); // Verify GetHashCode
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Name = "Modified";
|
||||
result.Count = 20;
|
||||
Assert.Equal ("Test", source.Name);
|
||||
Assert.Equal (10, source.Count);
|
||||
}
|
||||
|
||||
// Collections
|
||||
|
||||
[Fact]
|
||||
public void List_CreatesDeepCopy ()
|
||||
{
|
||||
List<string>? source = new () { "One", "Two" };
|
||||
List<string>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result!.Add ("Three");
|
||||
Assert.Equal (2, source.Count);
|
||||
Assert.Equal (3, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_CreatesDeepCopy ()
|
||||
{
|
||||
Dictionary<string, int>? source = new () { { "A", 1 }, { "B", 2 } };
|
||||
Dictionary<string, int>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result! ["C"] = 3;
|
||||
Assert.Equal (2, source.Count);
|
||||
Assert.Equal (3, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_CreatesDeepCopy_Including_Comparer_Options ()
|
||||
{
|
||||
Dictionary<string, int>? source = new (StringComparer.InvariantCultureIgnoreCase) { { "A", 1 }, { "B", 2 } };
|
||||
Dictionary<string, int>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result);
|
||||
Assert.Equal (source.Comparer, result.Comparer);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result! ["C"] = 3;
|
||||
Assert.Equal (2, source.Count);
|
||||
Assert.Equal (3, result.Count);
|
||||
|
||||
Assert.Contains ("A", result);
|
||||
Assert.Contains ("a", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_CreatesDeepCopy_WithCapacity ()
|
||||
{
|
||||
// Arrange: Create a dictionary with a specific capacity
|
||||
Dictionary<string, int> source = new (100) // Set initial capacity to 100
|
||||
{
|
||||
{ "Key1", 1 },
|
||||
{ "Key2", 2 }
|
||||
};
|
||||
|
||||
// Act: Clone the dictionary
|
||||
Dictionary<string, int>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
// Assert: Verify the dictionary was cloned correctly
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result); // Verify key-value pairs are cloned
|
||||
|
||||
// Verify that the capacity is preserved (if supported)
|
||||
Assert.True (result.Count <= result.EnsureCapacity (0)); // EnsureCapacity(0) returns the current capacity
|
||||
Assert.True (source.Count <= source.EnsureCapacity (0)); // EnsureCapacity(0) returns the current capacity
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentDictionary_CreatesDeepCopy ()
|
||||
{
|
||||
ConcurrentDictionary<string, int>? source = new (new Dictionary<string, int> () { { "A", 1 }, { "B", 2 } });
|
||||
ConcurrentDictionary<string, int>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result! ["C"] = 3;
|
||||
Assert.Equal (2, source.Count);
|
||||
Assert.Equal (3, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Array_CreatesDeepCopy ()
|
||||
{
|
||||
int []? source = { 1, 2, 3 };
|
||||
int []? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source, result);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result! [0] = 99;
|
||||
Assert.Equal (1, source [0]);
|
||||
Assert.Equal (99, result [0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImmutableList_ThrowsNotSupported ()
|
||||
{
|
||||
ImmutableList<string> source = ImmutableList.Create ("One", "Two");
|
||||
Assert.Throws<NotSupportedException> (() => DeepCloner.DeepClone (source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImmutableDictionary_ThrowsNotSupported ()
|
||||
{
|
||||
ImmutableDictionary<string, int> source = ImmutableDictionary.Create<string, int> ().Add ("A", 1);
|
||||
Assert.Throws<NotSupportedException> (() => DeepCloner.DeepClone (source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_SourceAddsItem_ClonesCorrectly ()
|
||||
{
|
||||
Dictionary<string, Attribute>? source = new ()
|
||||
{
|
||||
{ "Disabled", new (Color.White) },
|
||||
{ "Normal", new (Color.Blue) }
|
||||
};
|
||||
Dictionary<string, Attribute>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (2, result.Count);
|
||||
Assert.Equal ((Attribute)source ["Disabled"], (Attribute)result ["Disabled"]);
|
||||
Assert.Equal (source ["Normal"], result ["Normal"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_SourceUpdatesOneItem_ClonesCorrectly ()
|
||||
{
|
||||
Dictionary<string, Attribute>? source = new () { { "Disabled", new (Color.White) } };
|
||||
Dictionary<string, Attribute>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Single (result);
|
||||
Assert.Equal (source ["Disabled"], result ["Disabled"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_WithComplexKeys_ClonesCorrectly ()
|
||||
{
|
||||
Dictionary<ComplexKey, string>? source = new ()
|
||||
{
|
||||
{ new() { Id = 1 }, "Value1" }
|
||||
};
|
||||
Dictionary<ComplexKey, string>? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source.Keys.First ().Id, result.Keys.First ().Id);
|
||||
Assert.Equal (source.Values.First (), result.Values.First ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dictionary_WithCustomKeyComparer_ClonesCorrectly ()
|
||||
{
|
||||
Dictionary<Key, string> source = new (new KeyEqualityComparer ())
|
||||
{
|
||||
{ new (KeyCode.Esc), "Esc" }
|
||||
};
|
||||
Dictionary<Key, string> result = DeepCloner.DeepClone (source)!;
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Single (result);
|
||||
Assert.True (result.ContainsKey (new (KeyCode.Esc)));
|
||||
Assert.Equal ("Esc", result [new (KeyCode.Esc)]);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result [new (KeyCode.Q)] = "Q";
|
||||
Assert.False (source.ContainsKey (new (KeyCode.Q)));
|
||||
}
|
||||
|
||||
// Nested Objects
|
||||
|
||||
[Fact]
|
||||
public void CollectionContainer_CreatesDeepCopy ()
|
||||
{
|
||||
CollectionContainer? source = new ()
|
||||
{
|
||||
Strings = ["A", "B"],
|
||||
Counts = new () { { "X", 1 }, { "Y", 2 } },
|
||||
Numbers = [10, 20]
|
||||
};
|
||||
CollectionContainer? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.NotSame (source.Strings, result!.Strings);
|
||||
Assert.NotSame (source.Counts, result.Counts);
|
||||
Assert.NotSame (source.Numbers, result.Numbers);
|
||||
Assert.Equal (source.Strings, result.Strings);
|
||||
Assert.Equal (source.Counts, result.Counts);
|
||||
Assert.Equal (source.Numbers, result.Numbers);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Strings!.Add ("C");
|
||||
result.Counts! ["Z"] = 3;
|
||||
result.Numbers! [0] = 99;
|
||||
Assert.Equal (2, source.Strings.Count);
|
||||
Assert.Equal (2, source.Counts.Count);
|
||||
Assert.Equal (10, source.Numbers [0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedObject_CreatesDeepCopy ()
|
||||
{
|
||||
NestedObject? source = new ()
|
||||
{
|
||||
Inner = new () { Name = "Inner", Count = 5 },
|
||||
Values = new ()
|
||||
{
|
||||
new() { Number = 1, Flag = true },
|
||||
new() { Number = 2, Flag = false }
|
||||
}
|
||||
};
|
||||
NestedObject? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.NotSame (source.Inner, result!.Inner);
|
||||
Assert.NotSame (source.Values, result.Values);
|
||||
Assert.Equal (source.Inner!.Name, result.Inner!.Name);
|
||||
Assert.Equal (source.Inner.Count, result.Inner.Count);
|
||||
Assert.Equal (source.Values! [0].Number, result.Values! [0].Number);
|
||||
Assert.Equal (source.Values [0].Flag, result.Values [0].Flag);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Inner.Name = "Modified";
|
||||
result.Values [0].Number = 99;
|
||||
Assert.Equal ("Inner", source.Inner.Name);
|
||||
Assert.Equal (1, source.Values [0].Number);
|
||||
}
|
||||
|
||||
// Circular References
|
||||
|
||||
[Fact]
|
||||
public void CircularReference_HandlesCorrectly ()
|
||||
{
|
||||
CircularReference? source = new () { Name = "Cycle" };
|
||||
source.Self = source; // Create circular reference
|
||||
CircularReference? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source.Name, result!.Name);
|
||||
Assert.NotNull (result.Self);
|
||||
Assert.Same (result, result.Self); // Circular reference preserved
|
||||
Assert.NotSame (source.Self, result.Self);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Name = "Modified";
|
||||
Assert.Equal ("Cycle", source.Name);
|
||||
}
|
||||
|
||||
// Terminal.Gui-Specific Types
|
||||
|
||||
[Fact]
|
||||
public void ConfigPropertyMock_CreatesDeepCopy ()
|
||||
{
|
||||
ConfigPropertyMock? source = new ()
|
||||
{
|
||||
PropertyValue = new List<string> { "Red", "Blue" },
|
||||
Immutable = true
|
||||
};
|
||||
ConfigPropertyMock? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.NotSame (source.PropertyValue, result!.PropertyValue);
|
||||
Assert.Equal ((List<string>)source.PropertyValue, (List<string>)result.PropertyValue!);
|
||||
Assert.Equal (source.Immutable, result.Immutable);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
((List<string>)result.PropertyValue!).Add ("Green");
|
||||
Assert.Equal (2, ((List<string>)source.PropertyValue).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigProperty_CreatesDeepCopy ()
|
||||
{
|
||||
ConfigProperty? source = ConfigProperty.CreateImmutableWithAttributeInfo (CM.GetHardCodedConfigPropertyCache ()! ["Application.QuitKey"].PropertyInfo!);
|
||||
source.Immutable = false;
|
||||
source.PropertyValue = Key.A;
|
||||
ConfigProperty? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotNull (result.PropertyInfo);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.NotSame (source.PropertyValue, result!.PropertyValue);
|
||||
// PropertyInfo is effectively a simple type
|
||||
Assert.Same (source.PropertyInfo, result!.PropertyInfo);
|
||||
Assert.Equal (source.Immutable, result.Immutable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsScopeMockWithKey_CreatesDeepCopy ()
|
||||
{
|
||||
SettingsScopeMock? source = new ()
|
||||
{
|
||||
Theme = "Dark",
|
||||
["KeyBinding"] = new () { PropertyValue = new Key (KeyCode.A) { Handled = true } },
|
||||
["Counts"] = new () { PropertyValue = new Dictionary<string, int> { { "X", 1 } } }
|
||||
};
|
||||
SettingsScopeMock? result = DeepCloner.DeepClone (source);
|
||||
|
||||
Assert.NotNull (result);
|
||||
Assert.NotSame (source, result);
|
||||
Assert.Equal (source.Theme, result!.Theme);
|
||||
Assert.NotSame (source ["KeyBinding"], result ["KeyBinding"]);
|
||||
Assert.NotSame (source ["Counts"], result ["Counts"]);
|
||||
|
||||
ConfigPropertyMock clonedKeyProp = result ["KeyBinding"];
|
||||
var clonedKey = (Key)clonedKeyProp.PropertyValue!;
|
||||
Assert.NotSame (source ["KeyBinding"].PropertyValue, clonedKey);
|
||||
Assert.Equal (((Key)source ["KeyBinding"].PropertyValue!).KeyCode, clonedKey.KeyCode);
|
||||
Assert.Equal (((Key)source ["KeyBinding"].PropertyValue!).Handled, clonedKey.Handled);
|
||||
|
||||
Assert.Equal ((Dictionary<string, int>)source ["Counts"].PropertyValue!, (Dictionary<string, int>)result ["Counts"].PropertyValue!);
|
||||
|
||||
// Modify result, ensure source unchanged
|
||||
result.Theme = "Light";
|
||||
clonedKey.Handled = false;
|
||||
((Dictionary<string, int>)result ["Counts"].PropertyValue!).Add ("Y", 2);
|
||||
Assert.Equal ("Dark", source.Theme);
|
||||
Assert.True (((Key)source ["KeyBinding"].PropertyValue!).Handled);
|
||||
Assert.Single ((Dictionary<string, int>)source ["Counts"].PropertyValue!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThemeScopeList_WithThemes_ClonesSuccessfully ()
|
||||
{
|
||||
// Arrange: Create a ThemeScope and verify a property exists
|
||||
var defaultThemeScope = new ThemeScope ();
|
||||
defaultThemeScope.LoadHardCodedDefaults ();
|
||||
Assert.True (defaultThemeScope.ContainsKey ("Button.DefaultHighlightStyle"));
|
||||
|
||||
var darkThemeScope = new ThemeScope ();
|
||||
darkThemeScope.LoadHardCodedDefaults ();
|
||||
Assert.True (darkThemeScope.ContainsKey ("Button.DefaultHighlightStyle"));
|
||||
|
||||
// Create a Themes list with two themes
|
||||
List<Dictionary<string, ThemeScope>> themesList =
|
||||
[
|
||||
new () { { "Default", defaultThemeScope } },
|
||||
new () { { "Dark", darkThemeScope } }
|
||||
];
|
||||
|
||||
// Create a SettingsScope and set the Themes property
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
Assert.True (settingsScope.ContainsKey ("Themes"));
|
||||
settingsScope ["Themes"].PropertyValue = themesList;
|
||||
|
||||
// Act
|
||||
SettingsScope? result = DeepCloner.DeepClone (settingsScope);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (result);
|
||||
Assert.IsType<SettingsScope> (result);
|
||||
var resultScope = (SettingsScope)result;
|
||||
Assert.True (resultScope.ContainsKey ("Themes"));
|
||||
|
||||
Assert.NotNull (resultScope ["Themes"].PropertyValue);
|
||||
|
||||
List<Dictionary<string, ThemeScope>> clonedThemes = (List<Dictionary<string, ThemeScope>>)resultScope ["Themes"].PropertyValue!;
|
||||
Assert.Equal (2, clonedThemes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_SettingsScope_ClonesSuccessfully ()
|
||||
{
|
||||
// Arrange: Create a SettingsScope
|
||||
var settingsScope = new SettingsScope ();
|
||||
Assert.True (settingsScope.ContainsKey ("Themes"));
|
||||
|
||||
// Act
|
||||
SettingsScope? result = DeepCloner.DeepClone (settingsScope);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (result);
|
||||
Assert.IsType<SettingsScope> (result);
|
||||
|
||||
Assert.True (result.ContainsKey ("Themes"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsScope_With_Themes_Set_ClonesSuccessfully ()
|
||||
{
|
||||
// Arrange: Create a SettingsScope
|
||||
var settingsScope = new SettingsScope ();
|
||||
Assert.True (settingsScope.ContainsKey ("Themes"));
|
||||
|
||||
settingsScope ["Themes"].PropertyValue = new List<Dictionary<string, ThemeScope>>
|
||||
{
|
||||
new() { { "Default", new () } },
|
||||
new() { { "Dark", new () } }
|
||||
};
|
||||
|
||||
// Act
|
||||
SettingsScope? result = DeepCloner.DeepClone (settingsScope);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (result);
|
||||
Assert.IsType<SettingsScope> (result);
|
||||
Assert.True (result.ContainsKey ("Themes"));
|
||||
Assert.NotNull (result ["Themes"].PropertyValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeObject_PerformsWithinLimit ()
|
||||
{
|
||||
List<int> source = new (Enumerable.Range (1, 10000));
|
||||
var stopwatch = Stopwatch.StartNew ();
|
||||
List<int> result = DeepCloner.DeepClone (source)!;
|
||||
stopwatch.Stop ();
|
||||
|
||||
Assert.Equal (source, result);
|
||||
Assert.True (stopwatch.ElapsedMilliseconds < 1000); // Ensure it completes within 1 second
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class KeyCodeJsonConverterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData (KeyCode.A, "A")]
|
||||
[InlineData (KeyCode.A | KeyCode.ShiftMask, "A, ShiftMask")]
|
||||
[InlineData (KeyCode.A | KeyCode.CtrlMask, "A, CtrlMask")]
|
||||
[InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "A, CtrlMask, AltMask")]
|
||||
[InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "Space, A, CtrlMask, AltMask")]
|
||||
[InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "Space, A, ShiftMask")]
|
||||
[InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Delete, CtrlMask, AltMask")]
|
||||
[InlineData (KeyCode.D4, "D4")]
|
||||
[InlineData (KeyCode.Esc, "Esc")]
|
||||
public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
|
||||
{
|
||||
// Arrange
|
||||
var options = new JsonSerializerOptions ();
|
||||
options.Converters.Add (new KeyCodeJsonConverter ());
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (key, options);
|
||||
var deserializedKey = JsonSerializer.Deserialize<KeyCode> (json, options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedStringTo, deserializedKey.ToString ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Unicode;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class KeyJsonConverterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData (KeyCode.A, "\"a\"")]
|
||||
[InlineData ((KeyCode)'â', "\"â\"")]
|
||||
[InlineData (KeyCode.A | KeyCode.ShiftMask, "\"A\"")]
|
||||
[InlineData (KeyCode.A | KeyCode.CtrlMask, "\"Ctrl+A\"")]
|
||||
[InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+A\"")]
|
||||
[InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt+Delete\"")]
|
||||
[InlineData (KeyCode.D4, "\"4\"")]
|
||||
[InlineData (KeyCode.Esc, "\"Esc\"")]
|
||||
[InlineData ((KeyCode)'+' | KeyCode.AltMask | KeyCode.CtrlMask, "\"Ctrl+Alt++\"")]
|
||||
public void TestKey_Serialize (KeyCode key, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var options = new JsonSerializerOptions ();
|
||||
options.Converters.Add (new KeyJsonConverter ());
|
||||
options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize ((Key)key, options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expected, json);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (KeyCode.A, "a")]
|
||||
[InlineData (KeyCode.A | KeyCode.ShiftMask, "A")]
|
||||
[InlineData (KeyCode.A | KeyCode.CtrlMask, "Ctrl+A")]
|
||||
[InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+A")]
|
||||
[InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Delete")]
|
||||
[InlineData (KeyCode.D4, "4")]
|
||||
[InlineData (KeyCode.Esc, "Esc")]
|
||||
[InlineData ((KeyCode)'+' | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt++")]
|
||||
public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize ((Key)key, ConfigurationManager.SerializerContext.Options);
|
||||
var deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedStringTo, deserializedKey.ToString ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialized_Key_Equals ()
|
||||
{
|
||||
// Arrange
|
||||
Key key = Key.Q.WithCtrl;
|
||||
|
||||
// Act
|
||||
string json = "\"Ctrl+Q\"";
|
||||
Key deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (key, deserializedKey);
|
||||
|
||||
}
|
||||
[Fact]
|
||||
public void Separator_Property_Serializes_As_Glyph ()
|
||||
{
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (Key.Separator, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ($"\"{Key.Separator}\"", json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class RuneJsonConverterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData ("aa")]
|
||||
[InlineData ("☑☑")]
|
||||
[InlineData ("\\x2611")]
|
||||
[InlineData ("Z+2611")]
|
||||
[InlineData ("🍎🍎")]
|
||||
[InlineData ("U+FFF1F34E")]
|
||||
[InlineData ("\\UFFF1F34E")]
|
||||
[InlineData ("\\ud83d")] // not printable "high surrogate"
|
||||
[InlineData ("\\udc3d")] // not printable "low surrogate"
|
||||
[InlineData ("\\ud83d \\u1c69")] // bad surrogate pair
|
||||
[InlineData ("\\ud83ddc69")]
|
||||
|
||||
// Emoji - Family Unit:
|
||||
// Woman (U+1F469, 👩)
|
||||
// Zero Width Joiner (U+200D)
|
||||
// Woman (U+1F469, 👩)
|
||||
// Zero Width Joiner (U+200D)
|
||||
// Girl (U+1F467, 👧)
|
||||
// Zero Width Joiner (U+200D)
|
||||
// Girl (U+1F467, 👧)
|
||||
[InlineData ("U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467")]
|
||||
[InlineData ("\\U0001F469\\u200D\\U0001F469\\u200D\\U0001F467\\u200D\\U0001F467")]
|
||||
public void RoundTripConversion_Negative (string rune)
|
||||
{
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (rune, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Throws<JsonException> (
|
||||
() => JsonSerializer.Deserialize<Rune> (
|
||||
json,
|
||||
ConfigurationManager.SerializerContext.Options
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData ("a", "a")]
|
||||
[InlineData ("☑", "☑")]
|
||||
[InlineData ("\\u2611", "☑")]
|
||||
[InlineData ("U+2611", "☑")]
|
||||
[InlineData ("🍎", "🍎")]
|
||||
[InlineData ("U+1F34E", "🍎")]
|
||||
[InlineData ("\\U0001F34E", "🍎")]
|
||||
[InlineData ("\\ud83d \\udc69", "👩")]
|
||||
[InlineData ("\\ud83d\\udc69", "👩")]
|
||||
[InlineData ("U+d83d U+dc69", "👩")]
|
||||
[InlineData ("U+1F469", "👩")]
|
||||
[InlineData ("\\U0001F469", "👩")]
|
||||
[InlineData ("\\u0065\\u0301", "é")]
|
||||
public void RoundTripConversion_Positive (string rune, string expected)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize (rune, ConfigurationManager.SerializerContext.Options);
|
||||
var deserialized = JsonSerializer.Deserialize<Rune> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expected, deserialized.ToString ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Printable_Rune_Is_Serialized_As_Glyph ()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize ((Rune)'a', ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("\"a\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Non_Printable_Rune_Is_Serialized_As_u_Encoded_Value ()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize ((Rune)0x01, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("\"\\u0001\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Json_With_Glyph_Works ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "\"a\"";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<Rune> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("a", deserialized.ToString ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Json_With_u_Encoded_Works ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "\"\\u0061\"";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<Rune> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("a", deserialized.ToString ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Json_With_U_Encoded_Works ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "\"U+0061\"";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<Rune> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("a", deserialized.ToString ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Json_With_Decimal_Works ()
|
||||
{
|
||||
// Arrange
|
||||
var json = "97";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<Rune> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("a", deserialized.ToString ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class SchemeJsonConverterTests
|
||||
{
|
||||
//string json = @"
|
||||
// {
|
||||
// ""Schemes"": {
|
||||
// ""Base"": {
|
||||
// ""normal"": {
|
||||
// ""foreground"": ""White"",
|
||||
// ""background"": ""Blue""
|
||||
// },
|
||||
// ""focus"": {
|
||||
// ""foreground"": ""Black"",
|
||||
// ""background"": ""Gray""
|
||||
// },
|
||||
// ""hotNormal"": {
|
||||
// ""foreground"": ""BrightCyan"",
|
||||
// ""background"": ""Blue""
|
||||
// },
|
||||
// ""hotFocus"": {
|
||||
// ""foreground"": ""BrightBlue"",
|
||||
// ""background"": ""Gray""
|
||||
// },
|
||||
// ""disabled"": {
|
||||
// ""foreground"": ""DarkGray"",
|
||||
// ""background"": ""Blue""
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }";
|
||||
[Fact]
|
||||
public void TestSchemesSerialization_Equality ()
|
||||
{
|
||||
// Arrange
|
||||
var expectedScheme = new Scheme
|
||||
{
|
||||
Normal = new (Color.White, Color.Blue),
|
||||
Focus = new (Color.Black, Color.Gray),
|
||||
HotNormal = new (Color.BrightCyan, Color.Blue),
|
||||
HotFocus = new (Color.BrightBlue, Color.Gray),
|
||||
Active = new (Color.Gray, Color.Black),
|
||||
HotActive = new (Color.Blue, Color.Gray),
|
||||
Highlight = new (Color.Gray, Color.Black),
|
||||
Editable = new (Color.Gray, Color.Black),
|
||||
ReadOnly = new (Color.Gray, Color.Black),
|
||||
Disabled = new (Color.Gray, Color.Black),
|
||||
};
|
||||
|
||||
string serializedScheme =
|
||||
JsonSerializer.Serialize (expectedScheme, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Act
|
||||
var actualScheme =
|
||||
JsonSerializer.Deserialize<Scheme> (serializedScheme, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedScheme, actualScheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestSchemesSerialization ()
|
||||
{
|
||||
var expected = new Scheme
|
||||
{
|
||||
Normal = new (Color.White, Color.Blue),
|
||||
Focus = new (Color.Black, Color.Gray),
|
||||
HotNormal = new (Color.BrightCyan, Color.Blue),
|
||||
HotFocus = new (Color.BrightBlue, Color.Gray),
|
||||
Active = new (Color.Gray, Color.Black),
|
||||
HotActive = new (Color.Blue, Color.Gray),
|
||||
Highlight = new (Color.Gray, Color.Black),
|
||||
Editable = new (Color.Gray, Color.Black),
|
||||
ReadOnly = new (Color.Gray, Color.Black),
|
||||
Disabled = new (Color.Gray, Color.Black),
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize (expected, ConfigurationManager.SerializerContext.Options);
|
||||
Scheme actual = JsonSerializer.Deserialize<Scheme> (json, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
Assert.NotNull (actual);
|
||||
|
||||
foreach (VisualRole role in Enum.GetValues<VisualRole> ())
|
||||
{
|
||||
Attribute expectedAttr = expected.GetAttributeForRole (role);
|
||||
Attribute actualAttr = actual.GetAttributeForRole (role);
|
||||
|
||||
Assert.Equal (expectedAttr, actualAttr);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialized_Attributes_AreExplicitlySet ()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Normal": { "Foreground": "White", "Background": "Blue" },
|
||||
"Focus": { "Foreground": "Black", "Background": "Gray" },
|
||||
"HotNormal": { "Foreground": "BrightCyan", "Background": "Blue" },
|
||||
"HotFocus": { "Foreground": "BrightBlue", "Background": "Gray" },
|
||||
"Active": { "Foreground": "DarkGray", "Background": "Blue" },
|
||||
"HotActive": { "Foreground": "DarkGray", "Background": "Blue" },
|
||||
"Highlight": { "Foreground": "DarkGray", "Background": "Blue" },
|
||||
"Editable": { "Foreground": "DarkGray", "Background": "Blue" },
|
||||
"Readonly": { "Foreground": "DarkGray", "Background": "Blue" },
|
||||
"Disabled": { "Foreground": "DarkGray", "Background": "Blue" }
|
||||
}
|
||||
""";
|
||||
|
||||
Scheme scheme = JsonSerializer.Deserialize<Scheme> (json, ConfigurationManager.SerializerContext.Options)!;
|
||||
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Normal, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotNormal, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Focus, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotFocus, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotActive, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Highlight, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Editable, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.ReadOnly, out _));
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Disabled, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialized_Attributes_NotSpecified_AreImplicit ()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Normal": { "Foreground": "White", "Background": "Black" }
|
||||
}
|
||||
""";
|
||||
|
||||
Scheme scheme = JsonSerializer.Deserialize<Scheme> (json, ConfigurationManager.SerializerContext.Options)!;
|
||||
|
||||
// explicitly set
|
||||
Assert.True (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Normal, out _));
|
||||
|
||||
// derived from Normal
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotNormal, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Focus, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotFocus, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotActive, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Highlight, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Editable, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.ReadOnly, out _));
|
||||
Assert.False (scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Disabled, out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class SchemeManagerTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public void GetHardCodedSchemes_Gets_HardCodedDefaults ()
|
||||
{
|
||||
var hardCoded = SchemeManager.GetHardCodedSchemes ();
|
||||
|
||||
// Check that the hardcoded schemes are not null
|
||||
Assert.NotNull (hardCoded);
|
||||
// Check that the hardcoded schemes are not empty
|
||||
Assert.NotEmpty (hardCoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddScheme_Adds_And_Updates_Scheme ()
|
||||
{
|
||||
// Arrange
|
||||
var scheme = new Scheme (new Attribute (Color.Red, Color.Green));
|
||||
string schemeName = "CustomScheme";
|
||||
|
||||
// Act
|
||||
SchemeManager.AddScheme (schemeName, scheme);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (scheme, SchemeManager.GetScheme (schemeName));
|
||||
|
||||
// Update the scheme
|
||||
var updatedScheme = new Scheme (new Attribute (Color.Blue, Color.Yellow));
|
||||
SchemeManager.AddScheme (schemeName, updatedScheme);
|
||||
|
||||
Assert.Equal (updatedScheme, SchemeManager.GetScheme (schemeName));
|
||||
|
||||
// Cleanup
|
||||
SchemeManager.RemoveScheme (schemeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveScheme_Removes_Custom_Scheme ()
|
||||
{
|
||||
var scheme = new Scheme (new Attribute (Color.Red, Color.Green));
|
||||
string schemeName = "RemovableScheme";
|
||||
SchemeManager.AddScheme (schemeName, scheme);
|
||||
|
||||
Assert.Equal (scheme, SchemeManager.GetScheme (schemeName));
|
||||
|
||||
SchemeManager.RemoveScheme (schemeName);
|
||||
|
||||
Assert.Throws<KeyNotFoundException> (() => SchemeManager.GetScheme (schemeName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveScheme_Throws_On_BuiltIn_Scheme ()
|
||||
{
|
||||
// Built-in scheme name
|
||||
Assert.Throws<InvalidOperationException> (() => SchemeManager.RemoveScheme ("Base"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveScheme_Throws_On_NonExistent_Scheme ()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException> (() => SchemeManager.RemoveScheme ("DoesNotExist"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScheme_By_Enum_Returns_Scheme ()
|
||||
{
|
||||
var scheme = SchemeManager.GetScheme (Schemes.Base);
|
||||
Assert.NotNull (scheme);
|
||||
Assert.IsType<Scheme> (scheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSchemeNames_Returns_All_Scheme_Names ()
|
||||
{
|
||||
var names = SchemeManager.GetSchemeNames ();
|
||||
Assert.NotNull (names);
|
||||
Assert.Contains ("Base", names);
|
||||
Assert.Contains ("Menu", names);
|
||||
Assert.Contains ("Dialog", names);
|
||||
Assert.Contains ("Toplevel", names);
|
||||
Assert.Contains ("Error", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemesToSchemeName_And_SchemeNameToSchemes_RoundTrip ()
|
||||
{
|
||||
foreach (Schemes s in Enum.GetValues (typeof (Schemes)))
|
||||
{
|
||||
var name = SchemeManager.SchemesToSchemeName (s);
|
||||
Assert.NotNull (name);
|
||||
var roundTrip = SchemeManager.SchemeNameToSchemes (name!);
|
||||
Assert.NotNull (roundTrip);
|
||||
Assert.Equal (name, roundTrip);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScheme_Throws_On_Invalid_Enum ()
|
||||
{
|
||||
// Use an invalid enum value (not defined in Schemes)
|
||||
Assert.Throws<ArgumentException> (() => SchemeManager.GetScheme ((Schemes)999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScheme_Throws_On_Invalid_String ()
|
||||
{
|
||||
Assert.Throws<KeyNotFoundException> (() => SchemeManager.GetScheme ("NotAScheme"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ScopeJsonConverterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData ("\"ConfigurationManager.ThrowOnJsonErrors\":true")]
|
||||
[InlineData ("\"Key.Separator\":\"@\"")]
|
||||
[InlineData ("\"Themes\":[]")]
|
||||
[InlineData ("\"Themes\":[{\"themeName\":{}}]")]
|
||||
[InlineData ("\"Themes\":[{\"themeName\":{\"Dialog.DefaultButtonAlignment\":\"End\"}}]")]
|
||||
public void RoundTripConversion_Property_Positive (string configPropertyJson)
|
||||
{
|
||||
// Arrange
|
||||
string scopeJson = $"{{{configPropertyJson}}}";
|
||||
|
||||
// Act
|
||||
SettingsScope? deserialized = JsonSerializer.Deserialize<SettingsScope> (scopeJson, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
string? json = JsonSerializer.Serialize<SettingsScope> (deserialized!, ConfigurationManager.SerializerContext.Options);
|
||||
|
||||
// Strip all whitespace
|
||||
json = json.Replace (" ", string.Empty);
|
||||
json = json.Replace ("\n", string.Empty);
|
||||
json = json.Replace ("\r", string.Empty);
|
||||
json = json.Replace ("\t", string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.Contains (configPropertyJson, json);
|
||||
}
|
||||
}
|
||||
247
Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs
Normal file
247
Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
#nullable enable
|
||||
using System.Reflection;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ScopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Scope_Is_Empty ()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var scope = new Scope<object> ();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (scope);
|
||||
Assert.Empty (scope);
|
||||
}
|
||||
|
||||
// The property key will be "ScopeTests.BoolProperty"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsScope))]
|
||||
public static bool? BoolProperty { get; set; } = true;
|
||||
|
||||
// The property key will be "ScopeTests.StringProperty"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsScope))]
|
||||
public static string? StringProperty { get; set; } // null
|
||||
|
||||
// The property key will be "ScopeTests.KeyProperty"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsScope))]
|
||||
public static Key? KeyProperty { get; set; } = Key.A;
|
||||
|
||||
// The property key will be "ScopeTests.DictionaryProperty"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsScope))]
|
||||
public static Dictionary<string, ConfigProperty>? DictionaryProperty { get; set; }
|
||||
|
||||
public class ScopeTestsScope : Scope<ScopeTestsScope>
|
||||
{
|
||||
}
|
||||
|
||||
// The property key will be "ScopeTests.DictionaryItemProperty1"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsDictionaryItemScope))]
|
||||
public static string? DictionaryItemProperty1 { get; set; } // null
|
||||
|
||||
// The property key will be "ScopeTests.DictionaryItemProperty2"
|
||||
[ConfigurationProperty (Scope = typeof (ScopeTestsDictionaryItemScope))]
|
||||
public static string? DictionaryItemProperty2 { get; set; } // null
|
||||
public class ScopeTestsDictionaryItemScope : Scope<ScopeTestsDictionaryItemScope>
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void TestScope_Constructor_Creates_Properties ()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var scope = new ScopeTestsScope ();
|
||||
scope.LoadHardCodedDefaults ();
|
||||
|
||||
var cache = CM.GetHardCodedConfigPropertyCache ();
|
||||
|
||||
Assert.NotNull (cache);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (scope);
|
||||
Assert.True (scope.ContainsKey ("ScopeTests.BoolProperty"));
|
||||
|
||||
Assert.Equal (typeof (ScopeTests).GetProperty ("BoolProperty"), scope ["ScopeTests.BoolProperty"].PropertyInfo);
|
||||
Assert.True ((bool)scope ["ScopeTests.BoolProperty"].PropertyValue!);
|
||||
Assert.True (scope ["ScopeTests.BoolProperty"].HasValue);
|
||||
|
||||
Assert.Equal (typeof (ScopeTests).GetProperty ("StringProperty"), scope ["ScopeTests.StringProperty"].PropertyInfo);
|
||||
Assert.Null (scope ["ScopeTests.StringProperty"].PropertyValue);
|
||||
Assert.True (scope ["ScopeTests.StringProperty"].HasValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateFrom_Unknown_Key_Adds ()
|
||||
{
|
||||
// Arrange
|
||||
var scope = new ScopeTestsScope ();
|
||||
scope.LoadHardCodedDefaults ();
|
||||
var scopeWithAddedProperty = new ScopeTestsScope ();
|
||||
scopeWithAddedProperty.TryAdd ("AddedProperty", new ConfigProperty ()
|
||||
{
|
||||
Immutable = false,
|
||||
PropertyInfo = scope ["ScopeTests.BoolProperty"].PropertyInfo, // cheat and reuse the same PropertyInfo
|
||||
PropertyValue = false
|
||||
});
|
||||
|
||||
// Act
|
||||
scope.UpdateFrom (scopeWithAddedProperty);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (scope);
|
||||
Assert.NotEmpty (scope);
|
||||
Assert.True (scope.ContainsKey ("AddedProperty"));
|
||||
Assert.Equal (false, scopeWithAddedProperty ["AddedProperty"].PropertyValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateFrom_HasValue_Updates ()
|
||||
{
|
||||
// Arrange
|
||||
ScopeTestsScope originalScope = new ScopeTestsScope ();
|
||||
originalScope.LoadHardCodedDefaults ();
|
||||
Assert.Equal (Key.A, originalScope ["ScopeTests.KeyProperty"].PropertyValue);
|
||||
|
||||
ScopeTestsScope sourceScope = new ScopeTestsScope ();
|
||||
sourceScope.TryAdd ("ScopeTests.KeyProperty", new ConfigProperty
|
||||
{
|
||||
Immutable = false,
|
||||
PropertyInfo = originalScope ["ScopeTests.KeyProperty"].PropertyInfo,
|
||||
});
|
||||
sourceScope ["ScopeTests.KeyProperty"].PropertyValue = Key.B;
|
||||
|
||||
// StringProperty is set to null
|
||||
Assert.DoesNotContain ("ScopeTests.StringProperty", sourceScope);
|
||||
|
||||
Assert.True (sourceScope ["ScopeTests.KeyProperty"].HasValue);
|
||||
|
||||
// Act
|
||||
originalScope.UpdateFrom (sourceScope);
|
||||
|
||||
// Assert
|
||||
Assert.True (originalScope ["ScopeTests.StringProperty"].HasValue);
|
||||
Assert.True (originalScope ["ScopeTests.KeyProperty"].HasValue);
|
||||
Assert.Equal (Key.B, originalScope ["ScopeTests.KeyProperty"].PropertyValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateFrom_HasValue_Dictionary_Updates ()
|
||||
{
|
||||
// Arrange
|
||||
ScopeTestsScope originalScope = new ScopeTestsScope ();
|
||||
originalScope.LoadHardCodedDefaults ();
|
||||
Assert.Null (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue);
|
||||
|
||||
// QUESTION: Should this be done automatically?
|
||||
originalScope ["ScopeTests.DictionaryProperty"].PropertyValue = new Dictionary<string, ConfigProperty> ();
|
||||
|
||||
ScopeTestsScope sourceScope = new ScopeTestsScope ();
|
||||
sourceScope.LoadHardCodedDefaults ();
|
||||
sourceScope ["ScopeTests.DictionaryProperty"].PropertyValue = new Dictionary<string, ConfigProperty> ()
|
||||
{
|
||||
{ "item1", ConfigProperty.GetAllConfigProperties () ["ScopeTests.DictionaryItemProperty1"] },
|
||||
{ "item2", ConfigProperty.GetAllConfigProperties () ["ScopeTests.DictionaryItemProperty2"] }
|
||||
};
|
||||
|
||||
Assert.True (sourceScope ["ScopeTests.KeyProperty"].HasValue);
|
||||
Assert.True (sourceScope ["ScopeTests.DictionaryProperty"].HasValue);
|
||||
|
||||
Dictionary<string, ConfigProperty>? sourceDict = sourceScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary<string, ConfigProperty>;
|
||||
sourceDict! ["item1"].Immutable = false;
|
||||
sourceDict ["item2"].Immutable = false;
|
||||
Assert.NotNull (sourceDict);
|
||||
Assert.Equal (2, sourceDict!.Count);
|
||||
Assert.True (sourceDict.ContainsKey ("item1"));
|
||||
Assert.True (sourceDict.ContainsKey ("item2"));
|
||||
Assert.False (sourceDict ["item1"].HasValue);
|
||||
Assert.False (sourceDict ["item1"].Immutable);
|
||||
|
||||
// Update the original scope with the source scope, which has no values
|
||||
originalScope.UpdateFrom (sourceScope);
|
||||
|
||||
// Confirm original is unchanged
|
||||
Assert.NotNull (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue);
|
||||
Dictionary<string, ConfigProperty>? destDict = originalScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary<string, ConfigProperty>;
|
||||
Assert.NotNull (destDict);
|
||||
Assert.Empty (destDict);
|
||||
Assert.False (destDict.ContainsKey ("item1"));
|
||||
Assert.False (destDict.ContainsKey ("item2"));
|
||||
|
||||
// Confirm source is unchanged
|
||||
sourceDict ["item1"].PropertyValue = "hello";
|
||||
Assert.True (sourceDict ["item1"].HasValue);
|
||||
Assert.Equal ("hello", sourceDict ["item1"].PropertyValue);
|
||||
|
||||
// Now update the original scope with the source scope again
|
||||
originalScope.UpdateFrom (sourceScope);
|
||||
|
||||
// Confirm the original has been updated with only the values in source that have been set
|
||||
Assert.NotNull (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue);
|
||||
destDict = originalScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary<string, ConfigProperty>;
|
||||
|
||||
// 1 item (item1) should now be in the original scope
|
||||
Assert.Single (destDict!);
|
||||
Assert.True (destDict! ["item1"].HasValue);
|
||||
Assert.Equal ("hello", destDict ["item1"].PropertyValue);
|
||||
|
||||
originalScope.Apply ();
|
||||
|
||||
// Verify apply worked
|
||||
Assert.Equal ("hello", DictionaryProperty? ["item1"].PropertyValue);
|
||||
|
||||
// The item property should not have had its value set
|
||||
Assert.Null (DictionaryItemProperty1);
|
||||
|
||||
DictionaryItemProperty1 = null;
|
||||
}
|
||||
|
||||
#region Concurrency Testing
|
||||
|
||||
[Fact]
|
||||
public void Scope_Concurrent_Additions ()
|
||||
{
|
||||
// Arrange
|
||||
var scope = new Scope<object> ();
|
||||
int threadCount = 10;
|
||||
int itemsPerThread = 100;
|
||||
var tasks = new List<Task> ();
|
||||
|
||||
// Act
|
||||
for (int t = 0; t < threadCount; t++)
|
||||
{
|
||||
int threadId = t;
|
||||
tasks.Add (Task.Run (() =>
|
||||
{
|
||||
for (int i = 0; i < itemsPerThread; i++)
|
||||
{
|
||||
string key = $"Thread{threadId}_Item{i}";
|
||||
scope.TryAdd (key, new ConfigProperty { PropertyValue = i });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
|
||||
// Assert
|
||||
Assert.Equal (threadCount * itemsPerThread, scope.Count);
|
||||
for (int t = 0; t < threadCount; t++)
|
||||
{
|
||||
for (int i = 0; i < itemsPerThread; i++)
|
||||
{
|
||||
string key = $"Thread{t}_Item{i}";
|
||||
Assert.True (scope.ContainsKey (key));
|
||||
Assert.Equal (i, scope [key].PropertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class SettingsScopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Schema_Is_Correct ()
|
||||
{
|
||||
// Arrange
|
||||
var settingsScope = new SettingsScope ();
|
||||
// Act
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json", settingsScope.Schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Terminal.Gui.Configuration;
|
||||
|
||||
public class SourcesManagerTests
|
||||
{
|
||||
#region Update (Stream)
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNullSettingsScope_ReturnsFalse ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var stream = new MemoryStream ();
|
||||
var source = "test.json";
|
||||
var location = ConfigLocations.AppCurrent;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (null, stream, source, location);
|
||||
|
||||
// Assert
|
||||
Assert.False (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithValidStream_UpdatesSettingsScope ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Application.QuitKey": "Ctrl+Z"
|
||||
}
|
||||
""";
|
||||
var location = ConfigLocations.HardCoded;
|
||||
var source = "stream";
|
||||
|
||||
var stream = new MemoryStream ();
|
||||
var writer = new StreamWriter (stream);
|
||||
writer.Write (json);
|
||||
writer.Flush ();
|
||||
stream.Position = 0;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, stream, source, location);
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
Assert.Equal (Key.Z.WithCtrl, settingsScope ["Application.QuitKey"].PropertyValue as Key);
|
||||
Assert.Contains (source, sourcesManager.Sources.Values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithInvalidJson_AddsJsonError ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
|
||||
var settingsScope = new SettingsScope ();
|
||||
var invalidJson = "{ invalid json }";
|
||||
var stream = new MemoryStream ();
|
||||
var writer = new StreamWriter (stream);
|
||||
writer.Write (invalidJson);
|
||||
writer.Flush ();
|
||||
stream.Position = 0;
|
||||
|
||||
var source = "test.json";
|
||||
var location = ConfigLocations.AppCurrent;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, stream, source, location);
|
||||
|
||||
// Assert
|
||||
Assert.False (result);
|
||||
|
||||
// Assuming AddJsonError logs errors, verify the error was logged (mock or inspect logs if possible).
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update (FilePath)
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNonExistentFile_AddsToSourcesAndReturnsTrue ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
|
||||
var settingsScope = new SettingsScope ();
|
||||
var filePath = "nonexistent.json";
|
||||
var location = ConfigLocations.AppCurrent;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, filePath, location);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
Assert.Contains (filePath, sourcesManager.Sources.Values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithValidFile_UpdatesSettingsScope ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Application.QuitKey": "Ctrl+Z"
|
||||
}
|
||||
""";
|
||||
var source = Path.GetTempFileName ();
|
||||
var location = ConfigLocations.HardCoded;
|
||||
|
||||
File.WriteAllText (source, json);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, source, location);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
Assert.Equal (Key.Z.WithCtrl, settingsScope ["Application.QuitKey"].PropertyValue as Key);
|
||||
Assert.Contains (source, sourcesManager.Sources.Values);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
File.Delete (source);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithIOException_RetriesAndFailsGracefully ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
var filePath = "locked.json";
|
||||
var json = "{\"Application.UseSystemConsole\": true}";
|
||||
File.WriteAllText (filePath, json);
|
||||
|
||||
var location = ConfigLocations.AppCurrent;
|
||||
|
||||
try
|
||||
{
|
||||
using FileStream fileStream = File.Open (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, filePath, location);
|
||||
|
||||
// Assert
|
||||
Assert.False (result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
File.Delete (filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update (Json String)
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNullOrEmptyJson_ReturnsFalse ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
|
||||
var settingsScope = new SettingsScope ();
|
||||
var source = "test.json";
|
||||
var location = ConfigLocations.AppCurrent;
|
||||
|
||||
// Act
|
||||
bool resultWithNull = sourcesManager.Load (settingsScope, json: null, source, location);
|
||||
bool resultWithEmpty = sourcesManager.Load (settingsScope, string.Empty, source, location);
|
||||
|
||||
// Assert
|
||||
Assert.False (resultWithNull);
|
||||
Assert.False (resultWithEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithValidJson_UpdatesSettingsScope ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Application.QuitKey": "Ctrl+Z"
|
||||
}
|
||||
""";
|
||||
var source = "test.json";
|
||||
var location = ConfigLocations.HardCoded;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, json, source, location);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
Assert.Equal (Key.Z.WithCtrl, settingsScope ["Application.QuitKey"].PropertyValue as Key);
|
||||
Assert.Contains (source, sourcesManager.Sources.Values);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load
|
||||
|
||||
[Fact]
|
||||
public void Load_WithNullResourceName_ReturnsFalse ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly ();
|
||||
var location = ConfigLocations.AppResources;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, assembly, null, location);
|
||||
|
||||
// Assert
|
||||
Assert.False (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_WithValidResource_UpdatesSettingsScope ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
var assembly = Assembly.GetAssembly (typeof (ConfigurationManager));
|
||||
var resourceName = "Terminal.Gui.Resources.config.json";
|
||||
var location = ConfigLocations.LibraryResources;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, assembly!, resourceName, location);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
|
||||
// Verify settingsScope is updated as expected
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Runtime_Overrides ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
var assembly = Assembly.GetAssembly (typeof (ConfigurationManager));
|
||||
var resourceName = "Terminal.Gui.Resources.config.json";
|
||||
var location = ConfigLocations.LibraryResources;
|
||||
sourcesManager.Load (settingsScope, assembly!, resourceName, location);
|
||||
|
||||
Assert.Equal (Key.Esc, settingsScope ["Application.QuitKey"].PropertyValue);
|
||||
|
||||
var runtimeJson = """
|
||||
{
|
||||
"Application.QuitKey": "Ctrl+Z"
|
||||
}
|
||||
""";
|
||||
var runtimeSource = "runtime.json";
|
||||
var runtimeLocation = ConfigLocations.Runtime;
|
||||
var runtimeStream = new MemoryStream ();
|
||||
var writer = new StreamWriter (runtimeStream);
|
||||
writer.Write (runtimeJson);
|
||||
writer.Flush ();
|
||||
runtimeStream.Position = 0;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, runtimeStream, runtimeSource, runtimeLocation);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
|
||||
// Verify settingsScope is updated as expected
|
||||
Assert.Equal (Key.Z.WithCtrl, settingsScope ["Application.QuitKey"].PropertyValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToJson and ToStream
|
||||
|
||||
[Fact]
|
||||
public void ToJson_WithValidScope_ReturnsJsonString ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
// Act
|
||||
string json = sourcesManager.ToJson (settingsScope);
|
||||
|
||||
// Assert
|
||||
Assert.Contains ("""Application.QuitKey": "Ctrl+Q""", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToStream_WithValidScope_ReturnsStream ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
settingsScope.LoadHardCodedDefaults ();
|
||||
settingsScope ["Application.QuitKey"].PropertyValue = Key.Q.WithCtrl;
|
||||
|
||||
// Act
|
||||
var stream = sourcesManager.ToStream (settingsScope);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (stream);
|
||||
stream.Position = 0;
|
||||
var reader = new StreamReader (stream);
|
||||
string json = reader.ReadToEnd ();
|
||||
Assert.Contains ("""Application.QuitKey": "Ctrl+Q""", json);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sources Dictionary Tests
|
||||
|
||||
[Fact]
|
||||
public void Sources_Dictionary_IsInitializedEmpty ()
|
||||
{
|
||||
// Arrange & Act
|
||||
var sourcesManager = new SourcesManager ();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (sourcesManager.Sources);
|
||||
Assert.Empty (sourcesManager.Sources);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenCalledMultipleTimes_MaintainsLastSourceForLocation ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
// Act - Update with first source for location
|
||||
var firstSource = "first.json";
|
||||
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+A"}""", firstSource, ConfigLocations.Runtime);
|
||||
|
||||
// Update with second source for same location
|
||||
var secondSource = "second.json";
|
||||
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+B"}""", secondSource, ConfigLocations.Runtime);
|
||||
|
||||
// Assert - Only the last source should be stored for the location
|
||||
Assert.Single (sourcesManager.Sources);
|
||||
Assert.Equal (secondSource, sourcesManager.Sources [ConfigLocations.Runtime]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithDifferentLocations_AddsAllSourcesToCollection ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
ConfigLocations [] locations =
|
||||
[
|
||||
ConfigLocations.LibraryResources,
|
||||
ConfigLocations.Runtime,
|
||||
ConfigLocations.AppCurrent,
|
||||
ConfigLocations.GlobalHome
|
||||
];
|
||||
|
||||
// Act - Update with different sources for different locations
|
||||
foreach (var location in locations)
|
||||
{
|
||||
var source = $"config-{location}.json";
|
||||
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+Z"}""", source, location);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal (locations.Length, sourcesManager.Sources.Count);
|
||||
foreach (var location in locations)
|
||||
{
|
||||
Assert.Contains (location, sourcesManager.Sources.Keys);
|
||||
Assert.Equal ($"config-{location}.json", sourcesManager.Sources [location]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_AddsResourceSourceToCollection ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
var assembly = Assembly.GetAssembly (typeof (ConfigurationManager));
|
||||
var resourceName = "Terminal.Gui.Resources.config.json";
|
||||
var location = ConfigLocations.LibraryResources;
|
||||
|
||||
// Act
|
||||
bool result = sourcesManager.Load (settingsScope, assembly!, resourceName, location);
|
||||
|
||||
// Assert
|
||||
Assert.True (result);
|
||||
Assert.Contains (location, sourcesManager.Sources.Keys);
|
||||
Assert.Equal ($"resource://[{assembly!.GetName ().Name}]/{resourceName}", sourcesManager.Sources [location]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNonExistentFileAndDifferentLocations_TracksAllSources ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
// Define multiple files and locations
|
||||
var fileLocations = new Dictionary<string, ConfigLocations> (StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
{ "file1.json", ConfigLocations.AppCurrent },
|
||||
{ "file2.json", ConfigLocations.GlobalHome },
|
||||
{ "file3.json", ConfigLocations.AppHome }
|
||||
};
|
||||
|
||||
// Act
|
||||
foreach (var pair in fileLocations)
|
||||
{
|
||||
sourcesManager.Load (settingsScope, pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal (fileLocations.Count, sourcesManager.Sources.Count);
|
||||
foreach (var pair in fileLocations)
|
||||
{
|
||||
Assert.Contains (pair.Value, sourcesManager.Sources.Keys);
|
||||
Assert.Equal (pair.Key, sourcesManager.Sources [pair.Value]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sources_IsPreservedAcrossOperations ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
// First operation - file update
|
||||
var filePath = "testfile.json";
|
||||
var location1 = ConfigLocations.AppCurrent;
|
||||
sourcesManager.Load (settingsScope, filePath, location1);
|
||||
|
||||
// Second operation - json string update
|
||||
var jsonSource = "jsonstring";
|
||||
var location2 = ConfigLocations.Runtime;
|
||||
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+Z"}""", jsonSource, location2);
|
||||
|
||||
// Perform a stream operation
|
||||
var streamSource = "streamdata";
|
||||
var location3 = ConfigLocations.GlobalCurrent;
|
||||
var stream = new MemoryStream ();
|
||||
var writer = new StreamWriter (stream);
|
||||
writer.Write ("""{"Application.QuitKey": "Ctrl+Z"}""");
|
||||
writer.Flush ();
|
||||
stream.Position = 0;
|
||||
sourcesManager.Load (settingsScope, stream, streamSource, location3);
|
||||
|
||||
// Assert - all sources should be preserved
|
||||
Assert.Equal (3, sourcesManager.Sources.Count);
|
||||
Assert.Equal (filePath, sourcesManager.Sources [location1]);
|
||||
Assert.Equal (jsonSource, sourcesManager.Sources [location2]);
|
||||
Assert.Equal (streamSource, sourcesManager.Sources [location3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sources_StaysConsistentWhenUpdateFails ()
|
||||
{
|
||||
// Arrange
|
||||
var sourcesManager = new SourcesManager ();
|
||||
var settingsScope = new SettingsScope ();
|
||||
|
||||
// Add one successful source
|
||||
var validSource = "valid.json";
|
||||
var validLocation = ConfigLocations.Runtime;
|
||||
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+Z"}""", validSource, validLocation);
|
||||
|
||||
try
|
||||
{
|
||||
// Configure to throw on errors
|
||||
ConfigurationManager.ThrowOnJsonErrors = true;
|
||||
|
||||
// Act & Assert - attempt to update with invalid JSON
|
||||
var invalidSource = "invalid.json";
|
||||
var invalidLocation = ConfigLocations.AppCurrent;
|
||||
var invalidJson = "{ invalid json }";
|
||||
|
||||
Assert.Throws<JsonException> (
|
||||
() =>
|
||||
sourcesManager.Load (settingsScope, invalidJson, invalidSource, invalidLocation));
|
||||
|
||||
// The valid source should still be there
|
||||
Assert.Single (sourcesManager.Sources);
|
||||
Assert.Equal (validSource, sourcesManager.Sources [validLocation]);
|
||||
|
||||
// The invalid source should not have been added
|
||||
Assert.DoesNotContain (invalidLocation, sourcesManager.Sources.Keys);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Reset for other tests
|
||||
ConfigurationManager.ThrowOnJsonErrors = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Terminal.Gui.ConfigurationTests;
|
||||
|
||||
public class ThemeScopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Schemes_Property_Exists ()
|
||||
{
|
||||
var scope = new ThemeScope ();
|
||||
scope.LoadHardCodedDefaults();
|
||||
|
||||
Assert.NotEmpty (scope);
|
||||
|
||||
Assert.NotNull(scope ["Schemes"].PropertyValue);
|
||||
|
||||
Assert.NotEmpty (scope);
|
||||
}
|
||||
|
||||
//[Fact]
|
||||
//public void RetrievValues_Gets_Default_Values ()
|
||||
//{
|
||||
// // Need to call Initialize to setup readonly statics
|
||||
// ConfigurationManager.Initialize ();
|
||||
|
||||
// var themeScope = new ThemeScope ();
|
||||
|
||||
// Assert.NotEmpty (themeScope);
|
||||
|
||||
// // Schemes exists, but is not initialized
|
||||
// Assert.Null (themeScope ["Schemes"].PropertyValue);
|
||||
|
||||
// themeScope.RetrieveValues ();
|
||||
|
||||
// Assert.NotEmpty (themeScope);
|
||||
|
||||
// // Schemes exists, and has correct # of eleements
|
||||
// var schemes = themeScope ["Schemes"].PropertyValue as Dictionary<string, Scheme>;
|
||||
// Assert.NotNull (schemes);
|
||||
// Assert.Equal (5, schemes!.Count);
|
||||
|
||||
// // Base has correct values
|
||||
// var baseSchemee = schemes ["Base"];
|
||||
// Assert.Equal (new Attribute(Color.White, Color.Blue), baseSchemee.Normal);
|
||||
|
||||
//}
|
||||
}
|
||||
Reference in New Issue
Block a user