[AOT] Refactor SettingsLib/SettingsUI for Native AOT compatibility (#42644)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Key Changes:

1. Settings.UI.Library:
- Added SettingsSerializationContext.cs with comprehensive
JsonSerializable attributes for all settings types
- Updated BasePTModuleSettings.ToJsonString() to use AOT-compatible
serialization
- Updated SettingsUtils.GetFile<T>() to use AOT-compatible
deserialization
- Modified all ToString() methods in Properties classes to use
SettingsSerializationContext
- Converted struct fields to properties in SunTimes and
MouseWithoutBordersProperties for serialization compatibility

2. Settings.UI:
- Fixed namespace alias in SourceGenerationContextContext.cs to avoid
conflicts

For any future developers who discover incorrect settings resolution,
please follow up my changes to add your setting type into
JsonSerilizerContext.



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Co-authored-by: Yu Leng <yuleng@microsoft.com>
This commit is contained in:
moooyo
2025-12-02 16:31:02 +08:00
committed by GitHub
parent b075a021df
commit bcd1583bb7
38 changed files with 489 additions and 58 deletions

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CommonLibTest
{
[TestClass]
public class BasePTModuleSettingsSerializationTests
{
/// <summary>
/// Test to verify that all classes derived from BasePTModuleSettings are registered
/// in the SettingsSerializationContext for Native AOT compatibility.
/// </summary>
[TestMethod]
public void AllBasePTModuleSettingsClasses_ShouldBeRegisteredInSerializationContext()
{
// Arrange
var assembly = typeof(BasePTModuleSettings).Assembly;
var settingsClasses = assembly.GetTypes()
.Where(t => typeof(BasePTModuleSettings).IsAssignableFrom(t) && !t.IsAbstract && t != typeof(BasePTModuleSettings))
.OrderBy(t => t.Name)
.ToList();
Assert.IsTrue(settingsClasses.Count > 0, "No BasePTModuleSettings derived classes found. This test may be broken.");
var jsonSerializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = SettingsSerializationContext.Default,
};
var unregisteredTypes = new System.Collections.Generic.List<string>();
// Act & Assert
foreach (var settingsType in settingsClasses)
{
var typeInfo = jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(settingsType, jsonSerializerOptions);
if (typeInfo == null)
{
unregisteredTypes.Add(settingsType.FullName ?? settingsType.Name);
}
}
// Assert
if (unregisteredTypes.Count > 0)
{
var errorMessage = $"The following {unregisteredTypes.Count} settings class(es) are NOT registered in SettingsSerializationContext:\n" +
$"{string.Join("\n", unregisteredTypes.Select(t => $" - {t}"))}\n\n" +
$"Please add [JsonSerializable(typeof(ClassName))] attribute to SettingsSerializationContext.cs for each missing type.";
Assert.Fail(errorMessage);
}
// Print success message with count
Console.WriteLine($"✓ All {settingsClasses.Count} BasePTModuleSettings derived classes are properly registered in SettingsSerializationContext.");
}
/// <summary>
/// Test to verify that calling ToJsonString() on an unregistered type throws InvalidOperationException
/// with a helpful error message.
/// </summary>
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ToJsonString_UnregisteredType_ShouldThrowInvalidOperationException()
{
// Arrange
var unregisteredSettings = new UnregisteredTestSettings
{
Name = "UnregisteredModule",
Version = "1.0.0",
};
// Act - This should throw InvalidOperationException
var jsonString = unregisteredSettings.ToJsonString();
// Assert - Exception should be thrown, so this line should never be reached
Assert.Fail("Expected InvalidOperationException was not thrown.");
}
/// <summary>
/// Test to verify that the error message for unregistered types is helpful and contains
/// necessary information for developers.
/// </summary>
[TestMethod]
public void ToJsonString_UnregisteredType_ShouldHaveHelpfulErrorMessage()
{
// Arrange
var unregisteredSettings = new UnregisteredTestSettings
{
Name = "UnregisteredModule",
Version = "1.0.0",
};
// Act & Assert
try
{
var jsonString = unregisteredSettings.ToJsonString();
Assert.Fail("Expected InvalidOperationException was not thrown.");
}
catch (InvalidOperationException ex)
{
// Verify the error message contains helpful information
Assert.IsTrue(ex.Message.Contains("UnregisteredTestSettings"), "Error message should contain the type name.");
Assert.IsTrue(ex.Message.Contains("SettingsSerializationContext"), "Error message should mention SettingsSerializationContext.");
Assert.IsTrue(ex.Message.Contains("JsonSerializable"), "Error message should mention JsonSerializable attribute.");
Console.WriteLine($"✓ Error message is helpful: {ex.Message}");
}
}
/// <summary>
/// Test class that is intentionally NOT registered in SettingsSerializationContext
/// to verify error handling for unregistered types.
/// </summary>
private sealed class UnregisteredTestSettings : BasePTModuleSettings
{
// Intentionally empty - this class should NOT be registered in SettingsSerializationContext
}
}
}

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests;
namespace Microsoft.PowerToys.Settings.UnitTest
{
@@ -24,5 +26,11 @@ namespace Microsoft.PowerToys.Settings.UnitTest
{
return false;
}
// Override ToJsonString to use test-specific serialization context
public override string ToJsonString()
{
return JsonSerializer.Serialize(this, TestSettingsSerializationContext.Default.BasePTSettingsTest);
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests;
using Microsoft.PowerToys.Settings.UnitTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -22,7 +23,13 @@ namespace CommonLibTest
{
// Arrange
var mockFileSystem = new MockFileSystem();
var settingsUtils = new SettingsUtils(mockFileSystem);
var testSerializerOptions = new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
TypeInfoResolver = TestSettingsSerializationContext.Default,
};
var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions);
string file_name = "\\test";
string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}";
@@ -42,7 +49,13 @@ namespace CommonLibTest
{
// Arrange
var mockFileSystem = new MockFileSystem();
var settingsUtils = new SettingsUtils(mockFileSystem);
var testSerializerOptions = new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
TypeInfoResolver = TestSettingsSerializationContext.Default,
};
var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions);
string file_name = "test\\Test Folder";
string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}";

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UnitTest;
namespace Microsoft.PowerToys.Settings.UI.UnitTests
{
/// <summary>
/// JSON serialization context for unit tests.
/// This context provides source-generated serialization for test-specific types.
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
[JsonSerializable(typeof(BasePTSettingsTest))]
public partial class TestSettingsSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility;
using Microsoft.PowerToys.Settings.UI.UnitTests.Mocks;
using Microsoft.PowerToys.Settings.UI.ViewModels;
@@ -100,6 +101,22 @@ namespace ViewModelTests
mockFancyZonesSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<FancyZonesSettings>();
}
[TestCleanup]
public void CleanUp()
{
// Reset singleton instances to prevent state pollution between tests
ResetSettingsRepository<GeneralSettings>();
ResetSettingsRepository<FancyZonesSettings>();
}
private void ResetSettingsRepository<T>()
where T : class, ISettingsConfig, new()
{
var repositoryType = typeof(SettingsRepository<T>);
var field = repositoryType.GetField("settingsRepository", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
field?.SetValue(null, null);
}
[TestMethod]
public void IsEnabledShouldDisableModuleWhenSuccessful()
{