From 86ebdfefeb0d33a5fb5dc92775dd872361538699 Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 10 Nov 2026 21:48:26 +0100 Subject: [PATCH 1/2] Adopt .NET 11 syntax and allocation improvements --- .globalconfig | 9 +++++++++ .../Runner.Generator/SpecificationModelBuilder.cs | 14 +++++++------- Source/Runner/Reporting/JsonReportWriter.cs | 2 +- Source/Runner/Scheduling/RunCache.cs | 2 +- Source/Specifications/SpecificationDisplayNames.cs | 7 ++++--- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.globalconfig b/.globalconfig index 6d77e6d..0951082 100644 --- a/.globalconfig +++ b/.globalconfig @@ -59,6 +59,7 @@ dotnet_diagnostic.CS8632.severity = none dotnet_diagnostic.IDE0003.severity = warning dotnet_diagnostic.IDE0005.severity = warning dotnet_diagnostic.IDE0010.severity = none +dotnet_diagnostic.IDE0028.severity = suggestion dotnet_diagnostic.IDE0036.severity = warning dotnet_diagnostic.IDE0040.severity = none dotnet_diagnostic.IDE0040WithoutSuggestion.severity = none @@ -68,6 +69,14 @@ dotnet_diagnostic.IDE0072.severity = none dotnet_diagnostic.IDE0083.severity = none dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.IDE0160.severity = none +dotnet_diagnostic.IDE0300.severity = suggestion +dotnet_diagnostic.IDE0301.severity = suggestion +dotnet_diagnostic.IDE0302.severity = suggestion +dotnet_diagnostic.IDE0303.severity = suggestion +dotnet_diagnostic.IDE0304.severity = suggestion +dotnet_diagnostic.IDE0305.severity = suggestion +dotnet_diagnostic.IDE0306.severity = suggestion +dotnet_diagnostic.IDE0410.severity = suggestion dotnet_diagnostic.IDE1006.severity = warning dotnet_diagnostic.SA1000.severity = none dotnet_diagnostic.SA1009.severity = none diff --git a/Source/Runner.Generator/SpecificationModelBuilder.cs b/Source/Runner.Generator/SpecificationModelBuilder.cs index 935a27b..69b56cf 100644 --- a/Source/Runner.Generator/SpecificationModelBuilder.cs +++ b/Source/Runner.Generator/SpecificationModelBuilder.cs @@ -54,8 +54,8 @@ public static bool ReferencesRunner(Compilation compilation) => } var chain = Chain(type); - var errors = new List(); - var diagnostics = new List(); + List errors = []; + List diagnostics = []; var facts = Facts(chain, compilation, errors, diagnostics); if (facts.Count == 0 && errors.Count == 0) return null; @@ -106,14 +106,14 @@ static bool IsInGenericType(INamedTypeSymbol type) static List TypeNames(INamedTypeSymbol type) { - var names = new List(); + List names = []; for (var current = type; current is not null; current = current.ContainingType) names.Insert(0, current.Name); return names; } static List Chain(INamedTypeSymbol type) { - var chain = new List(); + List chain = []; for (var current = type; current is not null && current.ToDisplayString() != SpecificationType; current = current.BaseType) { chain.Insert(0, current); @@ -146,11 +146,11 @@ static EquatableArray Lifecycle(List chain, Compil static List Facts(List chain, Compilation compilation, List errors, List diagnostics) { - var facts = new List(); - var seen = new HashSet(StringComparer.Ordinal); + List facts = []; + HashSet seen = [with(StringComparer.Ordinal)]; for (var index = chain.Count - 1; index >= 0; index--) { - var own = new List(); + List own = []; foreach (var method in chain[index].GetMembers().OfType()) { var attribute = method.GetAttributes().FirstOrDefault(_ => _.AttributeClass?.ToDisplayString() == FactAttribute); diff --git a/Source/Runner/Reporting/JsonReportWriter.cs b/Source/Runner/Reporting/JsonReportWriter.cs index 044340e..6fb117b 100644 --- a/Source/Runner/Reporting/JsonReportWriter.cs +++ b/Source/Runner/Reporting/JsonReportWriter.cs @@ -37,7 +37,7 @@ public static string Render(ReportTree tree) writer.WriteEndObject(); } - return Encoding.UTF8.GetString(stream.ToArray()) + "\n"; + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length) + "\n"; } static double Milliseconds(TimeSpan duration) => Math.Round(duration.TotalMilliseconds, 3); diff --git a/Source/Runner/Scheduling/RunCache.cs b/Source/Runner/Scheduling/RunCache.cs index 55b5505..01bda7f 100644 --- a/Source/Runner/Scheduling/RunCache.cs +++ b/Source/Runner/Scheduling/RunCache.cs @@ -39,7 +39,7 @@ internal sealed class RunCache /// The cache. public static RunCache Load(string path) { - var entries = new Dictionary(StringComparer.Ordinal); + Dictionary entries = [with(StringComparer.Ordinal)]; try { if (!File.Exists(path)) return new(entries); diff --git a/Source/Specifications/SpecificationDisplayNames.cs b/Source/Specifications/SpecificationDisplayNames.cs index ccc95f0..ee4834e 100644 --- a/Source/Specifications/SpecificationDisplayNames.cs +++ b/Source/Specifications/SpecificationDisplayNames.cs @@ -36,7 +36,7 @@ public static string ForClass(string @namespace, IEnumerable typeNames) { var segments = @namespace.Length == 0 ? [] : @namespace.Split('.'); var start = Array.FindIndex(segments, _ => _.StartsWith("for_", StringComparison.Ordinal)); - var levels = new List(); + List levels = []; if (start >= 0) { levels.Add(Readable(segments[start].Substring(4))); @@ -54,8 +54,9 @@ public static string ForClass(string @namespace, IEnumerable typeNames) /// The readable class name. public static string ForType(Type type) { - var names = new List(); - for (var current = type; current is not null; current = current.DeclaringType) names.Insert(0, current.Name); + List names = []; + for (var current = type; current is not null; current = current.DeclaringType) names.Add(current.Name); + names.Reverse(); return ForClass(type.Namespace ?? string.Empty, names); } From 5b151e4e8f410c1db8fb8f0b507c8f5132262d82 Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 10 Nov 2026 21:48:33 +0100 Subject: [PATCH 2/2] Verify packed Specifications consumers on .NET 10 and .NET 11 --- .github/workflows/build.yml | 76 +++++++++++-------- .../AssertionXml/verify-generation.mjs | 5 +- Integration/Consumers/Directory.Build.props | 3 +- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9b1f20..0d20dd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,42 +146,52 @@ jobs: "test Integration/Consumers/AnalyzersOff/AnalyzersOff.csproj" "run Integration/Consumers/AssertionXml/OpenApi/OpenApi.csproj" ) - for consumer in "${consumers[@]}"; do - dotnet restore "${consumer#* }" - done - for consumer in "${consumers[@]}"; do - dotnet build "${consumer#* }" --configuration Release --no-restore - done - - # Prove the consumers restored the freshly packed packages and not stale ones from the cache. - packages="${NUGET_PACKAGES:-$HOME/.nuget/packages}" - checked=0 - for nupkg in artifacts/packages/*.0.0.0-ci.nupkg; do - name="$(basename "$nupkg" .nupkg)" - id="${name%.0.0.0-ci}" - lower="$(echo "$id" | tr "[:upper:]" "[:lower:]")" - restored="$packages/$lower/0.0.0-ci/$lower.0.0.0-ci.nupkg.sha512" - if [ -f "$restored" ]; then - expected="$(openssl dgst -sha512 -binary "$nupkg" | openssl base64 -A)" - if [ "$(cat "$restored")" != "$expected" ]; then - echo "::error::Restored $id 0.0.0-ci differs from the freshly packed package" - exit 1 + plain_assertions=Integration/Consumers/AssertionXml/PlainAssertions/PlainAssertions.csproj + # Restore separately for each TFM; leave net11.0 assets for the later default-framework checks. + for framework in net10.0 net11.0; do + for consumer in "${consumers[@]}"; do + dotnet restore "${consumer#* }" -p:ConsumerFramework="$framework" + done + dotnet restore "$plain_assertions" -p:ConsumerFramework="$framework" + for consumer in "${consumers[@]}"; do + dotnet build "${consumer#* }" --configuration Release --no-restore -p:ConsumerFramework="$framework" + done + dotnet build "$plain_assertions" --configuration Release --no-restore -p:ConsumerFramework="$framework" + + # Prove the consumers restored the freshly packed packages and not stale ones from the cache. + packages="${NUGET_PACKAGES:-$HOME/.nuget/packages}" + checked=0 + for nupkg in artifacts/packages/*.0.0.0-ci.nupkg; do + name="$(basename "$nupkg" .nupkg)" + id="${name%.0.0.0-ci}" + lower="$(echo "$id" | tr "[:upper:]" "[:lower:]")" + restored="$packages/$lower/0.0.0-ci/$lower.0.0.0-ci.nupkg.sha512" + if [ -f "$restored" ]; then + expected="$(openssl dgst -sha512 -binary "$nupkg" | openssl base64 -A)" + if [ "$(cat "$restored")" != "$expected" ]; then + echo "::error::Restored $id 0.0.0-ci differs from the freshly packed package" + exit 1 + fi + checked=$((checked + 1)) fi - checked=$((checked + 1)) + done + if [ "$checked" -eq 0 ]; then + echo "::error::No freshly packed package was restored by the consumers" + exit 1 fi - done - if [ "$checked" -eq 0 ]; then - echo "::error::No freshly packed package was restored by the consumers" - exit 1 - fi - echo "Verified $checked restored package(s) match artifacts/packages" + echo "Verified $checked restored package(s) match artifacts/packages" - for consumer in "${consumers[@]}"; do - if [ "${consumer%% *}" = "test" ]; then - dotnet test "${consumer#* }" --configuration Release --no-build - else - dotnet run --project "${consumer#* }" --configuration Release --no-build - fi + for consumer in "${consumers[@]}"; do + if [ "${consumer%% *}" = "test" ]; then + dotnet test "${consumer#* }" --configuration Release --no-build -p:ConsumerFramework="$framework" + else + dotnet run --project "${consumer#* }" --configuration Release --no-build -p:ConsumerFramework="$framework" + fi + done + dotnet run --project "$plain_assertions" -c Release --no-build --no-restore -p:ConsumerFramework="$framework" -- Probe.A-B Consumer.AssertionXml.FirstProbe + dotnet run --project "$plain_assertions" -c Release --no-build --no-restore -p:ConsumerFramework="$framework" -- Probe.A_B Consumer.AssertionXml.SecondProbe + ConsumerFramework="$framework" node Integration/Consumers/AssertionXml/verify-generation.mjs First Probe.A-B + ConsumerFramework="$framework" node Integration/Consumers/AssertionXml/verify-generation.mjs Second Probe.A_B done - name: Check isolated pre-lifecycle assertion registration diff --git a/Integration/Consumers/AssertionXml/verify-generation.mjs b/Integration/Consumers/AssertionXml/verify-generation.mjs index fa92732..2217726 100644 --- a/Integration/Consumers/AssertionXml/verify-generation.mjs +++ b/Integration/Consumers/AssertionXml/verify-generation.mjs @@ -6,10 +6,11 @@ import { readFileSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const [project, assemblyName, previousWriteTime] = process.argv.slice(2); +const framework = process.env.ConsumerFramework || 'net11.0'; assert.ok(['First', 'Second'].includes(project), 'Select the First or Second probe project.'); assert.ok(assemblyName, 'An expected assembly name is required.'); const root = fileURLToPath(new URL(`./${project}/`, import.meta.url)); -const sourcePath = `${root}obj/Release/net11.0/CratisSpecificationsAssertionFailures.g.cs`; +const sourcePath = `${root}obj/Release/${framework}/CratisSpecificationsAssertionFailures.g.cs`; const source = readFileSync(sourcePath, 'utf8'); const encodedName = Buffer.from(assemblyName).toString('base64') .replaceAll('+', '_p').replaceAll('/', '_s').replaceAll('=', '_e'); @@ -19,7 +20,7 @@ assert.ok(source.includes(`namespace Cratis.Generated.A${encodedName};`), 'Gener assert.ok(source.includes('[global::System.Runtime.CompilerServices.ModuleInitializer]'), 'Automatic registration must remain.'); assert.ok(source.includes('internal static void Register() => global::Cratis.Specifications.AssertionFailureRegistration.Register();'), 'The generated initializer must call the native assertion registration.'); -const xml = readFileSync(`${root}bin/Release/net11.0/${assemblyName}.xml`, 'utf8'); +const xml = readFileSync(`${root}bin/Release/${framework}/${assemblyName}.xml`, 'utf8'); assert.ok(xml.includes(`name="M:${registrationType}.Register"`), 'XML must document the assembly-specific registration.'); assert.ok(xml.includes('Registers the XUnit assertion failure factory'), 'Registration documentation must remain.'); if (previousWriteTime !== undefined) { diff --git a/Integration/Consumers/Directory.Build.props b/Integration/Consumers/Directory.Build.props index fadeb02..b8b8cd1 100644 --- a/Integration/Consumers/Directory.Build.props +++ b/Integration/Consumers/Directory.Build.props @@ -1,7 +1,8 @@ - net11.0 + net11.0 + $(ConsumerFramework) false enable enable