diff --git a/app/Build/Commands/VerifyCommand.cs b/app/Build/Commands/VerifyCommand.cs index a8b37992a..05007f172 100644 --- a/app/Build/Commands/VerifyCommand.cs +++ b/app/Build/Commands/VerifyCommand.cs @@ -94,6 +94,15 @@ public sealed class VerifyCommand var directory = Environment.GetTrimSmokeTestDirectory(); var publishDirectory = Path.Combine(directory, "bin", "verify"); + + // + // Publishing leaves the files of an earlier run in place, among them copies of assemblies which the trimmer + // now removes completely. The dependency manifest still lists such an assembly, so a leftover copy can be + // loaded in its place and hide exactly what the smoke test is there to find: + // + if (Directory.Exists(publishDirectory)) + Directory.Delete(publishDirectory, recursive: true); + var publishExitCode = await CommandRunner.RunAsync(directory, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --output \"{publishDirectory}\" --nologo"); if (publishExitCode is not 0) return publishExitCode; diff --git a/app/TrimSmokeTest/AppJsonOptions.cs b/app/TrimSmokeTest/AppJsonOptions.cs new file mode 100644 index 000000000..b90ab0689 --- /dev/null +++ b/app/TrimSmokeTest/AppJsonOptions.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AIStudio.TrimSmokeTest; + +/// +/// Finds the options with which AI Studio reads and writes JSON. +/// +/// +/// AI Studio keeps each kind of options in a static field or property, next to the code which uses them, so +/// these are the options which reach System.Text.Json. A list written by hand would miss the next new one. +/// The tests of the app compile this file, too, see Tests.csproj, so that both check the same options. +/// +public static class AppJsonOptions +{ + private const BindingFlags STATIC_MEMBERS = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + + /// + /// Returns the options of every static field and property of AI Studio which holds some. + /// + /// + /// Reading a member runs the type initializer of its type. Hence, the static state which the start of + /// the app fills has to be there before, Program.LOGGER_FACTORY above all. + /// + /// The options by the name of their type and member, e.g., SettingsManager.JSON_OPTIONS. + public static IReadOnlyDictionary FindAll() + { + var found = new SortedDictionary(StringComparer.Ordinal); + foreach (var type in typeof(AIStudio.Program).Assembly.GetTypes()) + { + // + // A context keeps options of its own for its Default instance, which never reach System.Text.Json + // directly. The static members of an open generic type hold no value at all: + // + if (type.ContainsGenericParameters || type.IsSubclassOf(typeof(JsonSerializerContext))) + continue; + + // + // The field behind a static auto-property is found as the property already: + // + foreach (var field in type.GetFields(STATIC_MEMBERS)) + if (field.FieldType == typeof(JsonSerializerOptions) && !field.IsDefined(typeof(CompilerGeneratedAttribute)) && field.GetValue(null) is JsonSerializerOptions options) + found.Add($"{type.Name}.{field.Name}", options); + + foreach (var property in type.GetProperties(STATIC_MEMBERS)) + if (property.PropertyType == typeof(JsonSerializerOptions) && property.GetMethod is not null && property.GetValue(null) is JsonSerializerOptions options) + found.Add($"{type.Name}.{property.Name}", options); + } + + return found; + } +} \ No newline at end of file