From 8409cfbde10681495c44d5f30611567442bc893b Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Tue, 27 Oct 2026 01:05:28 +0100 Subject: [PATCH 01/11] Fix: GitService.GetLatestVersions throws when two tags share a commit Map tags using a loop, to prevent duplicate keys in a dictionary. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docs/tool-commands/version.md | 1 + src/Buildvana.Tool/Services/Git/GitService.cs | 48 ++++++++++++------- .../Services/Git/IGitService.cs | 2 + tests/Buildvana.Tool.Tests/GitServiceTests.cs | 46 ++++++++++++++++++ 5 files changed, 82 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f72372..9a9991b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,7 @@ The patch number restarted from 1, below the 2.0.x versions already published. - `bv release` fails when the remote rejects the [push of its commits](docs/tool-commands/release.md#the-release-tag), where it used to go on as if the push had succeeded. - `bv release` reports a failed fetch or [push](docs/tool-commands/release.md#the-release-tag) as one error line naming the release remote, not as an unhandled exception. - When a `PackageVersion` item versions a package Buildvana SDK injects, the reference [is an ordinary `PackageReference`](docs/sdk-modules/standard-analyzers.md#package-references), where NuGet rejected the item with NU1009. +- `bv` no longer fails with `ArgumentException` when two [version tags](docs/tool-commands/version.md#bv-version-show) name the same commit. ### Known problems introduced by this release diff --git a/docs/tool-commands/version.md b/docs/tool-commands/version.md index 7292da45..e5ade89c 100644 --- a/docs/tool-commands/version.md +++ b/docs/tool-commands/version.md @@ -41,6 +41,7 @@ Current branch: main - The latest version is the version of the first tag, walking the history from `HEAD`, whose name is a semantic version. - The latest stable version is the version of the first such tag with no prerelease part. Both lines read `(none)` when there is no such tag. +- When one commit carries several such tags, the highest version among them counts. - The public release line says whether the current branch [produces public releases](../sdk-modules/versioning.md#public-releases). - The prerelease line says whether `VERSION` marks a prerelease line. - The current branch is the short name of the branch, or `(detached HEAD)`. diff --git a/src/Buildvana.Tool/Services/Git/GitService.cs b/src/Buildvana.Tool/Services/Git/GitService.cs index 08b63bd9..d865b1ee 100644 --- a/src/Buildvana.Tool/Services/Git/GitService.cs +++ b/src/Buildvana.Tool/Services/Git/GitService.cs @@ -99,33 +99,26 @@ public bool TagExists(string tag) /// public (SemanticVersion? Latest, SemanticVersion? LatestStable) GetLatestVersions() { - var versions = _repository.Tags - .Select(x => SemanticVersion.TryParse(x.FriendlyName, out var version) - ? (x.Target.Sha, Version: version) - : (Sha: null!, Version: null!)) - .Where(x => x.Sha is not null) - .ToDictionary(); - + var versionsByCommit = GetVersionsByCommit(); SemanticVersion? latest = null; - SemanticVersion? latestStable = null; foreach (var commit in _repository.Head.Commits) { - if (!versions.TryGetValue(commit.Sha, out var version)) + if (!versionsByCommit.TryGetValue(commit.Sha, out var versions)) { continue; } - latest ??= version; - if (version.IsPrerelease) + // A commit can carry several version tags. Its highest version and its highest stable version are the + // ones that count. + latest ??= versions.Max(); + var latestStable = versions.Where(static version => !version.IsPrerelease).Max(); + if (latestStable is not null) { - continue; + return (latest, latestStable); } - - latestStable = version; - break; } - return (latest, latestStable); + return (latest, null); } /// @@ -314,6 +307,29 @@ public void FetchTags(string remote) return signature is null ? null : new(signature.Name, signature.Email); } + private Dictionary> GetVersionsByCommit() + { + var versionsByCommit = new Dictionary>(StringComparer.Ordinal); + foreach (var tag in _repository.Tags) + { + if (!SemanticVersion.TryParse(tag.FriendlyName, out var version)) + { + continue; + } + + var sha = tag.Target.Sha; + if (!versionsByCommit.TryGetValue(sha, out var versions)) + { + versions = []; + versionsByCommit.Add(sha, versions); + } + + versions.Add(version); + } + + return versionsByCommit; + } + private void Push(string remote, string refSpec) { using var gitRemote = GetRemote(remote); diff --git a/src/Buildvana.Tool/Services/Git/IGitService.cs b/src/Buildvana.Tool/Services/Git/IGitService.cs index 3880b2df..48e627cc 100644 --- a/src/Buildvana.Tool/Services/Git/IGitService.cs +++ b/src/Buildvana.Tool/Services/Git/IGitService.cs @@ -67,6 +67,8 @@ internal interface IGitService /// /// A tuple of the latest version and the latest stable version. /// + /// When a commit carries several version tags, the highest one is the version of the commit, and the + /// highest stable one is its stable version. /// If no version tag is found in commit history, this method returns a tuple of two s. /// If no stable version tag is found in commit history, this method returns a tuple of the latest /// version and . diff --git a/tests/Buildvana.Tool.Tests/GitServiceTests.cs b/tests/Buildvana.Tool.Tests/GitServiceTests.cs index 74bd0342..21c2e8ff 100644 --- a/tests/Buildvana.Tool.Tests/GitServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/GitServiceTests.cs @@ -7,6 +7,7 @@ using Buildvana.Core.Testing; using Buildvana.Tool.Infrastructure; using Buildvana.Tool.Services.Git; +using NuGet.Versioning; internal sealed class GitServiceTests { @@ -147,6 +148,51 @@ public async Task Constructor_WithoutRemotes_Succeeds() await Assert.That(git.Remotes).IsEmpty(); } + [Test] + public async Task GetLatestVersions_WalksHistory_UntilAStableVersion() + { + using var repo = CreateRepoWithCommit(); + repo.CreateTag("1.0.0"); + repo.WriteFile("a.txt", "changed"); + repo.CommitAll(); + repo.CreateTag("1.1.0-preview"); + repo.CreateTag("not-a-version"); + using var git = CreateGitService(repo); + + var (latest, latestStable) = git.GetLatestVersions(); + + await Assert.That(latest).IsEqualTo(SemanticVersion.Parse("1.1.0-preview")); + await Assert.That(latestStable).IsEqualTo(SemanticVersion.Parse("1.0.0")); + } + + // One commit carries a prerelease tag and a stable tag. Each one counts for its own result. + [Test] + public async Task GetLatestVersions_WithTwoVersionTagsOnOneCommit_ReadsBoth() + { + using var repo = CreateRepoWithCommit(); + repo.CreateTag("2.0.0-preview"); + repo.CreateTag("1.5.0"); + using var git = CreateGitService(repo); + + var (latest, latestStable) = git.GetLatestVersions(); + + await Assert.That(latest).IsEqualTo(SemanticVersion.Parse("2.0.0-preview")); + await Assert.That(latestStable).IsEqualTo(SemanticVersion.Parse("1.5.0")); + } + + [Test] + public async Task GetLatestVersions_WithoutVersionTags_ReturnsNulls() + { + using var repo = CreateRepoWithCommit(); + repo.CreateTag("not-a-version"); + using var git = CreateGitService(repo); + + var (latest, latestStable) = git.GetLatestVersions(); + + await Assert.That(latest).IsNull(); + await Assert.That(latestStable).IsNull(); + } + [Test] public async Task CommitterIdentity_WhenSet_CommitsWithIt_AndWritesNothingToConfiguration() { From 46a64fb4ad0e88e8fb85f4dce928c9bd480ec4d9 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Tue, 27 Oct 2026 09:36:56 +0100 Subject: [PATCH 02/11] Remove null-forgiving operators from Core.Json and Core.Configuration An operator hides the reason a value cannot be null. A pattern or a parameter now carries that reason, and Debug.Assert states it where neither can. Co-Authored-By: Claude Opus 5.5 --- .../BuildvanaJsonConfigExample.cs | 11 +++++++-- .../BuildvanaJsonConfigProvider.cs | 6 ++--- .../JsonHelper-InsertProperty.cs | 7 +++--- src/Buildvana.Core.Json/JsonHelper.cs | 4 +++- ...sonKeyedObjectConverter.ListConverter`1.cs | 24 +++++++++++++------ .../JsonKeyedObjectConverter.cs | 19 +++++++++------ .../JsonSchemaGenerator-KeyedObjects.cs | 2 +- .../Schema/JsonSchemaGenerator.cs | 9 +++---- .../Schema/JsonSchemaValidator.cs | 5 ++-- 9 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/Buildvana.Core.Configuration/BuildvanaJsonConfigExample.cs b/src/Buildvana.Core.Configuration/BuildvanaJsonConfigExample.cs index 2ce8912a..33cf200d 100644 --- a/src/Buildvana.Core.Configuration/BuildvanaJsonConfigExample.cs +++ b/src/Buildvana.Core.Configuration/BuildvanaJsonConfigExample.cs @@ -166,6 +166,7 @@ internal static IReadOnlyList WrapDescription(string description) /// The schema document the pointer is relative to. /// The setting the reference was found at, for the error message. /// The subschema the pointer names. + /// carries no $ref keyword. /// The pointer does not name a subschema. /// /// A document-local pointer is the only kind the exporter emits, for a member type that occurs more @@ -177,7 +178,8 @@ internal static JsonObject ResolveReference(JsonObject schema, JsonObject root, ArgumentNullException.ThrowIfNull(schema); ArgumentNullException.ThrowIfNull(root); - var pointer = schema["$ref"]!.GetValue(); + var pointer = schema["$ref"]?.GetValue() + ?? throw new ArgumentException("The schema carries no \"$ref\" keyword.", nameof(schema)); JsonNode node = root; foreach (var token in pointer.Split('/').Skip(1)) { @@ -310,7 +312,12 @@ private static void WriteValue(StringBuilder text, JsonObject schema, JsonObject private static List<(string Name, JsonObject Schema)> CollectMembers(JsonObject schema) { List<(string Name, JsonObject Schema)> members = []; - foreach (var (name, node) in (JsonObject)schema["properties"]!) + if (schema["properties"] is not JsonObject properties) + { + return members; + } + + foreach (var (name, node) in properties) { if (node is JsonObject member) { diff --git a/src/Buildvana.Core.Configuration/BuildvanaJsonConfigProvider.cs b/src/Buildvana.Core.Configuration/BuildvanaJsonConfigProvider.cs index 8c0dc5b4..2c89fd0b 100644 --- a/src/Buildvana.Core.Configuration/BuildvanaJsonConfigProvider.cs +++ b/src/Buildvana.Core.Configuration/BuildvanaJsonConfigProvider.cs @@ -105,11 +105,11 @@ public static BuildvanaJsonConfig LoadFile(string? path) var node = Parse(json, path); - // Parse throws unless the document is well-formed JSON, and well-formed JSON always makes a map. - Validate(node, sourceMap!, path); + Debug.Assert(sourceMap is not null, "Parse throws unless the document is well-formed JSON, which always makes a map."); + Validate(node, sourceMap, path); // Validation guarantees a non-null object at the root, so deserialization cannot return null here. - return node!.Deserialize(BuildvanaJsonConfigContext.Default.BuildvanaJsonConfig) ?? new BuildvanaJsonConfig(); + return node.Deserialize(BuildvanaJsonConfigContext.Default.BuildvanaJsonConfig) ?? new BuildvanaJsonConfig(); } // The one probe for the configuration file: nothing outside this class asks which file a home directory diff --git a/src/Buildvana.Core.Json/JsonHelper-InsertProperty.cs b/src/Buildvana.Core.Json/JsonHelper-InsertProperty.cs index 3fed62b9..578dce1c 100644 --- a/src/Buildvana.Core.Json/JsonHelper-InsertProperty.cs +++ b/src/Buildvana.Core.Json/JsonHelper-InsertProperty.cs @@ -111,11 +111,12 @@ private static bool TryFindParentObject(ref Utf8JsonReader reader, IReadOnlyList break; case JsonTokenType.StartObject or JsonTokenType.StartArray: - var pushed = pendingProperty is not null; - if (pushed) + var pushed = false; + if (pendingProperty is { } property) { - pathSegments.Add(pendingProperty!); + pathSegments.Add(property); pendingProperty = null; + pushed = true; } containerPushedSegment.Push(pushed); diff --git a/src/Buildvana.Core.Json/JsonHelper.cs b/src/Buildvana.Core.Json/JsonHelper.cs index 9281d295..e28e084c 100644 --- a/src/Buildvana.Core.Json/JsonHelper.cs +++ b/src/Buildvana.Core.Json/JsonHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text.Encodings.Web; using System.Text.Json; @@ -281,7 +282,8 @@ private static void TryRecordStringEdit( string propertyName, List edits) { - var currentValue = reader.GetString()!; + var currentValue = reader.GetString(); + Debug.Assert(currentValue is not null, "GetString returns null only for a null token, and the reader is on a string."); pathSegments.Add(propertyName); var newValue = rewriter(pathSegments, currentValue); pathSegments.RemoveAt(pathSegments.Count - 1); diff --git a/src/Buildvana.Core.Json/JsonKeyedObjectConverter.ListConverter`1.cs b/src/Buildvana.Core.Json/JsonKeyedObjectConverter.ListConverter`1.cs index b13db4ec..fe57d35a 100644 --- a/src/Buildvana.Core.Json/JsonKeyedObjectConverter.ListConverter`1.cs +++ b/src/Buildvana.Core.Json/JsonKeyedObjectConverter.ListConverter`1.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -51,7 +52,8 @@ public override IReadOnlyList Read( var entries = new List<(string Key, JsonElement Value)>(); while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - var key = reader.GetString()!; + var key = reader.GetString(); + Debug.Assert(key is not null, "GetString returns null only for a null token, and the reader is on a property name."); _ = reader.Read(); var document = JsonDocument.ParseValue(ref reader); documents.Add(document); @@ -88,7 +90,9 @@ public override IReadOnlyList Read( buffer.ResetWrittenCount(); elementWriter.Reset(buffer); WriteElementJson(elementWriter, key, value); - result.Add(JsonSerializer.Deserialize(buffer.WrittenSpan, typeInfo)!); + var element = JsonSerializer.Deserialize(buffer.WrittenSpan, typeInfo); + Debug.Assert(element is not null, "WriteElementJson writes an object, and an object never deserializes as null."); + result.Add(element); } return result; @@ -123,11 +127,13 @@ public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSe private void WriteElementJson(Utf8JsonWriter writer, string key, JsonElement value) { + Debug.Assert(_keyJsonName is not null, "GetElementTypeInfo sets the key name."); writer.WriteStartObject(); - writer.WriteString(_keyJsonName!, key); + writer.WriteString(_keyJsonName, key); if (valuePropertyName is not null) { - writer.WritePropertyName(_valueJsonName!); + Debug.Assert(_valueJsonName is not null, "GetElementTypeInfo sets the value name for a value property."); + writer.WritePropertyName(_valueJsonName); value.WriteTo(writer); } else @@ -157,14 +163,17 @@ private void WriteElementJson(Utf8JsonWriter writer, string key, JsonElement val private void WriteElement(Utf8JsonWriter writer, JsonElement element, HashSet writtenKeys) { - if (!element.TryGetProperty(_keyJsonName!, out var keyElement) || keyElement.ValueKind != JsonValueKind.String) + Debug.Assert(_keyJsonName is not null, "GetElementTypeInfo sets the key name."); + var key = element.TryGetProperty(_keyJsonName, out var keyElement) && keyElement.ValueKind == JsonValueKind.String + ? keyElement.GetString() + : null; + if (key is null) { throw new JsonException($"The key property '{_keyJsonName}' must serialize as a non-null string."); } // Utf8JsonWriter validates structure only, so without this check two elements sharing a key would // produce a document this converter's own Read refuses or silently collapses. - var key = keyElement.GetString()!; if (!writtenKeys.Add(key)) { throw new JsonException($"Duplicate key '{key}'."); @@ -192,7 +201,8 @@ private void WriteElement(Utf8JsonWriter writer, JsonElement element, HashSet public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - var elementType = TryGetElementType(typeToConvert)!; - var attribute = elementType.GetCustomAttribute(inherit: false)!; + var keyedElementType = TryGetKeyedElementType(typeToConvert); + Debug.Assert(keyedElementType is not null, "The serializer calls CreateConverter only after CanConvert returns true."); + var (elementType, attribute) = keyedElementType.Value; // The attribute's CLR property names are resolved to JSON names by the converter itself, at first use, // from the element's JsonTypeInfo: the serializer's own metadata is right by construction under any // naming policy, TypeInfoResolver, or contract customization. Recomputing the names here from // reflection would silently disagree the moment a contract modifier renames a property. var converterType = typeof(ListConverter<>).MakeGenericType(elementType); - return (JsonConverter)Activator.CreateInstance(converterType, attribute.KeyPropertyName, attribute.ValuePropertyName)!; + var converter = (JsonConverter?)Activator.CreateInstance(converterType, attribute.KeyPropertyName, attribute.ValuePropertyName); + Debug.Assert(converter is not null, "Activator.CreateInstance returns null only for a nullable value type."); + return converter; } // Also serves JsonSchemaGenerator, which renders a keyed list from the same detection this factory // converts it by, so the parser and the schema cannot drift apart. - internal static Type? TryGetKeyedElementType(Type type) - => TryGetElementType(type) is { } elementType && elementType.IsDefined(typeof(JsonKeyedObjectAttribute), inherit: false) - ? elementType - : null; + internal static (Type ElementType, JsonKeyedObjectAttribute Attribute)? TryGetKeyedElementType(Type type) + => TryGetElementType(type) is { } elementType + && elementType.GetCustomAttribute(inherit: false) is { } attribute + ? (elementType, attribute) + : null; // Resolves the attribute's CLR property names to the JSON names the serializer will actually use, and // refuses the models the converter cannot read: a non-string key property, and key and value resolving to diff --git a/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator-KeyedObjects.cs b/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator-KeyedObjects.cs index a25ee25a..dbf308b0 100644 --- a/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator-KeyedObjects.cs +++ b/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator-KeyedObjects.cs @@ -18,6 +18,7 @@ partial class JsonSchemaGenerator // is synthesized here from the element type's own schema. private static JsonObject CreateKeyedObjectSchema( Type elementType, + JsonKeyedObjectAttribute attribute, JsonSerializerOptions options, NullabilityInfoContext nullabilityContext, HashSet keyedTypesInProgress) @@ -33,7 +34,6 @@ private static JsonObject CreateKeyedObjectSchema( try { - var attribute = elementType.GetCustomAttribute(inherit: false)!; var (keyJsonName, valueJsonName) = JsonKeyedObjectConverter.ResolveKeyedNames( options.GetTypeInfo(elementType), attribute.KeyPropertyName, diff --git a/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator.cs b/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator.cs index 40437f1a..bc402a62 100644 --- a/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator.cs +++ b/src/Buildvana.Core.Json/Schema/JsonSchemaGenerator.cs @@ -157,10 +157,11 @@ private static JsonNode TransformSchemaNode( var keyedElementType = context.TypeInfo.Kind is JsonTypeInfoKind.None ? JsonKeyedObjectConverter.TryGetKeyedElementType(context.TypeInfo.Type) : null; - if (keyedElementType is not null) + if (keyedElementType is var (elementType, attribute)) { var keyedSchema = CreateKeyedObjectSchema( - keyedElementType, + elementType, + attribute, context.TypeInfo.Options, nullabilityContext, keyedTypesInProgress); @@ -477,9 +478,9 @@ private static void RemoveNullFromType(JsonObject schema) } } - if (typeArray.Count == 1) + if (typeArray is [{ } single]) { - schema["type"] = typeArray[0]!.GetValue(); + schema["type"] = single.GetValue(); } } diff --git a/src/Buildvana.Core.Json/Schema/JsonSchemaValidator.cs b/src/Buildvana.Core.Json/Schema/JsonSchemaValidator.cs index e4497074..8925d315 100644 --- a/src/Buildvana.Core.Json/Schema/JsonSchemaValidator.cs +++ b/src/Buildvana.Core.Json/Schema/JsonSchemaValidator.cs @@ -447,14 +447,13 @@ private static bool TypeMatches(JsonNode? instance, string type) "array" => kind is JsonValueKind.Array, "string" => kind is JsonValueKind.String, "number" => kind is JsonValueKind.Number, - "integer" => kind is JsonValueKind.Number && IsIntegral(instance!), + "integer" => kind is JsonValueKind.Number && instance is JsonValue value && IsIntegral(value), _ => true, // Unknown type keyword: do not fail on a constraint we do not understand. }; } - private static bool IsIntegral(JsonNode instance) + private static bool IsIntegral(JsonValue value) { - var value = instance.AsValue(); if (value.TryGetValue(out _)) { return true; From e886f82d4a46ac8d3326580d163cf4bc75f78ba1 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Tue, 27 Oct 2026 22:03:22 +0100 Subject: [PATCH 03/11] Remove null-forgiving operators from bv deps and AppDirectiveEditor An operator hides the reason a value cannot be null. PinMove carries the non-null target of a moving pin, AppDirectiveEditor.RewriteVersions passes the version text to its callback, and Debug.Assert states what OverrideSelector guarantees. Co-Authored-By: Claude Opus 5.5 --- .../Services/Dependencies/OrphanDetector.cs | 7 ++--- .../Dependencies/OverrideLifecycle.cs | 7 +++-- .../Services/Dependencies/OverrideProject.cs | 11 +++---- .../Services/Dependencies/PackagePinDumps.cs | 31 +++++++++++++++++++ .../Services/Dependencies/PackagePinReader.cs | 9 +++--- .../Services/Dependencies/PackagePinWriter.cs | 18 ++++++++--- .../Services/Dependencies/PinMove.cs | 13 ++++++++ .../Services/Dependencies/PinWriting.cs | 18 ++++++++--- .../Services/Dependencies/SdkPinWriter.cs | 6 ++-- .../Services/Dependencies/SidecarReader.cs | 29 ++++++++++------- .../Services/Dependencies/SidecarWriter.cs | 6 +++- .../Services/Dependencies/ToolPinUpdater.cs | 6 ++-- .../Hooks/PostUpdateHookArgsFactory.cs | 16 ++++++++-- .../Services/SelfUpdate/FamilyPinUpdater.cs | 5 +-- .../Utilities/AppDirectiveEditor.cs | 11 ++++--- .../AppDirectiveEditorTests.cs | 18 +++++------ 16 files changed, 148 insertions(+), 63 deletions(-) create mode 100644 src/Buildvana.Tool/Services/Dependencies/PackagePinDumps.cs create mode 100644 src/Buildvana.Tool/Services/Dependencies/PinMove.cs diff --git a/src/Buildvana.Tool/Services/Dependencies/OrphanDetector.cs b/src/Buildvana.Tool/Services/Dependencies/OrphanDetector.cs index 698150c8..00f5558d 100644 --- a/src/Buildvana.Tool/Services/Dependencies/OrphanDetector.cs +++ b/src/Buildvana.Tool/Services/Dependencies/OrphanDetector.cs @@ -123,10 +123,9 @@ private static bool IsCentralPin(DependencyPin pin) IReadOnlyList evaluations) { return [ - .. evaluations - .Where(static dump => !string.IsNullOrEmpty(dump.ProjectAssetsFile)) - .GroupBy(static dump => dump.ProjectFullPath, StringComparer.OrdinalIgnoreCase) - .Select(static grouped => (grouped.Key, grouped.First().ProjectAssetsFile!)), + .. PackagePinDumps.WithAssetsFile(evaluations) + .GroupBy(static dump => dump.Evaluation.ProjectFullPath, StringComparer.OrdinalIgnoreCase) + .Select(static grouped => (grouped.Key, grouped.First().AssetsFilePath)), ]; } diff --git a/src/Buildvana.Tool/Services/Dependencies/OverrideLifecycle.cs b/src/Buildvana.Tool/Services/Dependencies/OverrideLifecycle.cs index 23911124..c5df3d59 100644 --- a/src/Buildvana.Tool/Services/Dependencies/OverrideLifecycle.cs +++ b/src/Buildvana.Tool/Services/Dependencies/OverrideLifecycle.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -177,7 +178,8 @@ private void Accept( // A project using central package management takes its version from the central file, so that one // version answers for every project that needs it. One managing its own versions takes it inline. case OverrideOutcome.Override when project.ManagesVersionsCentrally: - state.AddCentral(packageId, decision.Version!); + Debug.Assert(decision.Version is not null, "OverrideSelector states a version for every override."); + state.AddCentral(packageId, decision.Version); state.AddPromotion(project.ProjectFullPath, packageId, version: null); break; case OverrideOutcome.Override: @@ -187,7 +189,8 @@ private void Accept( state.AddPromotion(project.ProjectFullPath, packageId, version: null); break; default: - verdicts.Add(Verdict(project, packageId, request, decision.Reason!)); + Debug.Assert(decision.Reason is not null, "OverrideSelector states a reason for every decision that writes nothing."); + verdicts.Add(Verdict(project, packageId, request, decision.Reason)); break; } } diff --git a/src/Buildvana.Tool/Services/Dependencies/OverrideProject.cs b/src/Buildvana.Tool/Services/Dependencies/OverrideProject.cs index fcbd8a62..1e8fc637 100644 --- a/src/Buildvana.Tool/Services/Dependencies/OverrideProject.cs +++ b/src/Buildvana.Tool/Services/Dependencies/OverrideProject.cs @@ -64,9 +64,8 @@ public static IReadOnlyList Create( var moved = MovedCentralPins(packages); var gone = RemovedCentralPins(removed); return [ - .. evaluations - .Where(static dump => !string.IsNullOrEmpty(dump.ProjectAssetsFile)) - .GroupBy(static dump => dump.ProjectFullPath, StringComparer.OrdinalIgnoreCase) + .. PackagePinDumps.WithAssetsFile(evaluations) + .GroupBy(static dump => dump.Evaluation.ProjectFullPath, StringComparer.OrdinalIgnoreCase) .Select(grouped => Fold(home, grouped, moved, gone)) .OrderBy(static project => project.ProjectFullPath, StringComparer.Ordinal), ]; @@ -114,14 +113,14 @@ private static Dictionary> RemovedCentralPins(IReadOnlyL // reports more findings and blocks more promotions, and no automatic step should be the bolder reading. private static OverrideProject Fold( IHomeDirectoryProvider home, - IGrouping evaluations, + IGrouping evaluations, Dictionary> moved, Dictionary> gone) { var centralPins = new Dictionary(StringComparer.OrdinalIgnoreCase); var auditLevel = PackageVulnerabilitySeverity.Critical; var managesCentrally = false; - foreach (var evaluation in evaluations) + foreach (var (evaluation, _) in evaluations) { managesCentrally |= evaluation.ManagePackageVersionsCentrally; auditLevel = (PackageVulnerabilitySeverity)Math.Min((int)auditLevel, (int)AuditLevelOf(evaluation)); @@ -130,7 +129,7 @@ private static OverrideProject Fold( return new OverrideProject { ProjectFullPath = evaluations.Key, - AssetsFilePath = evaluations.First().ProjectAssetsFile!, + AssetsFilePath = evaluations.First().AssetsFilePath, AuditLevel = auditLevel, ManagesVersionsCentrally = managesCentrally, CentralPins = managesCentrally ? centralPins : new Dictionary(StringComparer.OrdinalIgnoreCase), diff --git a/src/Buildvana.Tool/Services/Dependencies/PackagePinDumps.cs b/src/Buildvana.Tool/Services/Dependencies/PackagePinDumps.cs new file mode 100644 index 00000000..6bcefd12 --- /dev/null +++ b/src/Buildvana.Tool/Services/Dependencies/PackagePinDumps.cs @@ -0,0 +1,31 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Buildvana.Core.Dependencies; + +namespace Buildvana.Tool.Services.Dependencies; + +/// +/// Reads the evaluations a pin dump answered with. +/// +internal static class PackagePinDumps +{ + /// + /// Pairs each evaluation that states an assets file with the path of that file. + /// + /// The evaluations the pin dump answered with. + /// The evaluations that state an assets file, each with its path, in the order given. An + /// evaluation that states none is left out: Buildvana SDK never reached its project. + public static IEnumerable<(PackagePinDump Evaluation, string AssetsFilePath)> WithAssetsFile( + IEnumerable evaluations) + { + foreach (var evaluation in evaluations) + { + if (evaluation.ProjectAssetsFile is { Length: > 0 } assetsFilePath) + { + yield return (evaluation, assetsFilePath); + } + } + } +} diff --git a/src/Buildvana.Tool/Services/Dependencies/PackagePinReader.cs b/src/Buildvana.Tool/Services/Dependencies/PackagePinReader.cs index 574300d5..b0960701 100644 --- a/src/Buildvana.Tool/Services/Dependencies/PackagePinReader.cs +++ b/src/Buildvana.Tool/Services/Dependencies/PackagePinReader.cs @@ -107,18 +107,19 @@ private void AddPin( MetadataPolicy = EvaluatedMetadata.Stated(item.UpdatePolicy), }; - pins.Add(key, ClassifyDeclaration(pin, versionOverride is not null)); + pins.Add(key, ClassifyDeclaration(pin, item.ItemType, versionOverride is not null)); } - private DependencyPin ClassifyDeclaration(DependencyPin pin, bool statesVersionOverride) + private DependencyPin ClassifyDeclaration(DependencyPin pin, string itemType, bool statesVersionOverride) { if (statesVersionOverride) { return pin with { Management = PinManagement.VersionOverride }; } - if (pin.Management != PinManagement.Managed - || _declarations.StatesVersion(pin.DeclaringFile, pin.ItemType!, pin.Id, pin.VersionText)) + var isSettled = pin.Management != PinManagement.Managed + || _declarations.StatesVersion(pin.DeclaringFile, itemType, pin.Id, pin.VersionText); + if (isSettled) { return pin; } diff --git a/src/Buildvana.Tool/Services/Dependencies/PackagePinWriter.cs b/src/Buildvana.Tool/Services/Dependencies/PackagePinWriter.cs index f3371150..bc08e714 100644 --- a/src/Buildvana.Tool/Services/Dependencies/PackagePinWriter.cs +++ b/src/Buildvana.Tool/Services/Dependencies/PackagePinWriter.cs @@ -42,7 +42,12 @@ public void Write(IReadOnlyList pins) if (items.Length > 0) { var targets = PinWriting.TargetsOf(items); - var itemTypes = items.Select(static pin => pin.Pin.ItemType!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + var itemTypes = items + .Select(static pin => pin.Pin.ItemType) + .OfType() + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + var rewritten = MsBuildPinEditor.RewritePins( path, itemTypes, @@ -54,11 +59,9 @@ public void Write(IReadOnlyList pins) if (directives.Length > 0) { var targets = PinWriting.TargetsOf(directives); - - // The editor calls back only for directives that carry a version, so VersionText is never null here. var rewritten = AppDirectiveEditor.RewriteVersions( path, - directive => PinWriting.Restate(targets, itemType: null, directive.Id, directive.VersionText!)); + (directive, versionText) => PinWriting.Restate(targets, itemType: null, directive.Id, versionText)); EnsureEdited(rewritten, path, "moves"); } @@ -82,7 +85,12 @@ public void Remove(IReadOnlyList pins) { var path = home.GetFullPath(file.Key); var keys = PinWriting.KeysOf(file); - var itemTypes = file.Select(static pin => pin.ItemType!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + var itemTypes = file + .Select(static pin => pin.ItemType) + .OfType() + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + var removed = MsBuildPinEditor.RemovePins( path, itemTypes, diff --git a/src/Buildvana.Tool/Services/Dependencies/PinMove.cs b/src/Buildvana.Tool/Services/Dependencies/PinMove.cs new file mode 100644 index 00000000..2dd03a72 --- /dev/null +++ b/src/Buildvana.Tool/Services/Dependencies/PinMove.cs @@ -0,0 +1,13 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using NuGet.Versioning; + +namespace Buildvana.Tool.Services.Dependencies; + +/// +/// A pin a run moves, with the version it moves to. +/// +/// The pin, as the repository states it. +/// The version the pin moves to. +internal readonly record struct PinMove(DependencyPin Pin, NuGetVersion Target); diff --git a/src/Buildvana.Tool/Services/Dependencies/PinWriting.cs b/src/Buildvana.Tool/Services/Dependencies/PinWriting.cs index e65636de..ba892930 100644 --- a/src/Buildvana.Tool/Services/Dependencies/PinWriting.cs +++ b/src/Buildvana.Tool/Services/Dependencies/PinWriting.cs @@ -24,21 +24,29 @@ internal static class PinWriting /// Names the pins a run moves. /// /// What the run made of a scope's pins. - /// The pins that move. - public static IEnumerable Moving(IEnumerable pins) - => pins.Where(static pin => pin is { State: PinResolutionState.Updated, Target: not null }); + /// The pins that move, each with the version it moves to. + public static IEnumerable Moving(IEnumerable pins) + { + foreach (var pin in pins) + { + if (pin is { State: PinResolutionState.Updated, Target: { } target }) + { + yield return new PinMove(pin.Pin, target); + } + } + } /// /// Indexes the versions a set of pins moves to, by what identifies each pin in its file. /// /// The pins that move. /// The lookup a rewrite consults for every declaration it walks past. - public static Dictionary TargetsOf(IEnumerable pins) + public static Dictionary TargetsOf(IEnumerable pins) { var targets = new Dictionary(); foreach (var pin in pins) { - targets[KeyOf(pin.Pin.ItemType, pin.Pin.Id, pin.Pin.VersionText)] = pin.Target!; + targets[KeyOf(pin.Pin.ItemType, pin.Pin.Id, pin.Pin.VersionText)] = pin.Target; } return targets; diff --git a/src/Buildvana.Tool/Services/Dependencies/SdkPinWriter.cs b/src/Buildvana.Tool/Services/Dependencies/SdkPinWriter.cs index d3d52da8..197fb745 100644 --- a/src/Buildvana.Tool/Services/Dependencies/SdkPinWriter.cs +++ b/src/Buildvana.Tool/Services/Dependencies/SdkPinWriter.cs @@ -47,7 +47,7 @@ public void Write(IReadOnlyList pins) var targets = PinWriting.TargetsOf(file); var rewritten = AppDirectiveEditor.RewriteVersions( path, - directive => PinWriting.Restate(targets, itemType: null, directive.Id, directive.VersionText!)); + (directive, versionText) => PinWriting.Restate(targets, itemType: null, directive.Id, versionText)); EnsureWritten(rewritten, path); } @@ -61,12 +61,12 @@ public void Write(IReadOnlyList pins) private static void EnsureWritten(bool written, string path) => BuildFailedException.ThrowIfNot(written, $"{path} no longer states a project SDK version this run moves."); - private void WriteGlobalJson(string path, IReadOnlyList pins) + private void WriteGlobalJson(string path, IReadOnlyList pins) { var targets = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var pin in pins) { - targets[pin.Pin.Id] = pin.Target!; + targets[pin.Pin.Id] = pin.Target; } var rewritten = jsonHelper.RewriteStringValues( diff --git a/src/Buildvana.Tool/Services/Dependencies/SidecarReader.cs b/src/Buildvana.Tool/Services/Dependencies/SidecarReader.cs index 4c42ec9f..bdb3fedd 100644 --- a/src/Buildvana.Tool/Services/Dependencies/SidecarReader.cs +++ b/src/Buildvana.Tool/Services/Dependencies/SidecarReader.cs @@ -51,8 +51,21 @@ public IReadOnlyList Read() return entries; } - private static IEnumerable ItemsOf(XDocument document, string itemType) - => document.Descendants().Where(element => string.Equals(element.Name.LocalName, itemType, StringComparison.Ordinal)); + // bv writes both values as attributes, and bv is the only writer these files have. + private static IEnumerable EntriesOf( + XDocument document, + string relativePath, + string itemType) + { + foreach (var element in document.Descendants()) + { + var isItem = string.Equals(element.Name.LocalName, itemType, StringComparison.Ordinal); + if (isItem && (string?)element.Attribute("Include") is { Length: > 0 } id) + { + yield return new TransitiveOverrideEntry(id, (string?)element.Attribute("Version"), relativePath); + } + } + } private void ReadFile(List entries, string relativePath, string itemType) { @@ -62,16 +75,10 @@ private void ReadFile(List entries, string relativePath return; } - // bv writes both values as attributes, and bv is the only writer these files have. - var items = ItemsOf(document, itemType) - .Select(static element => (Id: (string?)element.Attribute("Include"), Version: (string?)element.Attribute("Version"))) - .Where(static item => !string.IsNullOrEmpty(item.Id)) - .OrderBy(static item => item.Id, StringComparer.OrdinalIgnoreCase); + var fileEntries = EntriesOf(document, relativePath, itemType) + .OrderBy(static entry => entry.PackageId, StringComparer.OrdinalIgnoreCase); - foreach (var (id, version) in items) - { - entries.Add(new TransitiveOverrideEntry(id!, version, relativePath)); - } + entries.AddRange(fileEntries); } // A file bv wrote and something else mangled is not worth a failed command: the next apply run rewrites diff --git a/src/Buildvana.Tool/Services/Dependencies/SidecarWriter.cs b/src/Buildvana.Tool/Services/Dependencies/SidecarWriter.cs index 4e8acbdc..d1d44fbd 100644 --- a/src/Buildvana.Tool/Services/Dependencies/SidecarWriter.cs +++ b/src/Buildvana.Tool/Services/Dependencies/SidecarWriter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Security; @@ -80,7 +81,10 @@ DO NOT EDIT. Do not ignore in source control. """; private static string CentralItem(PackageOverride entry) - => $""" """; + { + Debug.Assert(entry.Version is not null, "Every central override states the version it pins."); + return $""" """; + } private static string ProjectItem(PackageOverride entry) { diff --git a/src/Buildvana.Tool/Services/Dependencies/ToolPinUpdater.cs b/src/Buildvana.Tool/Services/Dependencies/ToolPinUpdater.cs index 4f9496df..256f0cec 100644 --- a/src/Buildvana.Tool/Services/Dependencies/ToolPinUpdater.cs +++ b/src/Buildvana.Tool/Services/Dependencies/ToolPinUpdater.cs @@ -41,7 +41,7 @@ public async Task UpdateAsync(IReadOnlyList pins, CancellationTok Guard.IsNotNull(pins); foreach (var pin in PinWriting.Moving(pins)) { - var version = pin.Target!.ToNormalizedString(); + var version = pin.Target.ToNormalizedString(); reporter.Info($"Updating tool {pin.Pin.Id} to {version}..."); _ = await processRunner.RunAsync( DotNetMuxer.Path, @@ -53,8 +53,8 @@ public async Task UpdateAsync(IReadOnlyList pins, CancellationTok // The CLI has a downgrade guard of its own, and it stays armed unless the run is an assisted manual // edit: a policy-driven update never lowers a pin, so a downgrade the CLI blocks there is a bug caught. - private static bool IsDowngrade(PinResolution pin) - => pin.Pin.Version is { } current && VersionComparer.VersionRelease.Compare(pin.Target!, current) < 0; + private static bool IsDowngrade(PinMove pin) + => pin.Pin.Version is { } current && VersionComparer.VersionRelease.Compare(pin.Target, current) < 0; private static string[] ArgumentsFor(DependencyPin pin, string version, bool isDowngrade) => isDowngrade diff --git a/src/Buildvana.Tool/Services/Hooks/PostUpdateHookArgsFactory.cs b/src/Buildvana.Tool/Services/Hooks/PostUpdateHookArgsFactory.cs index 98485bc0..cc7bf22a 100644 --- a/src/Buildvana.Tool/Services/Hooks/PostUpdateHookArgsFactory.cs +++ b/src/Buildvana.Tool/Services/Hooks/PostUpdateHookArgsFactory.cs @@ -47,9 +47,8 @@ public PostUpdateHookArgs Create(DependencyResolution resolution, bool check) Tools = ResultsOf(resolution.Tools), Packages = ResultsOf(resolution.Packages.Where(static pin => pin.Pin.GroupCaption is null)), AdditionalPackages = [ - .. resolution.Packages - .Where(static pin => pin.Pin.GroupCaption is not null) - .GroupBy(static pin => pin.Pin.GroupCaption!) + .. GroupedPins(resolution.Packages) + .GroupBy(static grouped => grouped.Caption, static grouped => grouped.Pin) .Select(static group => new AdditionalPackagesResult { Caption = group.Key, Results = ResultsOf(group) }), ], @@ -59,6 +58,17 @@ .. resolution.Packages }; } + private static IEnumerable<(string Caption, PinResolution Pin)> GroupedPins(IEnumerable pins) + { + foreach (var pin in pins) + { + if (pin.Pin.GroupCaption is { } caption) + { + yield return (caption, pin); + } + } + } + private static IReadOnlyList OverridesOf(IReadOnlyList entries) { return [ diff --git a/src/Buildvana.Tool/Services/SelfUpdate/FamilyPinUpdater.cs b/src/Buildvana.Tool/Services/SelfUpdate/FamilyPinUpdater.cs index 2004da31..7b3e9426 100644 --- a/src/Buildvana.Tool/Services/SelfUpdate/FamilyPinUpdater.cs +++ b/src/Buildvana.Tool/Services/SelfUpdate/FamilyPinUpdater.cs @@ -116,10 +116,11 @@ public IReadOnlyList StampPins(IReadOnlyList pins, NuGetVersi } else { - // The editor calls back only for directives that carry a version, so VersionText is never null here. _ = AppDirectiveEditor.RewriteVersions( path, - directive => BuildvanaFamily.Contains(directive.Id) ? PinVersionText.Restate(directive.VersionText!, target) : null); + (directive, versionText) => BuildvanaFamily.Contains(directive.Id) + ? PinVersionText.Restate(versionText, target) + : null); } } diff --git a/src/Buildvana.Tool/Utilities/AppDirectiveEditor.cs b/src/Buildvana.Tool/Utilities/AppDirectiveEditor.cs index 4b08f4a1..5f702bc9 100644 --- a/src/Buildvana.Tool/Utilities/AppDirectiveEditor.cs +++ b/src/Buildvana.Tool/Utilities/AppDirectiveEditor.cs @@ -45,12 +45,13 @@ public static IReadOnlyList ReadDirectives(string path) /// place. /// /// The path of the file to edit. - /// Called once per directive that carries a version, in document order; - /// returns the new version text for the directive, or to leave it alone. Text - /// ordinally equal to the current version leaves the directive alone too. + /// Called once per directive that carries a version, in document order, + /// with the directive and its version text; returns the new version text for the directive, or + /// to leave it alone. Text ordinally equal to the current version leaves the + /// directive alone too. /// if the file was modified; otherwise, . /// The file is written only when it was modified. - public static bool RewriteVersions(string path, Func getNewVersionText) + public static bool RewriteVersions(string path, Func getNewVersionText) { Guard.IsNotNull(path); Guard.IsNotNull(getNewVersionText); @@ -63,7 +64,7 @@ public static bool RewriteVersions(string path, Func getN continue; } - var newVersionText = getNewVersionText(match.Directive); + var newVersionText = getNewVersionText(match.Directive, versionText); if (newVersionText is not null && !string.Equals(newVersionText, versionText, StringComparison.Ordinal)) { edits.Add(new TextEdit(match.VersionStart, versionText.Length, newVersionText)); diff --git a/tests/Buildvana.Tool.Tests/AppDirectiveEditorTests.cs b/tests/Buildvana.Tool.Tests/AppDirectiveEditorTests.cs index 75060a06..7e768da8 100644 --- a/tests/Buildvana.Tool.Tests/AppDirectiveEditorTests.cs +++ b/tests/Buildvana.Tool.Tests/AppDirectiveEditorTests.cs @@ -189,7 +189,7 @@ public async Task RewriteVersions_SplicesOnlyTheVersionTexts() ["Fake"] = "0.0.1", }; - var changed = AppDirectiveEditor.RewriteVersions(path, d => newVersions.GetValueOrDefault(d.Id)); + var changed = AppDirectiveEditor.RewriteVersions(path, (d, _) => newVersions.GetValueOrDefault(d.Id)); await Assert.That(changed).IsTrue(); await Assert.That(home.ReadFile(FileName)).IsEqualTo(""" @@ -214,15 +214,15 @@ public async Task RewriteVersions_OffersOnlyVersionedDirectives() using System; """; var path = WriteFile(home, content); - var offered = new List(); + var offered = new List<(AppDirective Directive, string VersionText)>(); - var changed = AppDirectiveEditor.RewriteVersions(path, d => { - offered.Add(d); + var changed = AppDirectiveEditor.RewriteVersions(path, (d, versionText) => { + offered.Add((d, versionText)); return null; }); await Assert.That(changed).IsFalse(); - await Assert.That(offered).IsEquivalentTo([new AppDirective(AppDirectiveKind.Package, "Beta", "1.0.0")]); + await Assert.That(offered).IsEquivalentTo([(new AppDirective(AppDirectiveKind.Package, "Beta", "1.0.0"), "1.0.0")]); await Assert.That(home.ReadFile(FileName)).IsEqualTo(content); } @@ -236,7 +236,7 @@ public async Task RewriteVersions_LeavesTheFileAlone_WhenTheNewTextEqualsTheOld( """; var path = WriteFile(home, content); - var changed = AppDirectiveEditor.RewriteVersions(path, _ => "1.0.0"); + var changed = AppDirectiveEditor.RewriteVersions(path, (_, _) => "1.0.0"); await Assert.That(changed).IsFalse(); await Assert.That(home.ReadFile(FileName)).IsEqualTo(content); @@ -253,7 +253,7 @@ public async Task RewriteVersions_PreservesCrOnlyLineEndings() const string expected = "#:package Alpha@1.0.1\rusing System;\r"; await File.WriteAllTextAsync(path, content).ConfigureAwait(false); - var changed = AppDirectiveEditor.RewriteVersions(path, _ => "1.0.1"); + var changed = AppDirectiveEditor.RewriteVersions(path, (_, _) => "1.0.1"); await Assert.That(changed).IsTrue(); var rewritten = await File.ReadAllTextAsync(path).ConfigureAwait(false); @@ -269,7 +269,7 @@ public async Task RewriteVersions_PreservesCrlfLineEndings() const string expected = "// Example hook.\r\n#:package Alpha@1.0.1\r\nusing System;\r\n"; await File.WriteAllTextAsync(path, content).ConfigureAwait(false); - var changed = AppDirectiveEditor.RewriteVersions(path, _ => "1.0.1"); + var changed = AppDirectiveEditor.RewriteVersions(path, (_, _) => "1.0.1"); await Assert.That(changed).IsTrue(); var rewritten = await File.ReadAllTextAsync(path).ConfigureAwait(false); @@ -292,7 +292,7 @@ public async Task RewriteVersions_PreservesTheByteOrderMark(bool hasByteOrderMar var contentBytes = Encoding.UTF8.GetBytes(content); await File.WriteAllBytesAsync(path, hasByteOrderMark ? [0xEF, 0xBB, 0xBF, .. contentBytes] : contentBytes).ConfigureAwait(false); - var changed = AppDirectiveEditor.RewriteVersions(path, _ => "1.0.1"); + var changed = AppDirectiveEditor.RewriteVersions(path, (_, _) => "1.0.1"); await Assert.That(changed).IsTrue(); var rewrittenBytes = await File.ReadAllBytesAsync(path).ConfigureAwait(false); From 180576fa656fa041ee5465bf3e3d341cdd09a446 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 00:12:09 +0100 Subject: [PATCH 04/11] Remove the last null-forgiving operators from src and post-release.cs Where a pattern match can prove the value non-null, the pattern match replaces the operator. Elsewhere, Debug.Assert names the guarantee the code relies on, and checks it in Debug builds. Co-Authored-By: Claude Opus 5.5 --- .buildvana/hooks/release/post-release.cs | 5 +++- .../Gitignore/GitignorePattern-matching.cs | 8 +++--- src/Buildvana.Core.Testing/TempGitRepo.cs | 5 +++- src/Buildvana.Core.Testing/TempHome.cs | 5 +++- .../Internal/ConstantValueParser.cs | 4 +-- .../Tasks/WritePackagePinDump.cs | 5 +++- src/Buildvana.Tool/Build/BuildPipeline.cs | 4 ++- .../Execution/CommandRegistry.cs | 9 +++++-- .../DeclaredApiFilesService.cs | 5 +++- .../Services/Hooks/HookRunner.cs | 5 +++- .../Services/SelfUpdate/SelfVersionService.cs | 26 +++++++++---------- .../Solution/SolutionContextExtensions.cs | 11 +++++++- 12 files changed, 63 insertions(+), 29 deletions(-) diff --git a/.buildvana/hooks/release/post-release.cs b/.buildvana/hooks/release/post-release.cs index 79893e73..a1e557b3 100644 --- a/.buildvana/hooks/release/post-release.cs +++ b/.buildvana/hooks/release/post-release.cs @@ -3,6 +3,7 @@ #:package Buildvana.Runtime +using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; using Buildvana.Runtime; @@ -28,7 +29,9 @@ // next file sits beside the one it replaces, and is checked in, so a missing one is a broken tree: // File.Copy throws, the hook exits non-zero, and the release fails naming the path. Skipping instead // would revert the configuration file at the release after. -var nextConfigFile = Path.Combine(Path.GetDirectoryName(configFile)!, "buildvana.next.jsonc"); +var configDirectory = Path.GetDirectoryName(configFile); +Debug.Assert(configDirectory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); +var nextConfigFile = Path.Combine(configDirectory, "buildvana.next.jsonc"); File.Copy(nextConfigFile, configFile, overwrite: true); // Same expression as SelfVersionService.SchemaUrlRegex in src/Buildvana.Tool, which `bv self-update` applies to diff --git a/src/Buildvana.Core.IO/Gitignore/GitignorePattern-matching.cs b/src/Buildvana.Core.IO/Gitignore/GitignorePattern-matching.cs index 1ee0445c..7a070348 100644 --- a/src/Buildvana.Core.IO/Gitignore/GitignorePattern-matching.cs +++ b/src/Buildvana.Core.IO/Gitignore/GitignorePattern-matching.cs @@ -134,10 +134,10 @@ private static bool MatchTokens( return tokenIndex == tokens.Length; } - private static bool MatchesChar(GitignoreToken token, char c, bool ignoreCase) => token.Kind switch { - GitignoreTokenKind.Literal => CharsEqual(token.Value, c, ignoreCase), - GitignoreTokenKind.AnyChar => true, - GitignoreTokenKind.CharClass => token.CharClass!.Matches(c, ignoreCase), + private static bool MatchesChar(GitignoreToken token, char c, bool ignoreCase) => token switch { + { Kind: GitignoreTokenKind.Literal } => CharsEqual(token.Value, c, ignoreCase), + { Kind: GitignoreTokenKind.AnyChar } => true, + { Kind: GitignoreTokenKind.CharClass, CharClass: { } charClass } => charClass.Matches(c, ignoreCase), _ => false, // AnyRun is consumed by MatchTokens and never reaches here. }; diff --git a/src/Buildvana.Core.Testing/TempGitRepo.cs b/src/Buildvana.Core.Testing/TempGitRepo.cs index 5a467713..835a95ad 100644 --- a/src/Buildvana.Core.Testing/TempGitRepo.cs +++ b/src/Buildvana.Core.Testing/TempGitRepo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -278,7 +279,9 @@ public void LockRemoteReference(string canonicalName, string name = "origin") { ArgumentException.ThrowIfNullOrEmpty(canonicalName); var lockPath = Path.Combine([_remotePaths[name], .. $"{canonicalName}.lock".Split('/')]); - _ = Directory.CreateDirectory(Path.GetDirectoryName(lockPath)!); + var lockDirectory = Path.GetDirectoryName(lockPath); + Debug.Assert(lockDirectory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(lockDirectory); File.WriteAllBytes(lockPath, []); } diff --git a/src/Buildvana.Core.Testing/TempHome.cs b/src/Buildvana.Core.Testing/TempHome.cs index ae0b56b3..28504648 100644 --- a/src/Buildvana.Core.Testing/TempHome.cs +++ b/src/Buildvana.Core.Testing/TempHome.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the project root for full license information. using System; +using System.Diagnostics; using System.IO; using Buildvana.Core.HomeDirectory; using CommunityToolkit.Diagnostics; @@ -47,7 +48,9 @@ public string GetFullPath(string name) public void WriteFile(string name, string content) { var path = GetFullPath(name); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } diff --git a/src/Buildvana.Sdk.SourceGenerators/Internal/ConstantValueParser.cs b/src/Buildvana.Sdk.SourceGenerators/Internal/ConstantValueParser.cs index 55c0050f..cd9b4656 100644 --- a/src/Buildvana.Sdk.SourceGenerators/Internal/ConstantValueParser.cs +++ b/src/Buildvana.Sdk.SourceGenerators/Internal/ConstantValueParser.cs @@ -36,13 +36,13 @@ internal static class ConstantValueParser public static bool TryParse(string? str, out object? result) { - if (string.IsNullOrEmpty(str)) + if (str is null or "") { result = null; return true; } - if (str!.Length > 1 && str[0] == '"' && str[str.Length - 1] == '"') + if (str.Length > 1 && str[0] == '"' && str[str.Length - 1] == '"') { result = str.Substring(1, str.Length - 2).Replace("\"\"", "\""); return true; diff --git a/src/Buildvana.Sdk.Tasks/Tasks/WritePackagePinDump.cs b/src/Buildvana.Sdk.Tasks/Tasks/WritePackagePinDump.cs index 12e52566..b164499d 100644 --- a/src/Buildvana.Sdk.Tasks/Tasks/WritePackagePinDump.cs +++ b/src/Buildvana.Sdk.Tasks/Tasks/WritePackagePinDump.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Security.Cryptography; @@ -124,7 +125,9 @@ private static void Write(string path, string content) // contract (see docs/sdk-diagnostics.md): every message a task issues must carry a BVSDK code. try { - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } catch (Exception e) when (e.IsIORelatedException) diff --git a/src/Buildvana.Tool/Build/BuildPipeline.cs b/src/Buildvana.Tool/Build/BuildPipeline.cs index d118fa3c..7a074247 100644 --- a/src/Buildvana.Tool/Build/BuildPipeline.cs +++ b/src/Buildvana.Tool/Build/BuildPipeline.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -135,7 +136,8 @@ private Task CleanAsync(CancellationToken cancellationToken) foreach (var project in _solution.Model.SolutionProjects) { cancellationToken.ThrowIfCancellationRequested(); - var projectDirectory = Path.GetDirectoryName(_solution.ResolveProjectPath(project))!; + var projectDirectory = Path.GetDirectoryName(_solution.ResolveProjectPath(project)); + Debug.Assert(projectDirectory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); UserDirectory.DeleteIfExists(Path.Combine(projectDirectory, "bin"), _reporter); UserDirectory.DeleteIfExists(Path.Combine(projectDirectory, "obj"), _reporter); } diff --git a/src/Buildvana.Tool/Infrastructure/Execution/CommandRegistry.cs b/src/Buildvana.Tool/Infrastructure/Execution/CommandRegistry.cs index 10ef6df4..aac1f672 100644 --- a/src/Buildvana.Tool/Infrastructure/Execution/CommandRegistry.cs +++ b/src/Buildvana.Tool/Infrastructure/Execution/CommandRegistry.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Reflection; using Buildvana.Core; @@ -304,11 +305,15 @@ private static void LinkAlias(CommandNode root, IReadOnlyList aliasPath, var canonicalNode = root; for (var depth = 0; depth < aliasPath.Count; depth++) { - aliasNode = aliasNode.FindChild(aliasPath[depth])!; + var aliasChild = aliasNode.FindChild(aliasPath[depth]); + Debug.Assert(aliasChild is not null, "AddPath created every node of the alias path."); + aliasNode = aliasChild; // An alias may be shorter than the path it names, as `version` is for `version show`. Its last // node then names the canonical node of the same depth, which for `version` is itself. - canonicalNode = canonicalNode.FindChild(canonicalPath[Math.Min(depth, canonicalPath.Count - 1)])!; + var canonicalChild = canonicalNode.FindChild(canonicalPath[Math.Min(depth, canonicalPath.Count - 1)]); + Debug.Assert(canonicalChild is not null, "AddPath created every node of the canonical path."); + canonicalNode = canonicalChild; if (aliasNode.IsOnCanonicalPath || ReferenceEquals(aliasNode, canonicalNode)) { continue; diff --git a/src/Buildvana.Tool/Services/DeclaredApiFiles/DeclaredApiFilesService.cs b/src/Buildvana.Tool/Services/DeclaredApiFiles/DeclaredApiFilesService.cs index 2f6d2980..7dcfa015 100644 --- a/src/Buildvana.Tool/Services/DeclaredApiFiles/DeclaredApiFilesService.cs +++ b/src/Buildvana.Tool/Services/DeclaredApiFiles/DeclaredApiFilesService.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -167,7 +168,9 @@ private IEnumerable TransferAllApisToShipped(string prefix, string kind) (string UnshippedPath, string ShippedPath)? GetPair(string shippedPath) { - var unshippedPath = Path.Combine(Path.GetDirectoryName(shippedPath)!, unshippedFileName); + var directory = Path.GetDirectoryName(shippedPath); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + var unshippedPath = Path.Combine(directory, unshippedFileName); return UserFile.Exists(unshippedPath) ? (unshippedPath, shippedPath) : null; } } diff --git a/src/Buildvana.Tool/Services/Hooks/HookRunner.cs b/src/Buildvana.Tool/Services/Hooks/HookRunner.cs index c8270c9c..089a0b46 100644 --- a/src/Buildvana.Tool/Services/Hooks/HookRunner.cs +++ b/src/Buildvana.Tool/Services/Hooks/HookRunner.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.IO; using System.Text.Json; using System.Threading; @@ -119,7 +120,9 @@ private async Task RunHookAsync( var json = JsonSerializer.Serialize(args, args.GetType(), BuildvanaJsonContext.Default); _reporter.Trace($"Hook {hookName}: args: {json}"); var argsPath = _home.GetFullPath(WellKnownPaths.GetHookArgsFile(context, @event)); - _ = UserDirectory.CreateDirectory(Path.GetDirectoryName(argsPath)!); + var argsDirectory = Path.GetDirectoryName(argsPath); + Debug.Assert(argsDirectory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = UserDirectory.CreateDirectory(argsDirectory); await UserFile.WriteAllTextAsync(argsPath, json, cancellationToken).ConfigureAwait(false); _reporter.Info($"Running hook {hookName}..."); var result = await _appRunner.RunFileBasedAppAsync( diff --git a/src/Buildvana.Tool/Services/SelfUpdate/SelfVersionService.cs b/src/Buildvana.Tool/Services/SelfUpdate/SelfVersionService.cs index 3adab593..381dac00 100644 --- a/src/Buildvana.Tool/Services/SelfUpdate/SelfVersionService.cs +++ b/src/Buildvana.Tool/Services/SelfUpdate/SelfVersionService.cs @@ -324,7 +324,8 @@ private static void EnsureNoUnforcedDowngrade( // downloads the version in one go — hand-editing the manifest would leave the pin unrestored. The choice // between update and install is keyed on entry presence, mirroring the CLI's own contract: update rewrites // an existing entry, and install adds one. (An entry with an unusable version never reaches this point; - // EnsureUsableManifestEntry rejects it up front.) The manifest itself is created with `dotnet new + // EnsureUsableManifestEntry rejects it up front. An entry is therefore present exactly when it has a version, + // and the code tests the version.) The manifest itself is created with `dotnet new // tool-manifest`, in the home directory, when there is none. Left to `dotnet tool install`, the CLI picks // the directory by its own markers — a `.git` directory anywhere above, then a solution file — which can be // an ancestor of the home directory. --tool-manifest names the file for the same reason. The template writes @@ -340,7 +341,6 @@ private async Task PinToolManifestAsync( { var targetText = target.ToNormalizedString(); var currentPin = manifestPin.Version; - var hasEntry = manifestPin.HasEntry; var isUnchanged = currentPin is not null && VersionComparer.VersionRelease.Equals(currentPin, target); // The dotnet CLI has a downgrade guard of its own: `tool update` refuses to move a tool to a lower @@ -360,12 +360,12 @@ private async Task PinToolManifestAsync( } string[] args = isDowngrade ? ["tool", "update", ToolPackageId, "--version", targetText, "--allow-downgrade"] - : hasEntry ? ["tool", "update", ToolPackageId, "--version", targetText] + : currentPin is not null ? ["tool", "update", ToolPackageId, "--version", targetText] : ["tool", "install", ToolPackageId, "--version", targetText]; await RunDotNetAsync([.. args, "--tool-manifest", ToolManifest.FileName], cancellationToken).ConfigureAwait(false); - return !hasEntry ? $"{ToolPackageId}: {targetText} (tool manifest, added)" + return currentPin is null ? $"{ToolPackageId}: {targetText} (tool manifest, added)" : isUnchanged ? ManifestLineWhenUnchanged(target) - : $"{ToolPackageId}: {currentPin!.ToNormalizedString()} -> {targetText} (tool manifest)"; + : $"{ToolPackageId}: {currentPin.ToNormalizedString()} -> {targetText} (tool manifest)"; } // A handed-off run finds the manifest already at the target, because the bv that handed off moved it @@ -384,11 +384,12 @@ private string ManifestLineWhenUnchanged(NuGetVersion target) return $"{ToolPackageId}: {targetText} (tool manifest, added)"; } - var isMove = NuGetVersion.TryParse(_handedOffFrom, out var previousPin) - && !VersionComparer.VersionRelease.Equals(previousPin, target); - return isMove - ? $"{ToolPackageId}: {previousPin!.ToNormalizedString()} -> {targetText} (tool manifest)" - : $"{ToolPackageId}: {targetText} (tool manifest, unchanged)"; + if (NuGetVersion.TryParse(_handedOffFrom, out var previousPin) && !VersionComparer.VersionRelease.Equals(previousPin, target)) + { + return $"{ToolPackageId}: {previousPin.ToNormalizedString()} -> {targetText} (tool manifest)"; + } + + return $"{ToolPackageId}: {targetText} (tool manifest, unchanged)"; } private async Task RunDotNetAsync(string[] args, CancellationToken cancellationToken) @@ -506,8 +507,7 @@ private void WritePin(string version) } var root = _jsonHelper.LoadObject(path); - var hasSdksSection = root.TryGetPropertyValue(MsbuildSdksPropertyName, out var sdksNode) && sdksNode is JsonObject; - if (!hasSdksSection) + if (!root.TryGetPropertyValue(MsbuildSdksPropertyName, out var sdksNode) || sdksNode is not JsonObject sdks) { var section = new JsonObject { [SdkPackageId] = version }; var inserted = _jsonHelper.InsertProperty(path, [], MsbuildSdksPropertyName, section); @@ -516,7 +516,7 @@ private void WritePin(string version) return; } - if (((JsonObject)sdksNode!).ContainsKey(SdkPackageId)) + if (sdks.ContainsKey(SdkPackageId)) { var rewritten = _jsonHelper.RewriteStringValues(path, (propertyPath, _) => IsPinPath(propertyPath) ? version : null); BuildFailedException.ThrowIfNot(rewritten, $"{path} has a '{MsbuildSdksPropertyName}.{SdkPackageId}' property that is not a string."); diff --git a/src/Buildvana.Tool/Services/Solution/SolutionContextExtensions.cs b/src/Buildvana.Tool/Services/Solution/SolutionContextExtensions.cs index 9405b966..dd38d57d 100644 --- a/src/Buildvana.Tool/Services/Solution/SolutionContextExtensions.cs +++ b/src/Buildvana.Tool/Services/Solution/SolutionContextExtensions.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.IO; using CommunityToolkit.Diagnostics; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -19,7 +20,15 @@ internal static class SolutionContextExtensions /// /// Gets the absolute path of the directory containing the solution file. /// - public string SolutionDirectory => Path.GetDirectoryName(@this.SolutionPath)!; + public string SolutionDirectory + { + get + { + var directory = Path.GetDirectoryName(@this.SolutionPath); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + return directory; + } + } /// /// Resolves a path that is relative to into an absolute path, From 5693443197ffe9ac676122c0cf3627d1d2ac969e Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 00:45:12 +0100 Subject: [PATCH 05/11] Remove the null-forgiving operators after Throws() in tests Chain message checks onto Throws(). Elsewhere, use methods with non-null returns, and Debug.Assert for asynchronous acts. Co-Authored-By: Claude Opus 5.5 --- .../BuildvanaJsonConfigExampleTests.cs | 26 +++--- .../BuildvanaJsonConfigProviderTests.cs | 85 ++++++++----------- .../UserDirectoryTests.cs | 16 ++-- .../Buildvana.Core.IO.Tests/UserFileTests.cs | 16 ++-- .../JsonHelperTests.cs | 21 ++--- .../JsonKeyedObjectConverterTests.cs | 40 ++++----- .../ProcessRunnerTests.cs | 10 +-- .../VersionCalculatorTests.cs | 20 ++--- .../VersionFileTests.cs | 10 +-- .../VersionSpecParseTests.cs | 15 +--- .../VersioningSettingsTests.cs | 15 +--- .../PostReleaseHookArgsTests.cs | 5 +- .../AdditionalGroupPinReaderTests.cs | 4 +- .../AnchoringHomeDirectoryProviderTests.cs | 5 +- tests/Buildvana.Tool.Tests/CIPlatformTests.cs | 10 ++- .../ChangelogUpdaterTests.cs | 14 ++- .../CommandArgumentValidatorTests.cs | 42 ++++----- .../DelegationServiceTests.cs | 11 +-- .../DependenciesUpdateSettingsTests.cs | 42 +++++---- .../DependencyResolverTests.cs | 25 ++++-- .../DependencyScopeSelectionTests.cs | 12 ++- .../EffectivePolicyResolverTests.cs | 4 +- .../ForgejoApiClientTests.cs | 18 ++-- .../Buildvana.Tool.Tests/ForgejoHostTests.cs | 11 +-- tests/Buildvana.Tool.Tests/GitHostTests.cs | 15 ++-- .../GitHubApiClientTests.cs | 12 +-- tests/Buildvana.Tool.Tests/GitHubHostTests.cs | 10 ++- .../GitLabApiClientTests.cs | 18 ++-- tests/Buildvana.Tool.Tests/GitLabHostTests.cs | 5 +- .../GitLabReleasePublisherTests.cs | 18 ++-- tests/Buildvana.Tool.Tests/GitServiceTests.cs | 42 +++++---- .../GiteaApiClientTests.cs | 12 +-- tests/Buildvana.Tool.Tests/GiteaHostTests.cs | 11 +-- tests/Buildvana.Tool.Tests/HookRunnerTests.cs | 7 +- .../OrphanDetectorTests.cs | 4 +- .../OverrideLifecycleTests.cs | 10 ++- .../ProjectAssetsReaderTests.cs | 8 +- .../ReleaseCommandChangelogTests.cs | 8 +- .../ReleaseCommandFailureTests.cs | 21 +++-- .../ReleaseCommandForgejoTests.cs | 14 +-- .../ReleaseCommandGitHubTests.cs | 11 +-- .../ReleaseCommandGitLabTests.cs | 11 ++- .../ReleaseCommandGiteaTests.cs | 14 +-- .../SelfReferenceUpdaterTests.cs | 7 +- .../SelfUpdateCommandTests.cs | 7 +- .../SelfUpdateSettingsTests.cs | 8 +- .../SelfUpdateTargetResolverTests.cs | 14 +-- .../SelfVersionServiceTests.cs | 85 ++++++++++--------- .../ServerReleaseTests.cs | 40 +++++---- .../SolutionPinReaderTests.cs | 7 +- .../ToolPinReaderTests.cs | 14 +-- .../ToolPinUpdaterTests.cs | 4 +- .../UrlMatchingReleaseRemoteResolverTests.cs | 18 ++-- .../UsageExitCodeTests.cs | 4 +- .../VerbosityParserTests.cs | 8 +- 55 files changed, 474 insertions(+), 470 deletions(-) diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs index 06e7e0d5..ec92f404 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs @@ -90,7 +90,7 @@ public async Task Generate_ExampleBesideMembers_Throws() ["properties"] = new JsonObject { ["member"] = new JsonObject { ["default"] = true } }, }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The setting 'section' states an example, and its value is a section: an object with members, " @@ -111,7 +111,7 @@ public async Task Generate_ExampleBesideMemberNames_Throws() ["additionalProperties"] = new JsonObject { ["type"] = "object" }, }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The setting 'groups' states an example, and its value is a section: an object with members, " @@ -138,7 +138,7 @@ public async Task Generate_ExampleOnAReferenceToASection_Throws() }, }; - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The setting 'section' states an example, and its value is a section: an object with members, " @@ -178,7 +178,7 @@ public async Task Generate_KeyedObjectWithoutAKeyExample_Throws() ["additionalProperties"] = new JsonObject { ["type"] = "object" }, }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The keyed object 'groups' states no example member name. Annotate the key property of its " @@ -200,7 +200,7 @@ public async Task Generate_KeyedObjectWithAnEmptyKeySchema_Throws() ["propertyNames"] = new JsonObject(), }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The keyed object 'groups' states no example member name. Annotate the key property of its " @@ -214,7 +214,7 @@ public async Task Generate_KeyedObjectWithoutAValueSchema_Throws() "groups", new JsonObject { ["propertyNames"] = new JsonObject { ["examples"] = new JsonArray { "My group" } } }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo("The keyed object 'groups' declares no value schema."); } @@ -226,7 +226,7 @@ public async Task Generate_SettingWithNoValue_Throws() { var root = SchemaOf("setting", new JsonObject { ["type"] = "string" }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( "The setting 'setting' states neither an example nor a default, and is not a container. " @@ -239,7 +239,7 @@ public async Task Generate_OverlongDescription_Throws() var description = Words(35); var root = SchemaOf("setting", new JsonObject { ["description"] = description, ["default"] = true }); - var problem = await ProblemOf(root).ConfigureAwait(false); + var problem = ProblemOf(root); await Assert.That(problem).IsEqualTo( $"The description \"{description}\" needs 3 comment lines, past the 2 a description gets. A " @@ -366,14 +366,8 @@ private static JsonObject SchemaOf(string name, JsonObject member) => new() { ["properties"] = new JsonObject { [name] = member } }; // The message the generator refuses a schema with. - private static async Task ProblemOf(JsonObject root) - { - string Act() => BuildvanaJsonConfigExample.Generate(root); - - var exception = await Assert.That(Act).Throws(); - - return exception!.Message; - } + private static string ProblemOf(JsonObject root) + => Assert.Throws(() => BuildvanaJsonConfigExample.Generate(root)).Message; // Four-letter words joined by single spaces, so the text is (5 * count - 1) characters long and a greedy // wrap has somewhere to break. diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs index b416b914..8982da7a 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs @@ -159,9 +159,8 @@ public async Task Config_BothFilesPresent_ThrowsNamingBothWithoutDiagnostics() { Write(dir, "buildvana.json", "{}"); Write(dir, "buildvana.jsonc", "{}"); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains(Path.Combine(dir, "buildvana.json")); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Message).Contains(Path.Combine(dir, "buildvana.json")); await Assert.That(exception.Message).Contains(Path.Combine(dir, "buildvana.jsonc")); await Assert.That(exception.Diagnostics.Count).IsEqualTo(0); } @@ -178,9 +177,8 @@ public async Task Config_InvalidJson_ReportsBV1100() try { Write(dir, "buildvana.jsonc", "{ not json "); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1100"); } finally @@ -196,9 +194,8 @@ public async Task Config_SchemaViolation_ReportsCodeAndPosition() try { Write(dir, "buildvana.jsonc", "{\n \"release\": { \"branches\": [\"main\", null] }\n}"); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1101"); await Assert.That(exception.Diagnostics[0].Line).IsEqualTo(2); } @@ -215,9 +212,8 @@ public async Task Config_UnknownProperty_ReportsBV1103() try { Write(dir, "buildvana.jsonc", """{ "bogus": 1 }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1103"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1103"); } finally { @@ -234,9 +230,8 @@ public async Task Config_FeedWithoutApiKeyEnv_ReportsBV1104() try { Write(dir, "buildvana.jsonc", """{ "nuget": { "feeds": { "release": { "source": "https://nuget.example" } } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1104"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1104"); await Assert.That(exception.Diagnostics[0].Message).Contains("apiKeyEnv"); } finally @@ -252,9 +247,8 @@ public async Task Config_IdentityWithoutEmail_ReportsBV1104() try { Write(dir, "buildvana.jsonc", """{ "git": { "identity": { "name": "Buildvana Bot" } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1104"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1104"); await Assert.That(exception.Diagnostics[0].Message).Contains("email"); } finally @@ -273,9 +267,8 @@ public async Task Config_EmptyFeedSource_ReportsBV1106() try { Write(dir, "buildvana.jsonc", """{ "nuget": { "feeds": { "release": { "source": "", "apiKeyEnv": "KEY" } } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1106"); } finally @@ -291,9 +284,8 @@ public async Task Config_WhitespaceIdentityName_ReportsBV1107() try { Write(dir, "buildvana.jsonc", """{ "git": { "identity": { "name": " ", "email": "bot@buildvana.invalid" } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1107"); } finally @@ -394,9 +386,8 @@ public async Task Config_BomPrefixedFile_DoesNotOffsetPositions() try { Write(dir, "buildvana.jsonc", "{\n \"release\": { \"branches\": [42] }\n}", bom: true); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Line).IsEqualTo(2); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Line).IsEqualTo(2); } finally { @@ -464,9 +455,8 @@ public async Task Config_DuplicatePolicyPattern_ReportsBV1108AtTheRepeat() """; Write(dir, "buildvana.jsonc", content); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1108"); await Assert.That(exception.Diagnostics[0].Message).Contains("A*"); await Assert.That(exception.Diagnostics[0].Line).IsEqualTo(6); @@ -489,9 +479,8 @@ public async Task Config_DuplicateEnvironmentVariable_ReportsBV1108() try { Write(dir, "buildvana.jsonc", """{ "dotnet": { "all": { "env": { "A": "1", "A": "2" } } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1108"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1108"); } finally { @@ -508,9 +497,8 @@ public async Task Config_BlankPolicyPattern_ReportsBV1106() try { Write(dir, "buildvana.jsonc", """{ "dependencies": { "policies": { "": "minor" } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1106"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1106"); await Assert.That(exception.Diagnostics[0].Line).IsEqualTo(1); // The name that is blank, not the value four characters past it. @@ -540,9 +528,8 @@ public async Task Config_BlankAdditionalPackagesCaption_ReportsBV1107() """; Write(dir, "buildvana.jsonc", content); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1107"); await Assert.That(exception.Diagnostics[0].Line).IsEqualTo(4); await Assert.That(exception.Diagnostics[0].Column).IsEqualTo(7); @@ -561,9 +548,8 @@ public async Task Config_PackagePolicyWithNetSdkKind_ReportsBV1102() try { Write(dir, "buildvana.jsonc", """{ "dependencies": { "policies": { "Some.Package": "lts" } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1102"); await Assert.That(exception.Diagnostics[0].Message).Contains("\"lts\""); } @@ -581,9 +567,8 @@ public async Task Config_NetSdkScopeWithPackageKind_ReportsBV1102() try { Write(dir, "buildvana.jsonc", """{ "dependencies": { "scopes": { "netsdk": "exact" } } }"""); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(1); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(1); await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1102"); } finally @@ -603,9 +588,8 @@ public async Task Config_AdditionalPackageGroupWithoutItems_ReportsBV1104() """; Write(dir, "buildvana.jsonc", content); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1104"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1104"); await Assert.That(exception.Diagnostics[0].Message).Contains("items"); } finally @@ -633,9 +617,8 @@ public async Task Config_AdditionalPackageGroupRestatingItsCaption_ReportsBV1105 """; Write(dir, "buildvana.jsonc", content); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics[0].Code).IsEqualTo("BV1105"); + var exception = await Assert.That(Catch(dir)).IsNotNull(); + await Assert.That(exception.Diagnostics[0].Code).IsEqualTo("BV1105"); } finally { diff --git a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs index 1aa418a7..91d8b7fd 100644 --- a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs @@ -37,8 +37,8 @@ public async Task CreateDirectory_WithFileAtTargetPath_Fails() { DirectoryInfo Act() => UserDirectory.CreateDirectory(path); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not create directory"); + var exception = Assert.Throws(() => Act()); + await Assert.That(exception.Message).Contains("Could not create directory"); await Assert.That(exception.InnerException).IsTypeOf(); } finally @@ -77,8 +77,8 @@ public async Task Delete_WithNonEmptyDirectory_Fails() { void Act() => UserDirectory.Delete(root); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not delete directory"); + var exception = Assert.Throws(Act); + await Assert.That(exception.Message).Contains("Could not delete directory"); await Assert.That(exception.InnerException).IsTypeOf(); } finally @@ -94,8 +94,8 @@ public async Task Delete_WithMissingDirectory_Fails() void Act() => UserDirectory.Delete(path); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not delete directory"); + var exception = Assert.Throws(Act); + await Assert.That(exception.Message).Contains("Could not delete directory"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -265,8 +265,8 @@ public async Task SetCurrentDirectory_WithMissingDirectory_Fails() void Act() => UserDirectory.SetCurrentDirectory(path); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("the current directory"); + var exception = Assert.Throws(Act); + await Assert.That(exception.Message).Contains("the current directory"); await Assert.That(exception.InnerException).IsTypeOf(); } diff --git a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs index bfc8ed62..a5713449 100644 --- a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text; using Buildvana.Core; using Buildvana.Core.IO; @@ -144,8 +145,8 @@ public async Task ReadAllText_WithMalformedPath_Fails() { static string Act() => UserFile.ReadAllText("a\0b"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not read from"); + var exception = Assert.Throws(() => Act()); + await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -154,8 +155,8 @@ public async Task WriteAllText_WithMalformedPath_Fails() { static void Act() => UserFile.WriteAllText("a\0b", "x"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not write to"); + var exception = Assert.Throws(Act); + await Assert.That(exception.Message).Contains("Could not write to"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -540,15 +541,16 @@ public async Task Exists_WithEmptyPath_ThrowsRaw() private static async Task AssertDenied(Action act, string expectedMessagePrefix) { - var exception = await Assert.That(act).Throws(); - await Assert.That(exception!.Message).Contains(expectedMessagePrefix); + var exception = Assert.Throws(act); + await Assert.That(exception.Message).Contains(expectedMessagePrefix); await Assert.That(exception.InnerException).IsTypeOf(); } private static async Task AssertDeniedAsync(Func act, string expectedMessagePrefix) { var exception = await Assert.That(act).Throws(); - await Assert.That(exception!.Message).Contains(expectedMessagePrefix); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Message).Contains(expectedMessagePrefix); await Assert.That(exception.InnerException).IsTypeOf(); } } diff --git a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs index c8a65b13..2c740a04 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs @@ -18,8 +18,8 @@ public async Task LoadObject_WithDeniedAccess_Fails() { static JsonObject Act() => new JsonHelper().LoadObject(DeniedAccessPath); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not read from"); + var exception = Assert.Throws(() => Act()); + await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -28,8 +28,8 @@ public async Task SaveObject_WithDeniedAccess_Fails() { static void Act() => new JsonHelper().SaveObject(new JsonObject(), DeniedAccessPath); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not write to"); + var exception = Assert.Throws(Act); + await Assert.That(exception.Message).Contains("Could not write to"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -38,8 +38,8 @@ public async Task RewriteStringValues_WithDeniedAccess_Fails() { static bool Act() => new JsonHelper().RewriteStringValues(DeniedAccessPath, (_, _) => null); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not read from"); + var exception = Assert.Throws(() => Act()); + await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -48,8 +48,8 @@ public async Task InsertProperty_WithDeniedAccess_Fails() { static bool Act() => new JsonHelper().InsertProperty(DeniedAccessPath, [], "a", JsonValue.Create("x")); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not read from"); + var exception = Assert.Throws(() => Act()); + await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -314,8 +314,9 @@ public async Task RewriteBooleanValues_OfInvalidJson_Fails() // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning bool Act() => new JsonHelper().RewriteBooleanValues(file.Path, static (_, current) => !current); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("does not contain valid JSON"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("does not contain valid JSON", StringComparison.Ordinal); } [Test] diff --git a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs index a45434c3..a6e29f06 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs @@ -108,8 +108,7 @@ public async Task Read_NonObject_Throws() { static IReadOnlyList Act() => Deserialize("""["a"]"""); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Expected a JSON object"); + await Assert.That(Act).Throws().WithMessageContaining("Expected a JSON object", StringComparison.Ordinal); } [Test] @@ -127,8 +126,7 @@ public async Task Read_DuplicateKey_WhenDisallowed_Throws() var options = CreateOptions(allowDuplicateProperties: false); object? Act() => JsonSerializer.Deserialize>("""{"a":"one","a":"two"}""", options); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Duplicate key 'a'"); + await Assert.That(Act).Throws().WithMessageContaining("Duplicate key 'a'", StringComparison.Ordinal); } [Test] @@ -146,8 +144,7 @@ static string Act() => Serialize([ new() { Pattern = "a", Policy = "two" }, ]); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Duplicate key 'a'"); + await Assert.That(Act).Throws().WithMessageContaining("Duplicate key 'a'", StringComparison.Ordinal); } [Test] @@ -171,8 +168,9 @@ public async Task Read_MembersShape_KeyRestatedInsideValue_Throws() { static IReadOnlyList Act() => Deserialize("""{"G1":{"caption":"X"}}"""); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("must not state the key property 'caption'"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("must not state the key property 'caption'", StringComparison.Ordinal); } [Test] @@ -180,8 +178,7 @@ public async Task Read_MembersShape_NonObjectValue_Throws() { static IReadOnlyList Act() => Deserialize("""{"G1":42}"""); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("must be a JSON object"); + await Assert.That(Act).Throws().WithMessageContaining("must be a JSON object", StringComparison.Ordinal); } [Test] @@ -214,8 +211,7 @@ public async Task Write_NullKey_Throws() { static string Act() => Serialize([new() { Pattern = null!, Policy = "x" }]); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("non-null string"); + await Assert.That(Act).Throws().WithMessageContaining("non-null string", StringComparison.Ordinal); } [Test] @@ -290,8 +286,9 @@ public async Task Read_MissingKeyProperty_Throws() { static object Act() => Deserialize("{}"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("no serializable property 'Nope'"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("no serializable property 'Nope'", StringComparison.Ordinal); } [Test] @@ -299,8 +296,9 @@ public async Task Read_MissingValueProperty_Throws() { static object Act() => Deserialize("{}"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("no serializable property 'Nope'"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("no serializable property 'Nope'", StringComparison.Ordinal); } [Test] @@ -308,8 +306,9 @@ public async Task Read_NonStringKeyProperty_Throws() { static object Act() => Deserialize("{}"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("must be of type string"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("must be of type string", StringComparison.Ordinal); } [Test] @@ -317,8 +316,9 @@ public async Task Read_SameKeyAndValueProperty_Throws() { static object Act() => Deserialize("{}"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("to the same JSON name 'name'"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("to the same JSON name 'name'", StringComparison.Ordinal); } private static JsonSerializerOptions CreateOptions(bool allowDuplicateProperties = true) diff --git a/tests/Buildvana.Core.Process.Tests/ProcessRunnerTests.cs b/tests/Buildvana.Core.Process.Tests/ProcessRunnerTests.cs index 066fd512..c06b2c2f 100644 --- a/tests/Buildvana.Core.Process.Tests/ProcessRunnerTests.cs +++ b/tests/Buildvana.Core.Process.Tests/ProcessRunnerTests.cs @@ -124,8 +124,9 @@ public async Task Run_WhenTheChildFails_StatesThatAnExternalProgramFailed() var exception = await Assert.That(async () => await runner.RunAsync(executable, args).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); await Assert.That(exception.Message).Contains("exited with code 9"); } @@ -134,10 +135,9 @@ public async Task RunWithInheritedStdio_WithMissingExecutable_Fails() { var runner = new ProcessRunner(); - var exception = await Assert.That(() => runner.RunWithInheritedStdioAsync("bv-test-no-such-executable", [])) - .Throws(); - - await Assert.That(exception!.Message).Contains("bv-test-no-such-executable"); + await Assert.That(() => runner.RunWithInheritedStdioAsync("bv-test-no-such-executable", [])) + .Throws() + .WithMessageContaining("bv-test-no-such-executable", StringComparison.Ordinal); } [Test] diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionCalculatorTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionCalculatorTests.cs index 164bf13a..70efcdf1 100644 --- a/tests/Buildvana.Core.Versioning.Tests/VersionCalculatorTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/VersionCalculatorTests.cs @@ -14,9 +14,8 @@ public async Task Calculate_MissingVersionFile_Throws() { using var repo = new TempGitRepo(); repo.CommitAll(); - var exception = CatchCalculate(repo, new BuildvanaConfig()); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("VERSION"); + var exception = await Assert.That(CatchCalculate(repo, new BuildvanaConfig())).IsNotNull(); + await Assert.That(exception.Message).Contains("VERSION"); } [Test] @@ -25,9 +24,8 @@ public async Task Calculate_MalformedVersionFile_Throws() using var repo = new TempGitRepo(); repo.WriteFile("VERSION", "banana\n"); repo.CommitAll(); - var exception = CatchCalculate(repo, new BuildvanaConfig()); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("banana"); + var exception = await Assert.That(CatchCalculate(repo, new BuildvanaConfig())).IsNotNull(); + await Assert.That(exception.Message).Contains("banana"); } [Test] @@ -36,9 +34,8 @@ public async Task Calculate_PrereleaseWithoutTag_Throws() using var repo = new TempGitRepo(); repo.WriteFile("VERSION", "1.0-\n"); repo.CommitAll(); - var exception = CatchCalculate(repo, new BuildvanaConfig()); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("versioning.prereleaseTag"); + var exception = await Assert.That(CatchCalculate(repo, new BuildvanaConfig())).IsNotNull(); + await Assert.That(exception.Message).Contains("versioning.prereleaseTag"); } [Test] @@ -48,9 +45,8 @@ public async Task Calculate_InvalidPrereleaseTag_Throws() repo.WriteFile("VERSION", "1.0-\n"); repo.CommitAll(); var config = new BuildvanaConfig { Versioning = new VersioningConfig { PrereleaseTag = "not valid" } }; - var exception = CatchCalculate(repo, config); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("not valid"); + var exception = await Assert.That(CatchCalculate(repo, config)).IsNotNull(); + await Assert.That(exception.Message).Contains("not valid"); } [Test] diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs index 7c7e36e3..c57e04b5 100644 --- a/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs @@ -11,9 +11,8 @@ internal sealed class VersionFileTests public async Task Load_MissingFile_Throws() { using var home = new TempHome(); - var exception = CatchLoad(home); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("not found"); + var exception = await Assert.That(CatchLoad(home)).IsNotNull(); + await Assert.That(exception.Message).Contains("not found"); } [Test] @@ -21,9 +20,8 @@ public async Task Load_MalformedFile_Throws() { using var home = new TempHome(); home.WriteFile(VersionFile.FileName, "banana\n"); - var exception = CatchLoad(home); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("banana"); + var exception = await Assert.That(CatchLoad(home)).IsNotNull(); + await Assert.That(exception.Message).Contains("banana"); } [Test] diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionSpecParseTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionSpecParseTests.cs index 2388ca4e..f9c95563 100644 --- a/tests/Buildvana.Core.Versioning.Tests/VersionSpecParseTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/VersionSpecParseTests.cs @@ -54,18 +54,9 @@ public async Task Parse_ValidInput_Parses() [Test] public async Task Parse_InvalidInput_ThrowsBuildFailedException() { - BuildFailedException? exception = null; - try - { - _ = VersionSpec.Parse("banana"); - } - catch (BuildFailedException e) - { - exception = e; - } - - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("banana"); + await Assert.That(() => VersionSpec.Parse("banana")) + .Throws() + .WithMessageContaining("banana", StringComparison.Ordinal); } [Test] diff --git a/tests/Buildvana.Core.Versioning.Tests/VersioningSettingsTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersioningSettingsTests.cs index 98830dce..17435a6a 100644 --- a/tests/Buildvana.Core.Versioning.Tests/VersioningSettingsTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/VersioningSettingsTests.cs @@ -72,17 +72,8 @@ public async Task IsPublicReleaseBranch_InvalidPattern_ThrowsBuildFailedExceptio { var config = new BuildvanaConfig { Release = new ReleaseConfig { Branches = ["("] } }; var settings = new VersioningSettings(config); - BuildFailedException? exception = null; - try - { - _ = settings.IsPublicReleaseBranch("main"); - } - catch (BuildFailedException e) - { - exception = e; - } - - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains("release.branches"); + await Assert.That(() => settings.IsPublicReleaseBranch("main")) + .Throws() + .WithMessageContaining("release.branches", StringComparison.Ordinal); } } diff --git a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs index 5abb3f64..acfc9636 100644 --- a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs +++ b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs @@ -126,8 +126,9 @@ public async Task Load_UnreadableArgsFile_Throws() PostReleaseHookArgs Act() => PostReleaseHookArgs.Load(dir); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not read from"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("Could not read from", StringComparison.Ordinal); } finally { diff --git a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs index b4157e28..ce7ff411 100644 --- a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Core.ConsoleOutput; using Buildvana.Core.Process; @@ -146,7 +147,8 @@ public async Task ReadAsync_WhenEvaluationFails_ReportsAFailedStep() var reader = CreateReader(home, runner); var exception = await Assert.That(async () => await reader.ReadAsync().ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.ExitCode).IsEqualTo(3); } private static Task> ReadAsync(TempHome home, IProcessRunner runner) diff --git a/tests/Buildvana.Tool.Tests/AnchoringHomeDirectoryProviderTests.cs b/tests/Buildvana.Tool.Tests/AnchoringHomeDirectoryProviderTests.cs index f03f18ba..422241b7 100644 --- a/tests/Buildvana.Tool.Tests/AnchoringHomeDirectoryProviderTests.cs +++ b/tests/Buildvana.Tool.Tests/AnchoringHomeDirectoryProviderTests.cs @@ -54,8 +54,9 @@ public async Task HomeDirectory_WhenAnchoringFails_PropagatesAndLeavesCurrentDir var provider = new AnchoringHomeDirectoryProvider(new FixedHomeDirectoryProvider(directory.FullName)); directory.Delete(); - var exception = await Assert.That(() => provider.HomeDirectory).Throws(); - await Assert.That(exception!.Message).Contains("the current directory"); + await Assert.That(() => provider.HomeDirectory) + .Throws() + .WithMessageContaining("the current directory", StringComparison.Ordinal); await Assert.That(Directory.GetCurrentDirectory()).IsEqualTo(previousCurrentDirectory); } } diff --git a/tests/Buildvana.Tool.Tests/CIPlatformTests.cs b/tests/Buildvana.Tool.Tests/CIPlatformTests.cs index 330ee7a3..15c66cca 100644 --- a/tests/Buildvana.Tool.Tests/CIPlatformTests.cs +++ b/tests/Buildvana.Tool.Tests/CIPlatformTests.cs @@ -165,8 +165,9 @@ public async Task OpenStepOutputs_WithoutOutputFile_Fails(string marker, string { var platform = Detect((marker, "true"), (outputVariable, path)); - var exception = await Assert.That(platform.OpenStepOutputs).Throws(); - await Assert.That(exception!.Message).Contains(outputVariable); + await Assert.That(platform.OpenStepOutputs) + .Throws() + .WithMessageContaining(outputVariable, StringComparison.Ordinal); } // Forgejo runners before v9.0.0 set GITHUB_OUTPUT and no FORGEJO_OUTPUT. @@ -176,8 +177,9 @@ public async Task OpenStepOutputs_OnForgejoActionsWithGitHubOutputOnly_Fails() using var home = new TempHome(); var platform = Detect(("FORGEJO_ACTIONS", "true"), ("GITHUB_OUTPUT", home.GetFullPath("output.txt"))); - var exception = await Assert.That(platform.OpenStepOutputs).Throws(); - await Assert.That(exception!.Message).Contains("FORGEJO_OUTPUT"); + await Assert.That(platform.OpenStepOutputs) + .Throws() + .WithMessageContaining("FORGEJO_OUTPUT", StringComparison.Ordinal); } [Test] diff --git a/tests/Buildvana.Tool.Tests/ChangelogUpdaterTests.cs b/tests/Buildvana.Tool.Tests/ChangelogUpdaterTests.cs index 4ec5d9a2..1ad188fe 100644 --- a/tests/Buildvana.Tool.Tests/ChangelogUpdaterTests.cs +++ b/tests/Buildvana.Tool.Tests/ChangelogUpdaterTests.cs @@ -68,8 +68,7 @@ public async Task HasUnreleasedChanges_FailsWhenThereAreNoSections() string[] lines = ["# Changelog", string.Empty, "Nothing to see here."]; bool Act() => ChangelogUpdater.HasUnreleasedChanges(lines); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo("CHANGELOG.md contains no sections."); + await Assert.That(Act).Throws().WithMessage("CHANGELOG.md contains no sections.", StringComparison.Ordinal); } [Test] @@ -321,8 +320,7 @@ public async Task PrepareForRelease_FailsWhenThereAreNoSections() string[] lines = ["# Changelog", string.Empty, "Nothing to see here."]; string Act() => PrepareForRelease(lines); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo("CHANGELOG.md contains no sections."); + await Assert.That(Act).Throws().WithMessage("CHANGELOG.md contains no sections.", StringComparison.Ordinal); } // The title is only needed when a new section is actually written: a changelog that fails validation @@ -521,8 +519,9 @@ public async Task FinalizeNewSection_FailsWhenThereIsOnlyOneSection() string Act() => FinalizeNewSection(lines); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo("CHANGELOG.md contains only one section."); + await Assert.That(Act) + .Throws() + .WithMessage("CHANGELOG.md contains only one section.", StringComparison.Ordinal); } [Test] @@ -531,8 +530,7 @@ public async Task FinalizeNewSection_FailsWhenThereAreNoSections() string[] lines = ["# Changelog", string.Empty, "Nothing to see here."]; string Act() => FinalizeNewSection(lines); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo("CHANGELOG.md contains no sections."); + await Assert.That(Act).Throws().WithMessage("CHANGELOG.md contains no sections.", StringComparison.Ordinal); } // Same guarantee as PrepareForRelease: the title is composed only when a heading is actually replaced. diff --git a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs index b9c83ad0..9a1cde97 100644 --- a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs @@ -54,9 +54,9 @@ public async Task SettingsLessCommand_RejectsUnknownOption() { var command = CommandRegistry.Find("clean")!; var parsed = CliArgSplitter.Split(["clean", "--bogus"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unknown option '--bogus' for command 'clean'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) + .Throws() + .WithMessage("Unknown option '--bogus' for command 'clean'.", StringComparison.Ordinal); } [Test] @@ -64,9 +64,9 @@ public async Task SettingsLessCommand_RejectsUnknownOption_ViaAlias() { var command = CommandRegistry.Find("version")!; var parsed = CliArgSplitter.Split(["version", "--bogus"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unknown option '--bogus' for command 'version show'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) + .Throws() + .WithMessage("Unknown option '--bogus' for command 'version show'.", StringComparison.Ordinal); } [Test] @@ -74,9 +74,9 @@ public async Task SettingsLessCommand_ReportsOffendingTokensInCommandLineOrder() { var command = CommandRegistry.Find("clean")!; var parsed = CliArgSplitter.Split(["clean", "junk", "--bogus"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unexpected argument 'junk' for command 'clean'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) + .Throws() + .WithMessage("Unexpected argument 'junk' for command 'clean'.", StringComparison.Ordinal); } [Test] @@ -84,9 +84,9 @@ public async Task SettingsCarryingCommand_RejectsUnknownOption() { var command = CommandRegistry.Find("release")!; var parsed = CliArgSplitter.Split(["release", "--bogus"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unknown option '--bogus' for command 'release'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) + .Throws() + .WithMessage("Unknown option '--bogus' for command 'release'.", StringComparison.Ordinal); } [Test] @@ -122,9 +122,9 @@ public async Task SettingsCarryingCommand_RejectsValueOptionWithoutValue() { var command = CommandRegistry.Find("release")!; var parsed = CliArgSplitter.Split(["release", "--bump"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Option '--bump' requires a value."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) + .Throws() + .WithMessage("Option '--bump' requires a value.", StringComparison.Ordinal); } [Test] @@ -222,9 +222,9 @@ public async Task UnknownOptionBeforeAnOperand_IsReportedFirst() { var command = CommandRegistry.Find("deps update")!; var parsed = CliArgSplitter.Split(["deps", "update", "--bogus", "Serilog"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unknown option '--bogus' for command 'dependencies update'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) + .Throws() + .WithMessage("Unknown option '--bogus' for command 'dependencies update'.", StringComparison.Ordinal); } [Test] @@ -232,9 +232,9 @@ public async Task ExcessOperandAfterAnOption_IsRejected() { var command = CommandRegistry.Find("version advance")!; var parsed = CliArgSplitter.Split(["version", "advance", "--force", "minor", "extra"]); - var exception = await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) - .Throws(); - await Assert.That(exception!.Message).IsEqualTo("Unexpected argument 'extra' for command 'version advance'."); + await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) + .Throws() + .WithMessage("Unexpected argument 'extra' for command 'version advance'.", StringComparison.Ordinal); } [Test] diff --git a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs index 2d4acd77..c8576241 100644 --- a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text.Json; using Buildvana.Core; using Buildvana.Core.Json; @@ -68,10 +69,9 @@ public async Task TryDelegate_WithManifestUnderDotConfig_Fails() var service = CreateService(runner); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await service.TryDelegateAsync(Context(home)).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("git mv .config/dotnet-tools.json dotnet-tools.json"); + await Assert.That(async () => await service.TryDelegateAsync(Context(home)).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("git mv .config/dotnet-tools.json dotnet-tools.json", StringComparison.Ordinal); await Assert.That(runner.Runs.Count).IsEqualTo(0); await Assert.That(runner.InheritedStdioRuns.Count).IsEqualTo(0); } @@ -91,8 +91,9 @@ public async Task TryDelegate_WithManifestsInBothPlaces_FailsNamingTheMerge() // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await service.TryDelegateAsync(Context(home)).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message) + await Assert.That(exception.Message) .Contains("Merge the tools of .config/dotnet-tools.json into dotnet-tools.json, then delete .config/dotnet-tools.json."); await Assert.That(exception.Message).DoesNotContain("git mv"); } diff --git a/tests/Buildvana.Tool.Tests/DependenciesUpdateSettingsTests.cs b/tests/Buildvana.Tool.Tests/DependenciesUpdateSettingsTests.cs index ae470dc4..5267c56f 100644 --- a/tests/Buildvana.Tool.Tests/DependenciesUpdateSettingsTests.cs +++ b/tests/Buildvana.Tool.Tests/DependenciesUpdateSettingsTests.cs @@ -36,8 +36,8 @@ public async Task Parse_ReadsCheckAndAll() [Test] public async Task Parse_WithAllAndNoCheck_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse([], ["--all"])).Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(() => DependenciesUpdateSettings.Parse([], ["--all"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] @@ -57,17 +57,16 @@ public async Task Parse_ReadsTheStatedVersion() [Test] public async Task Parse_WithAVersionThatDoesNotParse_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--to", "not-a-version"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--to", "not-a-version"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] public async Task Parse_WithToAndCheck_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--to", "3.1.0", "--check"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws( + () => DependenciesUpdateSettings.Parse(["Serilog"], ["--to", "3.1.0", "--check"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } // Unlike --to, --latest names no version, so it takes patterns and several arguments as a plain run does. @@ -82,40 +81,37 @@ public async Task Parse_ReadsLatestNextToPatterns() [Test] public async Task Parse_WithLatestAndTo_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--latest", "--to", "3.1.0"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws( + () => DependenciesUpdateSettings.Parse(["Serilog"], ["--latest", "--to", "3.1.0"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] public async Task Parse_WithLatestAndCheck_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse([], ["--latest", "--check"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(() => DependenciesUpdateSettings.Parse([], ["--latest", "--check"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] public async Task Parse_WithNetSdkAndAnArgument_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--netsdk"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(() => DependenciesUpdateSettings.Parse(["Serilog"], ["--netsdk"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] public async Task Parse_WithToAndAPattern_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog.*"], ["--to", "3.1.0"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(() => DependenciesUpdateSettings.Parse(["Serilog.*"], ["--to", "3.1.0"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] public async Task Parse_WithToAndSeveralArguments_IsRefused() { - var exception = await Assert.That(() => DependenciesUpdateSettings.Parse(["Serilog", "Newtonsoft.Json"], ["--to", "3.1.0"])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws( + () => DependenciesUpdateSettings.Parse(["Serilog", "Newtonsoft.Json"], ["--to", "3.1.0"])); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } } diff --git a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs index 0d5b7517..a6bfb378 100644 --- a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Runtime; using Buildvana.Tool.Services.Dependencies; @@ -74,7 +75,8 @@ public async Task ResolveAsync_AnIdNoSourceKnows_Fails() var versions = new FakePackageVersionSource(); var exception = await Assert.That(async () => await ResolveAsync(versions, Packages(Pin("Serilog", "3.0.0"))).ConfigureAwait(false)) .Throws(); - var diagnostic = exception!.Diagnostics.Single(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + var diagnostic = exception.Diagnostics.Single(); await Assert.That(diagnostic.Code).IsEqualTo("BV1200"); await Assert.That(diagnostic.File).IsEqualTo("Directory.Packages.props"); } @@ -85,7 +87,8 @@ public async Task ResolveAsync_AVersionNoSourceHas_Fails() var versions = new FakePackageVersionSource().Knows("Serilog", ["3.1.0"]); var exception = await Assert.That(async () => await ResolveAsync(versions, Packages(Pin("Serilog", "3.0.0"))).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Single().Code).IsEqualTo("BV1201"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Single().Code).IsEqualTo("BV1201"); } [Test] @@ -95,7 +98,8 @@ public async Task ResolveAsync_ReportsEveryPinItCannotResolve() var inventory = Packages(Pin("Serilog", "3.0.0"), Pin("Newtonsoft.Json", "13.0.3")); var exception = await Assert.That(async () => await ResolveAsync(versions, inventory).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Select(static diagnostic => diagnostic.Code)).IsEquivalentTo(["BV1201", "BV1200"]); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Select(static diagnostic => diagnostic.Code)).IsEquivalentTo(["BV1201", "BV1200"]); } [Test] @@ -132,7 +136,8 @@ public async Task ResolveAsync_ANetSdkVersionTheIndexHasNot_Fails() var exception = await Assert .That(async () => await ResolveNetSdkAsync(releases, NetSdkPin.Create("10.0.999", allowPrerelease: false)).ConfigureAwait(false)) .Throws(); - var diagnostic = exception!.Diagnostics.Single(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + var diagnostic = exception.Diagnostics.Single(); await Assert.That(diagnostic.Code).IsEqualTo("BV1202"); await Assert.That(diagnostic.File).IsEqualTo("global.json"); } @@ -202,7 +207,8 @@ public async Task ResolveAsync_WithAStatedVersionNoSourceHas_Fails() var exception = await Assert .That(async () => await ResolveAsync(versions, Packages(Pin("Serilog", "3.0.0")), request: request).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Select(static diagnostic => diagnostic.Code)).Contains("BV1201"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Select(static diagnostic => diagnostic.Code)).Contains("BV1201"); } [Test] @@ -213,7 +219,8 @@ public async Task ResolveAsync_WithAStatedVersionForAnIdNoPinHas_Fails() var exception = await Assert .That(async () => await ResolveAsync(versions, Packages(Pin("Serilog", "3.0.0")), request: request).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Single().Code).IsEqualTo("BV1203"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Single().Code).IsEqualTo("BV1203"); } // A family id with no pin bv manages gets the message that says where to go instead. @@ -225,7 +232,8 @@ public async Task ResolveAsync_WithAStatedVersionForAFamilyId_PointsAtSelfUpdate var exception = await Assert .That(async () => await ResolveAsync(versions, Packages(Pin("Serilog", "3.0.0")), request: request).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Single().Message).Contains("bv self-update"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Single().Message).Contains("bv self-update"); } // A request for the latest version moves past the kind of the policy, a disabled one included. The pin is @@ -303,7 +311,8 @@ public async Task ResolveAsync_WithAStatedNetSdkVersionTheIndexHasNot_Fails() var pin = NetSdkPin.Create("10.0.100", allowPrerelease: false); var exception = await Assert.That(async () => await ResolveNetSdkAsync(releases, pin, request).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.Diagnostics.Single().Code).IsEqualTo("BV1202"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Diagnostics.Single().Code).IsEqualTo("BV1202"); } private static DependencyPin Pin(string id, string versionText) diff --git a/tests/Buildvana.Tool.Tests/DependencyScopeSelectionTests.cs b/tests/Buildvana.Tool.Tests/DependencyScopeSelectionTests.cs index 4c197341..ffee96f1 100644 --- a/tests/Buildvana.Tool.Tests/DependencyScopeSelectionTests.cs +++ b/tests/Buildvana.Tool.Tests/DependencyScopeSelectionTests.cs @@ -39,10 +39,9 @@ public async Task Narrow_WithAVersionForTheNetSdkAlone_ChangesNothing() [Test] public async Task Narrow_WithAVersionAndAnotherScopeSelected_IsRefused() { - var exception = await Assert - .That(() => DependencyScopeSelection.Narrow(AllScopes.ToHashSet(), namesPins: false, statesVersion: true)) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws( + () => DependencyScopeSelection.Narrow(AllScopes.ToHashSet(), namesPins: false, statesVersion: true)); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } [Test] @@ -78,9 +77,8 @@ public async Task Resolve_WithScopesExcluded_TakesTheRest() [Test] public async Task Resolve_MixingTheTwoFamilies_IsAUsageError() { - var exception = await Assert.That(() => Resolve([DependencyScope.NetSdk], [DependencyScope.Packages])) - .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(2); + var exception = Assert.Throws(() => Resolve([DependencyScope.NetSdk], [DependencyScope.Packages])); + await Assert.That(exception.ExitCode).IsEqualTo(2); } [Test] diff --git a/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs b/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs index 6573fdf3..4a0a51a5 100644 --- a/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs @@ -100,8 +100,8 @@ public async Task Resolve_OfAPinStatingNonsense_Fails() { var resolver = new EffectivePolicyResolver(new DependenciesConfig()); var pin = Pin(DependencyScope.Packages) with { MetadataPolicy = "sometimes" }; - var exception = await Assert.That(() => resolver.Resolve(pin)).Throws(); - await Assert.That(exception!.Message).Contains("sometimes"); + var exception = Assert.Throws(() => resolver.Resolve(pin)); + await Assert.That(exception.Message).Contains("sometimes"); await Assert.That(exception.Message).Contains("Serilog"); await Assert.That(exception.Message).Contains("Directory.Packages.props"); } diff --git a/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs b/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs index 88941739..d6d379b1 100644 --- a/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs @@ -201,9 +201,9 @@ public async Task Operation_WhenTheApiRefuses_FailsWithTheStatusAndTheMessage(st using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.Conflict, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).Contains("409 Conflict: release 1.0.0 already exists"); + await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("409 Conflict: release 1.0.0 already exists", StringComparison.Ordinal); } [Test] @@ -220,9 +220,9 @@ public async Task Operation_WhenTheApiAnswersNotFoundWithErrors_FailsWithTheMess using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.NotFound, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).Contains("404 Not Found: The target couldn't be found."); + await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("404 Not Found: The target couldn't be found.", StringComparison.Ordinal); } [Test] @@ -235,9 +235,9 @@ public async Task Operation_WhenTheErrorAnswerStatesNoMessage_FailsWithTheStatus using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.BadGateway, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).EndsWith("with 502 Bad Gateway."); + await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) + .Throws() + .WithMessageMatching("*with 502 Bad Gateway."); } private static ForgejoApiClient NewClient(StubHttpMessageHandler handler) => new("forgejo.example.com", Token, handler); diff --git a/tests/Buildvana.Tool.Tests/ForgejoHostTests.cs b/tests/Buildvana.Tool.Tests/ForgejoHostTests.cs index e5a124ee..c9a539e6 100644 --- a/tests/Buildvana.Tool.Tests/ForgejoHostTests.cs +++ b/tests/Buildvana.Tool.Tests/ForgejoHostTests.cs @@ -76,9 +76,9 @@ public async Task Constructor_ForAPathOfThreeSegments_Throws() await using (services.ConfigureAwait(false)) { // ReSharper disable once AccessToDisposedClosure // False positive: the assertion runs the lambda before services is disposed - var exception = await Assert.That(() => new ForgejoHost(services, Parse("https://codeberg.org/a/b/c"))) - .Throws(); - await Assert.That(exception!.Message).Contains("not a valid Forgejo repository URL"); + await Assert.That(() => new ForgejoHost(services, Parse("https://codeberg.org/a/b/c"))) + .Throws() + .WithMessageContaining("not a valid Forgejo repository URL", StringComparison.Ordinal); } } @@ -127,8 +127,9 @@ public async Task IsPrivateRepositoryAsync_WithoutAToken_Throws() Environment.SetEnvironmentVariable(TokenVariable, null); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before host is disposed - var exception = await Assert.That(host.IsPrivateRepositoryAsync).Throws(); - await Assert.That(exception!.Message).Contains(TokenVariable); + await Assert.That(host.IsPrivateRepositoryAsync) + .Throws() + .WithMessageContaining(TokenVariable, StringComparison.Ordinal); await Assert.That(api.Requests).IsEmpty(); } } diff --git a/tests/Buildvana.Tool.Tests/GitHostTests.cs b/tests/Buildvana.Tool.Tests/GitHostTests.cs index 62609f91..8b07aef4 100644 --- a/tests/Buildvana.Tool.Tests/GitHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GitHostTests.cs @@ -102,15 +102,17 @@ public async Task DetectKind_ForAnotherHost_IsNull() [Arguments("")] public async Task ParseRepositoryUrl_WithoutAUrl_Throws(string? url) { - var exception = await Assert.That(() => GitHost.ParseRepositoryUrl(Repository(url))).Throws(); - await Assert.That(exception!.Message).Contains("repository.url"); + await Assert.That(() => GitHost.ParseRepositoryUrl(Repository(url))) + .Throws() + .WithMessageContaining("repository.url", StringComparison.Ordinal); } [Test] public async Task ParseRepositoryUrl_WithAnInvalidUrl_Throws() { - var exception = await Assert.That(() => GitHost.ParseRepositoryUrl(Repository("not a URL"))).Throws(); - await Assert.That(exception!.Message).Contains("not a Git URL"); + await Assert.That(() => GitHost.ParseRepositoryUrl(Repository("not a URL"))) + .Throws() + .WithMessageContaining("not a Git URL", StringComparison.Ordinal); } [Test] @@ -176,8 +178,9 @@ public async Task Create_ForAUrlWithOneSegment_Throws() await using (services.ConfigureAwait(false)) { // ReSharper disable once AccessToDisposedClosure // False positive: the lambda runs before services is disposed - var exception = await Assert.That(() => GitHost.Create(services)).Throws(); - await Assert.That(exception!.Message).Contains("names no repository"); + await Assert.That(() => GitHost.Create(services)) + .Throws() + .WithMessageContaining("names no repository", StringComparison.Ordinal); } } diff --git a/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs b/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs index 993f8fac..55fe8a9c 100644 --- a/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs @@ -262,9 +262,9 @@ public async Task Operation_WhenTheApiRefuses_FailsWithTheStatusAndTheMessage(st new StubHttpAnswer(HttpStatusCode.UnprocessableEntity, """{"message":"Validation Failed"}"""))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).Contains("422 Unprocessable Entity: Validation Failed"); + await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("422 Unprocessable Entity: Validation Failed", StringComparison.Ordinal); } [Test] @@ -276,9 +276,9 @@ public async Task Operation_WhenTheErrorAnswerStatesNoMessage_FailsWithTheStatus using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.BadGateway, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).EndsWith("with 502 Bad Gateway."); + await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) + .Throws() + .WithMessageMatching("*with 502 Bad Gateway."); } [Test] diff --git a/tests/Buildvana.Tool.Tests/GitHubHostTests.cs b/tests/Buildvana.Tool.Tests/GitHubHostTests.cs index 10652df8..5cfe4e18 100644 --- a/tests/Buildvana.Tool.Tests/GitHubHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GitHubHostTests.cs @@ -60,8 +60,9 @@ public async Task Constructor_ForAPathOfThreeSegments_Throws() await using (services.ConfigureAwait(false)) { // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before services is disposed - var exception = await Assert.That(() => new GitHubHost(services, Parse("https://github.com/a/b/c"))).Throws(); - await Assert.That(exception!.Message).Contains("not a valid GitHub repository URL"); + await Assert.That(() => new GitHubHost(services, Parse("https://github.com/a/b/c"))) + .Throws() + .WithMessageContaining("not a valid GitHub repository URL", StringComparison.Ordinal); } } @@ -102,8 +103,9 @@ public async Task IsPrivateRepositoryAsync_WithoutAToken_Throws() Environment.SetEnvironmentVariable(TokenVariable, null); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before host is disposed - var exception = await Assert.That(host.IsPrivateRepositoryAsync).Throws(); - await Assert.That(exception!.Message).Contains(TokenVariable); + await Assert.That(host.IsPrivateRepositoryAsync) + .Throws() + .WithMessageContaining(TokenVariable, StringComparison.Ordinal); await Assert.That(api.Requests).IsEmpty(); } } diff --git a/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs b/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs index a843121b..8a76dfd7 100644 --- a/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs @@ -316,9 +316,9 @@ public async Task Operation_WhenTheApiRefuses_FailsWithTheStatusAndTheMessage(st new StubHttpAnswer(HttpStatusCode.Forbidden, """{"message":"403 Forbidden"}"""))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).Contains("403 Forbidden: 403 Forbidden"); + await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("403 Forbidden: 403 Forbidden", StringComparison.Ordinal); } [Test] @@ -332,9 +332,9 @@ public async Task Operation_WhenTheApiRefuses_QuotesTheMessageWhateverItsShape(s using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.BadRequest, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetProjectAsync(Project).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).EndsWith("with 400 Bad Request" + expected); + await Assert.That(async () => await client.GetProjectAsync(Project).ConfigureAwait(false)) + .Throws() + .WithMessageMatching("*with 400 Bad Request" + expected); } [Test] @@ -349,9 +349,9 @@ public async Task Operation_WhenTheErrorAnswerStatesNoMessage_FailsWithTheStatus using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.BadGateway, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetProjectAsync(Project).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).EndsWith("with 502 Bad Gateway."); + await Assert.That(async () => await client.GetProjectAsync(Project).ConfigureAwait(false)) + .Throws() + .WithMessageMatching("*with 502 Bad Gateway."); } [Test] diff --git a/tests/Buildvana.Tool.Tests/GitLabHostTests.cs b/tests/Buildvana.Tool.Tests/GitLabHostTests.cs index b1aa03c3..05a98f08 100644 --- a/tests/Buildvana.Tool.Tests/GitLabHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabHostTests.cs @@ -166,8 +166,9 @@ public async Task IsPrivateRepositoryAsync_WithoutAToken_Throws() Environment.SetEnvironmentVariable(TokenVariable, null); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before host is disposed - var exception = await Assert.That(host.IsPrivateRepositoryAsync).Throws(); - await Assert.That(exception!.Message).Contains(TokenVariable); + await Assert.That(host.IsPrivateRepositoryAsync) + .Throws() + .WithMessageContaining(TokenVariable, StringComparison.Ordinal); await Assert.That(api.Requests).IsEmpty(); } } diff --git a/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs b/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs index b93ad526..8329f8ca 100644 --- a/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Net; using Buildvana.Core; using Buildvana.Core.ConsoleOutput; @@ -45,10 +46,9 @@ public async Task BeginAsync_BelowTheMaintainerRole_Throws() using var api = Answering(Member(30)); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before api is disposed - var exception = await Assert.That(() => RunAsync(api, static (publisher, _) => publisher.BeginAsync())) - .Throws(); - - await Assert.That(exception!.Message).Contains("access level 30 on tenacom/buildvana, below the Maintainer role"); + await Assert.That(() => RunAsync(api, static (publisher, _) => publisher.BeginAsync())) + .Throws() + .WithMessageContaining("access level 30 on tenacom/buildvana, below the Maintainer role", StringComparison.Ordinal); } // An instance administrator, or an organization owner, has no membership and can still delete a package. @@ -86,10 +86,9 @@ public async Task BeginAsync_WhenTheReleasesCannotBeRead_Throws_AndReadsNothingE }); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before api is disposed - var exception = await Assert.That(() => RunAsync(api, static (publisher, _) => publisher.BeginAsync())) - .Throws(); - - await Assert.That(exception!.Message).Contains("403 Forbidden"); + await Assert.That(() => RunAsync(api, static (publisher, _) => publisher.BeginAsync())) + .Throws() + .WithMessageContaining("403 Forbidden", StringComparison.Ordinal); await Assert.That(api.Requests).Count().IsEqualTo(1); } @@ -215,9 +214,10 @@ public async Task CheckAssets_ReportsEveryAssetGitLabRefuses() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before api is disposed var exception = await Assert.That(() => RunAsync(api, (publisher, _) => CheckAssetsAsync(publisher, assets))) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); await Assert.That(api.Requests).IsEmpty(); - await Assert.That(exception!.Message).IsEqualTo( + await Assert.That(exception.Message).IsEqualTo( "GitLab refuses the release assets:" + "\n- 'Foo 1.zip' is not a valid file name in the generic package registry." + "\n- 'a+b.zip' is not a valid file name for a release link." diff --git a/tests/Buildvana.Tool.Tests/GitServiceTests.cs b/tests/Buildvana.Tool.Tests/GitServiceTests.cs index 21c2e8ff..7476fc6e 100644 --- a/tests/Buildvana.Tool.Tests/GitServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/GitServiceTests.cs @@ -248,8 +248,9 @@ public async Task PushBranch_WhenNotFastForward_Throws() var remoteTip = repo.GetRemoteTipSha(); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushBranch("origin")).Throws(); - await Assert.That(exception!.Message).Contains($"'refs/heads/{repo.CurrentBranchName}'"); + await Assert.That(() => git.PushBranch("origin")) + .Throws() + .WithMessageContaining($"'refs/heads/{repo.CurrentBranchName}'", StringComparison.Ordinal); await Assert.That(repo.GetRemoteTipSha()).IsEqualTo(remoteTip); } } @@ -279,8 +280,9 @@ public async Task PushBranch_WhenRemoteRefusesUpdate_Throws_AndLeavesRemoteBranc repo.LockRemoteReference(branchRef); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushBranch("origin")).Throws(); - await Assert.That(exception!.Message).Contains(branchRef); + await Assert.That(() => git.PushBranch("origin")) + .Throws() + .WithMessageContaining(branchRef, StringComparison.Ordinal); await Assert.That(repo.GetRemoteTipSha()).IsEqualTo(remoteTip); } @@ -291,8 +293,8 @@ public async Task PushBranch_WhenRemoteIsUnreachable_Throws() using var git = CreateGitService(repo); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushBranch("origin")).Throws(); - await Assert.That(exception!.Message).Contains("'origin'"); + var exception = Assert.Throws(() => git.PushBranch("origin")); + await Assert.That(exception.Message).Contains("'origin'"); await Assert.That(exception.InnerException).IsNotNull(); } @@ -314,8 +316,9 @@ public async Task PushBranch_ToMissingRemote_Throws() using var git = CreateGitService(repo); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushBranch("upstream")).Throws(); - await Assert.That(exception!.Message).Contains("'upstream'"); + await Assert.That(() => git.PushBranch("upstream")) + .Throws() + .WithMessageContaining("'upstream'", StringComparison.Ordinal); } [Test] @@ -366,8 +369,9 @@ public async Task PushTag_WhenRemoteTagPointsToAncestor_Throws_AndLeavesRemoteTa git.CreateTag("1.0.0", repo.HeadSha); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushTag("origin", "1.0.0")).Throws(); - await Assert.That(exception!.Message).Contains(firstSha); + await Assert.That(() => git.PushTag("origin", "1.0.0")) + .Throws() + .WithMessageContaining(firstSha, StringComparison.Ordinal); await Assert.That(repo.GetRemoteTagSha("1.0.0")).IsEqualTo(firstSha); } @@ -407,8 +411,9 @@ public async Task PushTag_WhenRemoteRefusesUpdate_Throws() repo.LockRemoteReference("refs/tags/1.0.0"); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushTag("origin", "1.0.0")).Throws(); - await Assert.That(exception!.Message).Contains("refs/tags/1.0.0"); + await Assert.That(() => git.PushTag("origin", "1.0.0")) + .Throws() + .WithMessageContaining("refs/tags/1.0.0", StringComparison.Ordinal); await Assert.That(repo.GetRemoteTagSha("1.0.0")).IsNull(); } @@ -420,8 +425,8 @@ public async Task PushTag_WhenRemoteIsUnreachable_Throws() git.CreateTag("1.0.0", repo.HeadSha); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.PushTag("origin", "1.0.0")).Throws(); - await Assert.That(exception!.Message).Contains("'origin'"); + var exception = Assert.Throws(() => git.PushTag("origin", "1.0.0")); + await Assert.That(exception.Message).Contains("'origin'"); await Assert.That(exception.InnerException).IsNotNull(); } @@ -451,8 +456,9 @@ public async Task DeleteRemoteTag_WhenRemoteRefusesUpdate_Throws_AndKeepsRemoteT repo.LockRemoteReference("refs/tags/1.0.0"); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.DeleteRemoteTag("origin", "1.0.0")).Throws(); - await Assert.That(exception!.Message).Contains("refs/tags/1.0.0"); + await Assert.That(() => git.DeleteRemoteTag("origin", "1.0.0")) + .Throws() + .WithMessageContaining("refs/tags/1.0.0", StringComparison.Ordinal); await Assert.That(repo.GetRemoteTagSha("1.0.0")).IsEqualTo(repo.HeadSha); } @@ -490,8 +496,8 @@ public async Task FetchTags_WhenRemoteIsUnreachable_Throws() using var git = CreateGitService(repo); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before git is disposed - var exception = await Assert.That(() => git.FetchTags("origin")).Throws(); - await Assert.That(exception!.Message).Contains("'origin'"); + var exception = Assert.Throws(() => git.FetchTags("origin")); + await Assert.That(exception.Message).Contains("'origin'"); await Assert.That(exception.InnerException).IsNotNull(); } diff --git a/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs b/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs index 0c0aab58..14cfdb1d 100644 --- a/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs @@ -200,9 +200,9 @@ public async Task Operation_WhenTheApiRefuses_FailsWithTheStatusAndTheMessage(st using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.Conflict, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).Contains("409 Conflict: release 1.0.0 already exists"); + await Assert.That(async () => await InvokeAsync(client, operation).ConfigureAwait(false)) + .Throws() + .WithMessageContaining("409 Conflict: release 1.0.0 already exists", StringComparison.Ordinal); } [Test] @@ -215,9 +215,9 @@ public async Task Operation_WhenTheErrorAnswerStatesNoMessage_FailsWithTheStatus using var client = NewClient(AnsweringEverything(new StubHttpAnswer(HttpStatusCode.BadGateway, body))); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) - .Throws(); - await Assert.That(exception!.Message).EndsWith("with 502 Bad Gateway."); + await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).ConfigureAwait(false)) + .Throws() + .WithMessageMatching("*with 502 Bad Gateway."); } private static GiteaApiClient NewClient(StubHttpMessageHandler handler) => new("gitea.example.com", Token, handler); diff --git a/tests/Buildvana.Tool.Tests/GiteaHostTests.cs b/tests/Buildvana.Tool.Tests/GiteaHostTests.cs index 3f82fd51..a12f8674 100644 --- a/tests/Buildvana.Tool.Tests/GiteaHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GiteaHostTests.cs @@ -75,9 +75,9 @@ public async Task Constructor_ForAPathOfThreeSegments_Throws() await using (services.ConfigureAwait(false)) { // ReSharper disable once AccessToDisposedClosure // False positive: the assertion runs the lambda before services is disposed - var exception = await Assert.That(() => new GiteaHost(services, Parse("https://gitea.com/a/b/c"))) - .Throws(); - await Assert.That(exception!.Message).Contains("not a valid Gitea repository URL"); + await Assert.That(() => new GiteaHost(services, Parse("https://gitea.com/a/b/c"))) + .Throws() + .WithMessageContaining("not a valid Gitea repository URL", StringComparison.Ordinal); } } @@ -126,8 +126,9 @@ public async Task IsPrivateRepositoryAsync_WithoutAToken_Throws() Environment.SetEnvironmentVariable(TokenVariable, null); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before host is disposed - var exception = await Assert.That(host.IsPrivateRepositoryAsync).Throws(); - await Assert.That(exception!.Message).Contains(TokenVariable); + await Assert.That(host.IsPrivateRepositoryAsync) + .Throws() + .WithMessageContaining(TokenVariable, StringComparison.Ordinal); await Assert.That(api.Requests).IsEmpty(); } } diff --git a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs index c12204fe..b3e5d7e6 100644 --- a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs +++ b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text.Json; using Buildvana.Core; using Buildvana.Core.ConsoleOutput; @@ -149,7 +150,8 @@ public async Task RunHookAsync_WhenACheckRunsHookFails_Fails() Task Act() => runner.RunHookAsync(args, acceptsPendingWork: true); var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); } [Test] @@ -180,8 +182,7 @@ public async Task RunHookAsync_WhenArgsFileCannotBeWritten_FailsWithCleanError() Task Act() => runner.RunHookAsync(args); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Could not write to"); + await Assert.That(Act).Throws().WithMessageContaining("Could not write to", StringComparison.Ordinal); await Assert.That(appRunner.Runs.Count).IsEqualTo(0); } diff --git a/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs b/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs index db328c8a..a192cf3b 100644 --- a/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs +++ b/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Core.Dependencies; using Buildvana.Core.Testing; @@ -162,8 +163,9 @@ public async Task DetectAsync_WithARestoreThatFailedForAReasonOfItsOwn_ReportsAF // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await DetectAsync(Inventory(home, ["Alpha"])).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); } // Audit findings are errors under TreatWarningsAsErrors, and the restore wrote every graph all the same. diff --git a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs index 169d2e25..87219875 100644 --- a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs +++ b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; @@ -185,8 +186,9 @@ public async Task RunAsync_WithASourceTheRestoreCouldNotRead_ReportsAFailedStep( // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await RunAsync(home, new FakeDependencyRestorer(), Advisories(), Versions()) .ConfigureAwait(false)).Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + await Assert.That(exception.ExitCode).IsEqualTo(3); await Assert.That(exception.Message).Contains("could not read a package source"); } @@ -198,8 +200,9 @@ public async Task RunAsync_WithARestoreThatFailedForAnotherReason_ReportsAFailed // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await RunAsync(home, new FakeDependencyRestorer(), Advisories(), Versions()) .ConfigureAwait(false)).Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + await Assert.That(exception.ExitCode).IsEqualTo(3); await Assert.That(exception.Message).Contains("NU1101"); } @@ -249,8 +252,9 @@ public async Task RunAsync_WhenTheGraphNeverSettles_ReportsAFailedStep() // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await RunAsync(home, restorer, advisories, versions).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.ExitCode).IsEqualTo(1); + await Assert.That(exception.ExitCode).IsEqualTo(1); await Assert.That(exception.Message).Contains("did not settle"); } diff --git a/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs b/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs index 950e3d4f..2ce3109c 100644 --- a/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs @@ -116,8 +116,8 @@ public async Task Read_WithNoAssetsFile_ReportsAFailedStep() using var home = new TempHome(); var project = home.GetFullPath(ProjectFileName); var assetsPath = home.GetFullPath(AssetsFileName); - var exception = await Assert.That(() => ProjectAssetsReader.Read(project, assetsPath)).Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + var exception = Assert.Throws(() => ProjectAssetsReader.Read(project, assetsPath)); + await Assert.That(exception.ExitCode).IsEqualTo(3); } [Test] @@ -127,8 +127,8 @@ public async Task Read_WithAGraphThatDoesNotParse_ReportsAFailedStep() home.WriteFile(AssetsFileName, "this is not an assets file"); var project = home.GetFullPath(ProjectFileName); var assetsPath = home.GetFullPath(AssetsFileName); - var exception = await Assert.That(() => ProjectAssetsReader.Read(project, assetsPath)).Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + var exception = Assert.Throws(() => ProjectAssetsReader.Read(project, assetsPath)); + await Assert.That(exception.ExitCode).IsEqualTo(3); } private static ProjectAssets Read(TempHome home) diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs index 2ddc7497..fc15be69 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs @@ -160,8 +160,7 @@ public async Task Release_WithEmptySectionAndNoSubstitute_Fails() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("release.emptyChangelog"); + await Assert.That(Act).Throws().WithMessageContaining("release.emptyChangelog", StringComparison.Ordinal); // The failure happens before anything is built or committed. await Assert.That(harness.PipelineSteps.Count).IsEqualTo(0); @@ -228,8 +227,9 @@ public async Task Release_WithLinkOutsideRepository_FailsNamingTheTarget() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo("CHANGELOG.md links '../outside.md', which is not a path to a file in the repository."); + await Assert.That(Act) + .Throws() + .WithMessage("CHANGELOG.md links '../outside.md', which is not a path to a file in the repository.", StringComparison.Ordinal); // The links are pinned once the version is final, after the build, so the failure comes with the // release commit in place. The rollback undoes that commit. diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs index ae2f0693..0be11d1e 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; /// @@ -21,8 +22,9 @@ public async Task Release_OnExistingTag_FailsBeforeBuilding() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains($"Tag '{ReleasedVersion}' already exists"); + await Assert.That(Act) + .Throws() + .WithMessageContaining($"Tag '{ReleasedVersion}' already exists", StringComparison.Ordinal); // The check gates the pipeline run: no step runs, Clean included. await Assert.That(harness.PipelineSteps.Count).IsEqualTo(0); @@ -81,8 +83,7 @@ public async Task Release_WithoutRepositoryUrl_FailsBeforeBuilding() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("repository.url"); + await Assert.That(Act).Throws().WithMessageContaining("repository.url", StringComparison.Ordinal); // The preliminary checks run before the pipeline, so a release that can never succeed costs // nothing: no clean, no build, no test run. @@ -98,8 +99,7 @@ public async Task Release_OnAnUnknownHost_WithoutAllowUnknownHost_FailsBeforeBui // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("release.allowUnknownHost"); + await Assert.That(Act).Throws().WithMessageContaining("release.allowUnknownHost", StringComparison.Ordinal); await Assert.That(harness.PipelineSteps.Count).IsEqualTo(0); await Assert.That(harness.Repo.GetRemoteTipSha()).IsNull(); } @@ -116,8 +116,7 @@ public async Task Release_WhenTheReleaseRemoteHasAHigherTag_FailsBeforeBuilding( // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("lower than latest"); + await Assert.That(Act).Throws().WithMessageContaining("lower than latest", StringComparison.Ordinal); await Assert.That(harness.Repo.TagNames).IsEquivalentTo(["2.4.0"]); await Assert.That(harness.PipelineSteps.Count).IsEqualTo(0); } @@ -130,8 +129,7 @@ public async Task Release_WithoutAnyCommitterIdentity_FailsBeforeBuilding() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("committer identity"); + await Assert.That(Act).Throws().WithMessageContaining("committer identity", StringComparison.Ordinal); await Assert.That(harness.PipelineSteps.Count).IsEqualTo(0); await Assert.That(harness.Events.Count).IsEqualTo(0); } @@ -149,7 +147,8 @@ public async Task Release_WithUncommittedVersionFile_Fails() Task Act() => harness.RunAsync(); var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Git height of 0"); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.Message).Contains("Git height of 0"); await Assert.That(exception.Message).Contains("Commit VERSION"); // It is caught before anything is packed, and the release commit is rolled back. diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandForgejoTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandForgejoTests.cs index f4d0be76..3cc46f89 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandForgejoTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandForgejoTests.cs @@ -71,8 +71,7 @@ public async Task Release_WithoutPushAccess_OrWithReleasesDisabled_FailsBeforeTh // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains(expectedMessage); + await Assert.That(Act).Throws().WithMessageContaining(expectedMessage, StringComparison.Ordinal); await Assert.That(api.Requests.Count).IsEqualTo(1); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); await Assert.That(harness.Repo.GetRemoteTagSha(ReleasedVersion)).IsNull(); @@ -99,9 +98,11 @@ public async Task Release_WithTwoAssetsOfTheSameFileName_FailsBeforeThePush() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo( - "The release assets cannot go to a Forgejo release:\n- More than one asset has the file name 'setup.exe'."); + await Assert.That(Act) + .Throws() + .WithMessage( + "The release assets cannot go to a Forgejo release:\n- More than one asset has the file name 'setup.exe'.", + StringComparison.Ordinal); await Assert.That(harness.Events.Select(static x => x.Name)).DoesNotContain("nuget-push"); await Assert.That(api.Requests.Count).IsEqualTo(1); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); @@ -118,8 +119,7 @@ public async Task Release_WhenAnUploadFails_DeletesTheDraft_ThenTheRecordOfTheTa // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("quota exceeded"); + await Assert.That(Act).Throws().WithMessageContaining("quota exceeded", StringComparison.Ordinal); var deletions = api.Requests.Where(static x => x.Method == HttpMethod.Delete).Select(static x => x.Url); string[] expected = [ReleaseUrl, ApiRepositoryUrl + "/tags/" + ReleasedVersion]; await Assert.That(deletions).IsEquivalentTo(expected, CollectionOrdering.Matching); diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandGitHubTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandGitHubTests.cs index 3f60f2aa..9c328db2 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandGitHubTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandGitHubTests.cs @@ -71,9 +71,11 @@ public async Task Release_WithTwoAssetsOfTheSameFileName_FailsBeforeThePush() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo( - "GitHub refuses the release assets:\n- More than one asset has the file name 'setup.exe'."); + await Assert.That(Act) + .Throws() + .WithMessage( + "GitHub refuses the release assets:\n- More than one asset has the file name 'setup.exe'.", + StringComparison.Ordinal); await Assert.That(harness.Events.Select(static x => x.Name)).DoesNotContain("nuget-push"); await Assert.That(api.Requests.Any(static x => x.Url.StartsWith(UploadUrl, StringComparison.Ordinal))).IsFalse(); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); @@ -90,8 +92,7 @@ public async Task Release_WhenAnUploadFails_DeletesTheDraft_AndTheTag() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Validation Failed"); + await Assert.That(Act).Throws().WithMessageContaining("Validation Failed", StringComparison.Ordinal); var delete = api.Requests[^1]; await Assert.That(delete.Method).IsEqualTo(HttpMethod.Delete); await Assert.That(delete.Url).IsEqualTo(ApiRepositoryUrl + "/releases/42"); diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandGitLabTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandGitLabTests.cs index 9cec0823..271eb16a 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandGitLabTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandGitLabTests.cs @@ -62,8 +62,7 @@ public async Task Release_BelowTheMaintainerRole_FailsBeforeTheReleaseCommit() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("below the Maintainer role"); + await Assert.That(Act).Throws().WithMessageContaining("below the Maintainer role", StringComparison.Ordinal); await Assert.That(api.Requests.Count).IsEqualTo(3); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); await Assert.That(harness.Repo.GetRemoteTagSha(ReleasedVersion)).IsNull(); @@ -103,8 +102,9 @@ public async Task Release_WithAnAssetGitLabRefuses_FailsBeforeThePush() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("'Foo 1.zip' is not a valid file name in the generic package registry."); + await Assert.That(Act) + .Throws() + .WithMessageContaining("'Foo 1.zip' is not a valid file name in the generic package registry.", StringComparison.Ordinal); await Assert.That(harness.Events.Select(static x => x.Name)).DoesNotContain("nuget-push"); await Assert.That(api.Requests.Any(static x => x.Method == HttpMethod.Put)).IsFalse(); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); @@ -121,8 +121,7 @@ public async Task Release_WhenTheReleaseCannotBeCreated_DeletesThePackage_AndThe // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("Validation failed"); + await Assert.That(Act).Throws().WithMessageContaining("Validation failed", StringComparison.Ordinal); var delete = api.Requests[^1]; await Assert.That(delete.Method).IsEqualTo(HttpMethod.Delete); await Assert.That(delete.Url).IsEqualTo(ApiProjectUrl + "/packages/7"); diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandGiteaTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandGiteaTests.cs index 0dcb24b0..25ca40a6 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandGiteaTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandGiteaTests.cs @@ -71,8 +71,7 @@ public async Task Release_WithoutPushAccess_OrWithReleasesDisabled_FailsBeforeTh // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains(expectedMessage); + await Assert.That(Act).Throws().WithMessageContaining(expectedMessage, StringComparison.Ordinal); await Assert.That(api.Requests.Count).IsEqualTo(1); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); await Assert.That(harness.Repo.GetRemoteTagSha(ReleasedVersion)).IsNull(); @@ -99,9 +98,11 @@ public async Task Release_WithTwoAssetsOfTheSameFileName_FailsBeforeThePush() // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).IsEqualTo( - "The release assets cannot go to a Gitea release:\n- More than one asset has the file name 'setup.exe'."); + await Assert.That(Act) + .Throws() + .WithMessage( + "The release assets cannot go to a Gitea release:\n- More than one asset has the file name 'setup.exe'.", + StringComparison.Ordinal); await Assert.That(harness.Events.Select(static x => x.Name)).DoesNotContain("nuget-push"); await Assert.That(api.Requests.Count).IsEqualTo(1); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(remoteTip); @@ -118,8 +119,7 @@ public async Task Release_WhenAnUploadFails_DeletesTheDraft_ThenTheRecordOfTheTa // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the harness is disposed Task Act() => harness.RunAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("quota exceeded"); + await Assert.That(Act).Throws().WithMessageContaining("quota exceeded", StringComparison.Ordinal); var deletions = api.Requests.Where(static x => x.Method == HttpMethod.Delete).Select(static x => x.Url); string[] expected = [ReleaseUrl, ApiRepositoryUrl + "/tags/" + ReleasedVersion]; await Assert.That(deletions).IsEquivalentTo(expected, CollectionOrdering.Matching); diff --git a/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs b/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs index 2b12a881..d6359f39 100644 --- a/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs @@ -31,11 +31,10 @@ public async Task UpdateReferences_WithManifestUnderDotConfig_Fails() home.WriteFile(".config/dotnet-tools.json", Manifest); var updater = CreateUpdater(home); - var exception = await Assert + await Assert .That(() => updater.UpdateReferences(new Dictionary { ["bv"] = "2.1.41-preview" })) - .Throws(); - - await Assert.That(exception!.Message).Contains("git mv .config/dotnet-tools.json dotnet-tools.json"); + .Throws() + .WithMessageContaining("git mv .config/dotnet-tools.json dotnet-tools.json", StringComparison.Ordinal); await Assert.That(home.ReadFile(".config/dotnet-tools.json")).IsEqualTo(Manifest); } diff --git a/tests/Buildvana.Tool.Tests/SelfUpdateCommandTests.cs b/tests/Buildvana.Tool.Tests/SelfUpdateCommandTests.cs index 654ecddd..50c708fe 100644 --- a/tests/Buildvana.Tool.Tests/SelfUpdateCommandTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfUpdateCommandTests.cs @@ -142,11 +142,10 @@ public async Task ExecuteAsync_WithInvalidToVersion_FailsBeforeChangingAnything( var settings = new SelfUpdateSettings { To = "not-a-version" }; var command = new SelfUpdateCommand(CreateService(home, runner), settings, new TestConsole()); - var exception = await Assert + await Assert .That(async () => _ = await command.ExecuteAsync(CancellationToken.None).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("--to"); + .Throws() + .WithMessageContaining("--to", StringComparison.Ordinal); await Assert.That(runner.Runs.Count).IsEqualTo(0); } diff --git a/tests/Buildvana.Tool.Tests/SelfUpdateSettingsTests.cs b/tests/Buildvana.Tool.Tests/SelfUpdateSettingsTests.cs index c9a47ad1..f6518166 100644 --- a/tests/Buildvana.Tool.Tests/SelfUpdateSettingsTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfUpdateSettingsTests.cs @@ -48,9 +48,9 @@ public async Task ResolveTo_WithInvalidVersion_Fails() { var settings = SelfUpdateSettings.Parse(["--to", "not-a-version"]); - var exception = await Assert.That(() => _ = settings.ResolveTo()).Throws(); + var exception = Assert.Throws(() => _ = settings.ResolveTo()); - await Assert.That(exception!.Message).Contains("--to"); + await Assert.That(exception.Message).Contains("--to"); await Assert.That(exception.Message).Contains("not-a-version"); } @@ -99,9 +99,9 @@ public async Task Parse_WithForce_AndATargetOption_Succeeds(string option) [Arguments("--preview", "--repair", "--to=2.1.40-preview")] public async Task Parse_WithTwoTargetOptions_Fails(params string[] options) { - var exception = await Assert.That(() => _ = SelfUpdateSettings.Parse(options)).Throws(); + var exception = Assert.Throws(() => _ = SelfUpdateSettings.Parse(options)); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); foreach (var option in options) { await Assert.That(exception.Message).Contains(option.Split('=')[0]); diff --git a/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs b/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs index bfe3d510..ad2ed9d2 100644 --- a/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Tool.Services; using Buildvana.Tool.Services.SelfUpdate; @@ -48,8 +49,9 @@ public async Task Resolve_WithRepair_AndNoManifestEntry_Fails() var exception = await Assert .That(async () => _ = await Resolve(source, request, NoEntry).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("--repair"); + await Assert.That(exception.Message).Contains("--repair"); await Assert.That(exception.Message).Contains("dotnet-tools.json"); await Assert.That(source.Asked.Count).IsEqualTo(0); } @@ -186,11 +188,10 @@ public async Task Resolve_WithoutSources_Fails() { var source = new FakePackageVersionSource { Sources = [] }; - var exception = await Assert + await Assert .That(async () => _ = await Resolve(source, SelfUpdateRequest.Default, Pin("2.1.8")).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("No package source is configured"); + .Throws() + .WithMessageContaining("No package source is configured", StringComparison.Ordinal); } // A source that knows nothing of bv is a misconfigured source, and the message names the sources so @@ -203,8 +204,9 @@ public async Task Resolve_WhenNoSourceKnowsBv_Fails() var exception = await Assert .That(async () => _ = await Resolve(source, SelfUpdateRequest.Default, Pin("2.1.8")).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("No configured package source knows bv"); + await Assert.That(exception.Message).Contains("No configured package source knows bv"); await Assert.That(exception.Message).Contains("nuget.org, private"); } diff --git a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs index 78aaa8be..e2570f9a 100644 --- a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; @@ -37,9 +38,9 @@ public async Task EnsureSdkVersionMatch_WithMismatchedPin_FailsNamingBothVersion WriteGlobalJson(home, pin); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert.That(service.EnsureSdkVersionMatch).Throws(); + var exception = Assert.Throws(service.EnsureSdkVersionMatch); - await Assert.That(exception!.Message).Contains(pin); + await Assert.That(exception.Message).Contains(pin); await Assert.That(exception.Message).Contains("2.1.41-preview"); await Assert.That(exception.Message).Contains("dotnet bv self-update --repair"); } @@ -50,9 +51,9 @@ public async Task EnsureSdkVersionMatch_WithoutGlobalJson_Fails() using var home = new TempHome(); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert.That(service.EnsureSdkVersionMatch).Throws(); + var exception = Assert.Throws(service.EnsureSdkVersionMatch); - await Assert.That(exception!.Message).Contains("global.json"); + await Assert.That(exception.Message).Contains("global.json"); await Assert.That(exception.Message).Contains("dotnet bv self-update --repair"); } @@ -63,9 +64,9 @@ public async Task EnsureSdkVersionMatch_WithoutSdksSection_Fails() home.WriteFile("global.json", """{ "sdk": { "version": "10.0.302" } }"""); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert.That(service.EnsureSdkVersionMatch).Throws(); - - await Assert.That(exception!.Message).Contains("msbuild-sdks"); + await Assert.That(service.EnsureSdkVersionMatch) + .Throws() + .WithMessageContaining("msbuild-sdks", StringComparison.Ordinal); } [Test] @@ -75,9 +76,9 @@ public async Task EnsureSdkVersionMatch_WithoutPinEntry_Fails() home.WriteFile("global.json", """{ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.134" } }"""); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert.That(service.EnsureSdkVersionMatch).Throws(); - - await Assert.That(exception!.Message).Contains("Buildvana.Sdk"); + await Assert.That(service.EnsureSdkVersionMatch) + .Throws() + .WithMessageContaining("Buildvana.Sdk", StringComparison.Ordinal); } [Test] @@ -87,9 +88,9 @@ public async Task EnsureSdkVersionMatch_WithUnparseablePin_Fails() WriteGlobalJson(home, "not-a-version"); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert.That(service.EnsureSdkVersionMatch).Throws(); - - await Assert.That(exception!.Message).Contains("not-a-version"); + await Assert.That(service.EnsureSdkVersionMatch) + .Throws() + .WithMessageContaining("not-a-version", StringComparison.Ordinal); } // No file changes, but the dotnet CLI still runs: a matching manifest pin proves neither that the @@ -203,11 +204,10 @@ public async Task UpdateRepository_WhenTemplateCreatesNoManifest_FailsBeforeChan var runner = new FakeProcessRunner(); var service = CreateService(home, "2.1.41-preview", runner); - var exception = await Assert + await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("'dotnet new tool-manifest' did not create dotnet-tools.json"); + .Throws() + .WithMessageContaining("'dotnet new tool-manifest' did not create dotnet-tools.json", StringComparison.Ordinal); await Assert.That(runner.Runs.Count).IsEqualTo(1); await Assert.That(home.ReadFile("global.json")).IsEqualTo(before); } @@ -357,8 +357,9 @@ public async Task UpdateRepository_WithDeniedGlobalJsonWrite_Fails() var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("Could not write to"); + await Assert.That(exception.Message).Contains("Could not write to"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -378,8 +379,9 @@ public async Task UpdateRepository_WithNewerPin_FailsWithoutForce(string manifes var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, To("2.1.41-preview")).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("downgrade"); + await Assert.That(exception.Message).Contains("downgrade"); await Assert.That(exception.Message).Contains("dotnet bv self-update --to "); await Assert.That(exception.Message).Contains("--force"); await Assert.That(exception.Message).Contains(offender); @@ -401,8 +403,9 @@ public async Task UpdateRepository_ByDefault_WithNewerSdkPin_FailsWithoutForce() var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).StartsWith("The latest bv on the package sources is 2.1.41-preview, but "); + await Assert.That(exception.Message).StartsWith("The latest bv on the package sources is 2.1.41-preview, but "); await Assert.That(exception.Message).Contains("global.json pins Buildvana.Sdk 2.1.42-preview"); await Assert.That(runner.Runs.Count).IsEqualTo(0); } @@ -425,13 +428,13 @@ public async Task UpdateRepository_WithThreeNewerPins_ListsThemReadably() home.WriteFile("Directory.Packages.props", project); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert + await Assert .That(async () => _ = await UpdateInPlaceAsync(service, To("2.1.41-preview")).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains( - "the tool manifest pins bv 2.1.42-preview, global.json pins Buildvana.Sdk 2.1.42-preview, " - + "and Directory.Packages.props pins Buildvana.Runtime 2.1.42-preview"); + .Throws() + .WithMessageContaining( + "the tool manifest pins bv 2.1.42-preview, global.json pins Buildvana.Sdk 2.1.42-preview, " + + "and Directory.Packages.props pins Buildvana.Runtime 2.1.42-preview", + StringComparison.Ordinal); } [Test] @@ -510,8 +513,9 @@ public async Task UpdateRepository_WithOlderToVersion_FailsWithoutForce_NamingTh var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, To("2.1.40-preview")).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("--to"); + await Assert.That(exception.Message).Contains("--to"); await Assert.That(exception.Message).Contains("2.1.40-preview"); await Assert.That(exception.Message).Contains("downgrade"); await Assert.That(runner.Runs.Count).IsEqualTo(0); @@ -695,11 +699,10 @@ public async Task UpdateRepository_WithRepair_AndNoManifestEntry_FailsBeforeChan var runner = new FakeProcessRunner(); var service = CreateService(home, "2.1.41-preview", runner); - var exception = await Assert + await Assert .That(async () => _ = await UpdateInPlaceAsync(service, new SelfUpdateRequest { Repair = true }).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("--repair"); + .Throws() + .WithMessageContaining("--repair", StringComparison.Ordinal); await Assert.That(runner.Runs.Count).IsEqualTo(0); await Assert.That(home.ReadFile("global.json")).IsEqualTo(before); } @@ -762,8 +765,9 @@ public async Task UpdateRepository_WithNewerFamilyPin_FailsWithoutForce() var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains("Directory.Packages.props pins Buildvana.Runtime 2.1.42-preview"); + await Assert.That(exception.Message).Contains("Directory.Packages.props pins Buildvana.Runtime 2.1.42-preview"); await Assert.That(exception.Message).Contains("downgrade"); await Assert.That(runner.Runs.Count).IsEqualTo(0); await Assert.That(home.ReadFile("Directory.Packages.props")).IsEqualTo(project); @@ -786,8 +790,9 @@ public async Task UpdateRepository_WithUnusableManifestEntry_FailsBeforeChanging var exception = await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) .Throws(); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); - await Assert.That(exception!.Message).Contains(expectedDetail); + await Assert.That(exception.Message).Contains(expectedDetail); await Assert.That(exception.Message).Contains("dotnet-tools.json"); await Assert.That(runner.Runs.Count).IsEqualTo(0); await Assert.That(home.ReadFile("global.json")).IsEqualTo(before); @@ -805,11 +810,10 @@ public async Task UpdateRepository_WithManifestUnderDotConfig_FailsBeforeChangin var runner = new FakeProcessRunner(); var service = CreateService(home, "2.1.41-preview", runner); - var exception = await Assert + await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("git mv .config/dotnet-tools.json dotnet-tools.json"); + .Throws() + .WithMessageContaining("git mv .config/dotnet-tools.json dotnet-tools.json", StringComparison.Ordinal); await Assert.That(runner.Runs.Count).IsEqualTo(0); await Assert.That(home.ReadFile("global.json")).IsEqualTo(before); } @@ -965,11 +969,10 @@ public async Task UpdateRepository_WithMultipleConfigFiles_Fails() home.WriteFile("buildvana.jsonc", "{}\n"); var service = CreateService(home, "2.1.41-preview"); - var exception = await Assert + await Assert .That(async () => _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false)) - .Throws(); - - await Assert.That(exception!.Message).Contains("Multiple"); + .Throws() + .WithMessageContaining("Multiple", StringComparison.Ordinal); } // The sources list the running bv's own version and nothing else, unless a test scripts them: the default diff --git a/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs b/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs index 92e518e9..8772c144 100644 --- a/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs +++ b/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs @@ -32,8 +32,9 @@ public async Task EnsureReleaseCommit_AfterPushingUpdates_Throws() void Act() => release.EnsureReleaseCommit(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("already been pushed"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("already been pushed", StringComparison.Ordinal); } } @@ -50,8 +51,9 @@ public async Task UpdateRepository_AfterPushingUpdates_Throws() void Act() => release.UpdateRepository("docs/late.md"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("already been pushed"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("already been pushed", StringComparison.Ordinal); } } @@ -68,8 +70,9 @@ public async Task UpdateRepository_AfterAPostReleaseCommit_Throws() void Act() => release.UpdateRepository("docs/late.md"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("after a post-release commit"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("after a post-release commit", StringComparison.Ordinal); } } @@ -86,8 +89,9 @@ public async Task AddPostReleaseCommit_AfterPushingUpdates_Throws() void Act() => release.AddPostReleaseCommit("Post-release updates [skip ci]", "docs/late.md"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("already been pushed"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("already been pushed", StringComparison.Ordinal); } } @@ -102,8 +106,9 @@ public async Task AddPostReleaseCommit_BeforeTheReleaseCommit_Throws() void Act() => release.AddPostReleaseCommit("Post-release updates [skip ci]", "docs/early.md"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("before the release commit"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("before the release commit", StringComparison.Ordinal); } } @@ -117,8 +122,9 @@ public async Task PushUpdates_WithoutAReleaseCommit_Throws() { void Act() => release.PushUpdates(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("before the release commit"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("before the release commit", StringComparison.Ordinal); } await Assert.That(harness.Repo.GetRemoteTipSha()).IsNull(); @@ -158,8 +164,9 @@ public async Task AddAsset_AfterPushingUpdates_Throws() void Act() => release.AddAsset("artifacts/late.bin"); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("already been pushed"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("already been pushed", StringComparison.Ordinal); } } @@ -191,8 +198,9 @@ public async Task PublishAsync_BeforePushingUpdates_Throws() Task Act() => release.PublishAsync(); - var exception = await Assert.That(Act).Throws(); - await Assert.That(exception!.Message).Contains("before pushing updates"); + await Assert.That(Act) + .Throws() + .WithMessageContaining("before pushing updates", StringComparison.Ordinal); await Assert.That(harness.Publisher!.IsPublished).IsFalse(); } } diff --git a/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs b/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs index 5ffdc0b0..c4c7ee4d 100644 --- a/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text.Json; using Buildvana.Core; using Buildvana.Core.ConsoleOutput; @@ -87,7 +88,8 @@ public async Task ReadAsync_WhenEvaluationFails_ReportsAFailedStep() var reader = CreateReader(home, runner); var exception = await Assert.That(async () => await reader.ReadAsync([ProjectPath]).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.ExitCode).IsEqualTo(3); } [Test] @@ -98,7 +100,8 @@ public async Task ReadAsync_WithAnUnreadableDump_ReportsAFailedStep() var reader = CreateReader(home, runner); var exception = await Assert.That(async () => await reader.ReadAsync([ProjectPath]).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(3); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.ExitCode).IsEqualTo(3); } private static SolutionPinReader CreateReader(TempHome home, IProcessRunner runner) diff --git a/tests/Buildvana.Tool.Tests/ToolPinReaderTests.cs b/tests/Buildvana.Tool.Tests/ToolPinReaderTests.cs index c05b3ab0..4fd878da 100644 --- a/tests/Buildvana.Tool.Tests/ToolPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/ToolPinReaderTests.cs @@ -71,9 +71,9 @@ public async Task Read_WithAManifestUnderDotConfig_NamesEachOneAndItsMove() home.WriteFile("docs/.config/dotnet-tools.json", """{ "tools": { } }"""); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(() => CreateReader(home).Read()).Throws(); + var exception = Assert.Throws(() => CreateReader(home).Read()); - await Assert.That(exception!.Message).Contains("git mv .config/dotnet-tools.json dotnet-tools.json"); + await Assert.That(exception.Message).Contains("git mv .config/dotnet-tools.json dotnet-tools.json"); await Assert.That(exception.Message).Contains("git mv docs/.config/dotnet-tools.json docs/dotnet-tools.json"); } @@ -88,9 +88,9 @@ public async Task Read_WithAManifestUnderDotConfigNextToOne_NamesTheMerge() home.WriteFile("docs/.config/dotnet-tools.json", """{ "tools": { } }"""); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(() => CreateReader(home).Read()).Throws(); + var exception = Assert.Throws(() => CreateReader(home).Read()); - await Assert.That(exception!.Message) + await Assert.That(exception.Message) .Contains("merge the tools of .config/dotnet-tools.json into dotnet-tools.json, then delete .config/dotnet-tools.json"); await Assert.That(exception.Message).Contains("git mv docs/.config/dotnet-tools.json docs/dotnet-tools.json"); } @@ -105,9 +105,9 @@ public async Task Read_WithAManifestUnderDotConfigAndAnUnparseableOne_NamesTheMo home.WriteFile("dotnet-tools.json", "{ not json"); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning - var exception = await Assert.That(() => CreateReader(home).Read()).Throws(); - - await Assert.That(exception!.Message).Contains("git mv docs/.config/dotnet-tools.json docs/dotnet-tools.json"); + await Assert.That(() => CreateReader(home).Read()) + .Throws() + .WithMessageContaining("git mv docs/.config/dotnet-tools.json docs/dotnet-tools.json", StringComparison.Ordinal); } // The bv entry of the home directory's manifest moves with the family, so bv dependencies never sees it. diff --git a/tests/Buildvana.Tool.Tests/ToolPinUpdaterTests.cs b/tests/Buildvana.Tool.Tests/ToolPinUpdaterTests.cs index 4662eacb..86d91038 100644 --- a/tests/Buildvana.Tool.Tests/ToolPinUpdaterTests.cs +++ b/tests/Buildvana.Tool.Tests/ToolPinUpdaterTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core; using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; @@ -64,7 +65,8 @@ public async Task UpdateAsync_WhenAnUpdateFails_StopsThere() // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning var exception = await Assert.That(async () => await UpdateAsync(home, runner, pins).ConfigureAwait(false)) .Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); + Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.ExternalProgramFailed); await Assert.That(runner.Runs.Count).IsEqualTo(1); } diff --git a/tests/Buildvana.Tool.Tests/UrlMatchingReleaseRemoteResolverTests.cs b/tests/Buildvana.Tool.Tests/UrlMatchingReleaseRemoteResolverTests.cs index a7f1b944..622e2af0 100644 --- a/tests/Buildvana.Tool.Tests/UrlMatchingReleaseRemoteResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/UrlMatchingReleaseRemoteResolverTests.cs @@ -39,8 +39,8 @@ public async Task Resolve_WithAnSshRemoteAlone_Throws() using var repo = CreateRepo(("origin", "ssh://git@github.com/Tenacom/Buildvana.git")); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before repo is disposed - var exception = await Assert.That(() => Resolve(repo, RepositoryUrl)).Throws(); - await Assert.That(exception!.Message).Contains("Remote 'origin' names repository.url"); + var exception = Assert.Throws(() => Resolve(repo, RepositoryUrl)); + await Assert.That(exception.Message).Contains("Remote 'origin' names repository.url"); await Assert.That(exception.Message).Contains("HTTP and HTTPS"); } @@ -50,8 +50,8 @@ public async Task Resolve_WithoutAMatchingRemote_ThrowsNamingTheRemotes() using var repo = CreateRepo(("origin", "https://github.com/someone/Buildvana")); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before repo is disposed - var exception = await Assert.That(() => Resolve(repo, RepositoryUrl)).Throws(); - await Assert.That(exception!.Message).Contains($"No remote of the clone names repository.url '{RepositoryUrl}'"); + var exception = Assert.Throws(() => Resolve(repo, RepositoryUrl)); + await Assert.That(exception.Message).Contains($"No remote of the clone names repository.url '{RepositoryUrl}'"); await Assert.That(exception.Message).Contains("'origin' (https://github.com/someone/Buildvana)"); } @@ -61,8 +61,9 @@ public async Task Resolve_WithoutRemotes_Throws() using var repo = CreateRepo(); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before repo is disposed - var exception = await Assert.That(() => Resolve(repo, RepositoryUrl)).Throws(); - await Assert.That(exception!.Message).Contains("The clone has no remotes."); + await Assert.That(() => Resolve(repo, RepositoryUrl)) + .Throws() + .WithMessageContaining("The clone has no remotes.", StringComparison.Ordinal); } [Test] @@ -71,8 +72,9 @@ public async Task Resolve_WithoutRepositoryUrl_Throws() using var repo = CreateRepo(("origin", RepositoryUrl)); // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before repo is disposed - var exception = await Assert.That(() => Resolve(repo, null)).Throws(); - await Assert.That(exception!.Message).Contains("repository.url"); + await Assert.That(() => Resolve(repo, null)) + .Throws() + .WithMessageContaining("repository.url", StringComparison.Ordinal); } private static TempGitRepo CreateRepo(params (string Name, string Url)[] remotes) diff --git a/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs b/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs index 99a72b4a..cb75f839 100644 --- a/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs +++ b/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs @@ -68,7 +68,7 @@ private static void ParseCommandLine(string commandLine) private static async Task AssertUsageExitCode(Action action) { - var exception = await Assert.That(action).Throws(); - await Assert.That(exception!.ExitCode).IsEqualTo(ExitCodes.Usage); + var exception = Assert.Throws(action); + await Assert.That(exception.ExitCode).IsEqualTo(ExitCodes.Usage); } } diff --git a/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs b/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs index d06ece7b..d5c96b63 100644 --- a/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs +++ b/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs @@ -29,9 +29,11 @@ public async Task Parse_KnownLevel_ReturnsVerbosity(string raw, Verbosity expect [Test] public async Task Parse_UnknownLevel_FailsNamingValueAndAlternatives() { - var exception = await Assert.That(() => VerbosityParser.Parse("loud")).Throws(); - await Assert.That(exception!.Message) - .IsEqualTo("Unknown verbosity level 'loud'. Use one of: [q]uiet, [m]inimal, [n]ormal, [d]etailed, [diag]nostic."); + await Assert.That(() => VerbosityParser.Parse("loud")) + .Throws() + .WithMessage( + "Unknown verbosity level 'loud'. Use one of: [q]uiet, [m]inimal, [n]ormal, [d]etailed, [diag]nostic.", + StringComparison.Ordinal); } [Test] From 5a51f6b7bf4e36be15c51f1a5c31bc7d46cc50ca Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 01:43:11 +0100 Subject: [PATCH 06/11] Remove the null-forgiving operators from JsonNode lookups in tests Assert intermediate nodes with IsNotNull(), use ?. for value comparisons. Synchronous helpers pass the null on, or throw. Use Debug.Assert as a last resort. Co-Authored-By: Claude Opus 5.5 --- .../BuildvanaJsonConfigExampleTests.cs | 6 +- .../BuildvanaJsonConfigSchemaTests.cs | 29 +- .../ConfigurationPageTests.cs | 9 +- .../RepositoryConfigFilesTests.cs | 10 +- .../JsonSchemaGeneratorTests.cs | 248 +++++++++--------- .../JsonSchemaValidatorTests.cs | 8 +- 6 files changed, 161 insertions(+), 149 deletions(-) diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs index ec92f404..bbdd4d64 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigExampleTests.cs @@ -317,7 +317,7 @@ public async Task ResolveReference_FollowsADocumentLocalPointer() var resolved = BuildvanaJsonConfigExample.ResolveReference(Reference("#/properties/args"), root, "args"); - await Assert.That(resolved["type"]!.GetValue()).IsEqualTo("array"); + await Assert.That(resolved["type"]?.GetValue()).IsEqualTo("array"); } // A member name holding a slash or a tilde is escaped in a pointer, and has to survive the trip back. @@ -329,8 +329,8 @@ public async Task ResolveReference_UnescapesPointerTokens() var slash = BuildvanaJsonConfigExample.ResolveReference(Reference("#/properties/a~1b"), root, "a/b"); var tilde = BuildvanaJsonConfigExample.ResolveReference(Reference("#/properties/c~0d"), root, "c~d"); - await Assert.That(slash["type"]!.GetValue()).IsEqualTo("string"); - await Assert.That(tilde["type"]!.GetValue()).IsEqualTo("boolean"); + await Assert.That(slash["type"]?.GetValue()).IsEqualTo("string"); + await Assert.That(tilde["type"]?.GetValue()).IsEqualTo("boolean"); } [Test] diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigSchemaTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigSchemaTests.cs index af458dbd..8e4807b4 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigSchemaTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigSchemaTests.cs @@ -10,13 +10,13 @@ // kind added to either enum without a matching schema value fails here. internal sealed class BuildvanaJsonConfigSchemaTests { - private static readonly JsonNode Dependencies - = JsonNode.Parse(BuildvanaJsonConfigSchema.Generate())!["properties"]!["dependencies"]!["properties"]!; + private static readonly JsonNode? Dependencies + = JsonNode.Parse(BuildvanaJsonConfigSchema.Generate())?["properties"]?["dependencies"]?["properties"]; [Test] public async Task Generate_NetSdkScope_EnumeratesEveryNetSdkPolicyString() { - var netSdk = AllowedValues(Dependencies["scopes"]!["properties"]!["netsdk"]); + var netSdk = AllowedValues(Dependencies?["scopes"]?["properties"]?["netsdk"]); await Assert.That(netSdk.SequenceEqual(NetSdkPolicyStrings())).IsTrue(); } @@ -25,19 +25,19 @@ public async Task Generate_NetSdkScope_EnumeratesEveryNetSdkPolicyString() public async Task Generate_PackagePositions_EnumerateEveryPackagePolicyString() { var expected = PackagePolicyStrings(); - var scopes = Dependencies["scopes"]!["properties"]!; + var scopes = Dependencies?["scopes"]?["properties"]; - await Assert.That(AllowedValues(scopes["sdks"]).SequenceEqual(expected)).IsTrue(); - await Assert.That(AllowedValues(scopes["tools"]).SequenceEqual(expected)).IsTrue(); - await Assert.That(AllowedValues(scopes["packages"]).SequenceEqual(expected)).IsTrue(); - await Assert.That(AllowedValues(Dependencies["policies"]!["additionalProperties"]).SequenceEqual(expected)).IsTrue(); - await Assert.That(AllowedValues(GroupProperties()["policy"]).SequenceEqual(expected)).IsTrue(); + await Assert.That(AllowedValues(scopes?["sdks"]).SequenceEqual(expected)).IsTrue(); + await Assert.That(AllowedValues(scopes?["tools"]).SequenceEqual(expected)).IsTrue(); + await Assert.That(AllowedValues(scopes?["packages"]).SequenceEqual(expected)).IsTrue(); + await Assert.That(AllowedValues(Dependencies?["policies"]?["additionalProperties"]).SequenceEqual(expected)).IsTrue(); + await Assert.That(AllowedValues(GroupProperties()?["policy"]).SequenceEqual(expected)).IsTrue(); } // The caption travels as the member name, so the value object must not restate it. [Test] public async Task Generate_AdditionalPackageGroup_ForbidsTheCaptionInsideTheValue() - => await Assert.That(GroupProperties()["caption"]!.GetValueKind()).IsEqualTo(JsonValueKind.False); + => await Assert.That(GroupProperties()?["caption"]?.GetValueKind()).IsEqualTo(JsonValueKind.False); private static List NetSdkPolicyStrings() { @@ -63,9 +63,10 @@ private static List PackagePolicyStrings() return strings; } - private static JsonNode GroupProperties() - => Dependencies["additionalPackages"]!["additionalProperties"]!["properties"]!; + private static JsonNode? GroupProperties() + => Dependencies?["additionalPackages"]?["additionalProperties"]?["properties"]; - private static List AllowedValues(JsonNode? schema) - => [.. ((JsonArray)schema!["enum"]!).Select(static value => value!.GetValue())]; + // A missing node yields no values, and a null element a null string, so either fails the comparison. + private static List AllowedValues(JsonNode? schema) + => [.. schema?["enum"]?.AsArray().Select(static value => value?.GetValue()) ?? []]; } diff --git a/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs b/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs index 14b57669..e527673f 100644 --- a/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs @@ -70,10 +70,15 @@ private static DocumentationPage LoadPage() // says whether the member is a section. private static IEnumerable<(string Name, string? Default)> Settings(JsonObject schema, string path) { - foreach (var (name, node) in (JsonObject)schema["properties"]!) + if (schema["properties"] is not JsonObject properties) + { + yield break; + } + + foreach (var (name, node) in properties) { - var member = (JsonObject)node!; var memberPath = path.Length == 0 ? name : $"{path}.{name}"; + var member = node?.AsObject() ?? throw new InvalidOperationException($"The schema states null for {memberPath}."); var declaring = member["$ref"] is null ? member : BuildvanaJsonConfigExample.ResolveReference(member, Schema, memberPath); diff --git a/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs b/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs index 09b607da..f4ef90ce 100644 --- a/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs @@ -156,25 +156,27 @@ private static bool IsSchemaMember(string line) private static string SchemaUrlFor(string reference) => $"https://raw.githubusercontent.com/Tenacom/Buildvana/{reference}/schemas/buildvana.schema.json"; - private static async Task SchemaReferenceOfAsync(string fileName) + private static async Task SchemaReferenceOfAsync(string fileName) { var file = await ReadJsonAsync(fileName).ConfigureAwait(false); - return file[SchemaMemberName]!.GetValue(); + return file[SchemaMemberName]?.GetValue(); } private static async Task PinnedToolVersionAsync() { var manifest = await ReadJsonAsync(ToolManifestFileName).ConfigureAwait(false); + var version = await Assert.That(manifest["tools"]?[ToolPackageId]?["version"]).IsNotNull(); - return manifest["tools"]![ToolPackageId]!["version"]!.GetValue(); + return version.GetValue(); } private static async Task ReadJsonAsync(string fileName) { var text = await File.ReadAllTextAsync(PathOf(fileName)).ConfigureAwait(false); + var node = await Assert.That(JsonNode.Parse(text, documentOptions: ReaderOptions)).IsNotNull(); - return (JsonObject)JsonNode.Parse(text, documentOptions: ReaderOptions)!; + return node.AsObject(); } // Every "description" keyword in a schema document, wherever it sits. diff --git a/tests/Buildvana.Core.Json.Tests/JsonSchemaGeneratorTests.cs b/tests/Buildvana.Core.Json.Tests/JsonSchemaGeneratorTests.cs index 8ce574ac..81d82d8c 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonSchemaGeneratorTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonSchemaGeneratorTests.cs @@ -26,48 +26,47 @@ internal sealed class JsonSchemaGeneratorTests public async Task Generate_EmitsDialectAndTitle() { var schema = Generate(); - await Assert.That(schema["$schema"]!.GetValue()) + await Assert.That(schema["$schema"]?.GetValue()) .IsEqualTo("https://json-schema.org/draft/2020-12/schema"); - await Assert.That(schema["title"]!.GetValue()).IsEqualTo("Sample Title"); + await Assert.That(schema["title"]?.GetValue()).IsEqualTo("Sample Title"); } [Test] public async Task Generate_StripsNullFromPlainNullableProperty() { - var type = Generate()["properties"]!["plain"]!["type"]; - await Assert.That(type!.GetValueKind()).IsEqualTo(JsonValueKind.String); + var type = await Assert.That(Generate()["properties"]?["plain"]?["type"]).IsNotNull(); + await Assert.That(type.GetValueKind()).IsEqualTo(JsonValueKind.String); await Assert.That(type.GetValue()).IsEqualTo("string"); } [Test] public async Task Generate_KeepsNullWhenPropertyIsJsonNullable() { - var type = Generate()["properties"]!["maybe"]!["type"]; - await Assert.That(type is JsonArray).IsTrue(); - await Assert.That(((JsonArray)type!).Count).IsEqualTo(2); + var type = await Assert.That(Generate()["properties"]?["maybe"]?["type"]).IsNotNull(); + await Assert.That(type.AsArray().Count).IsEqualTo(2); } [Test] public async Task Generate_SurfacesDescription() { - var description = Generate()["properties"]!["described"]!["description"]; - await Assert.That(description!.GetValue()).IsEqualTo("a described field"); + var description = Generate()["properties"]?["described"]?["description"]; + await Assert.That(description?.GetValue()).IsEqualTo("a described field"); } [Test] public async Task Generate_SurfacesExample() { - var examples = (JsonArray)Generate()["properties"]!["sampled"]!["examples"]!; - await Assert.That(examples.Count).IsEqualTo(1); - await Assert.That(examples[0]!.ToJsonString()).IsEqualTo("""["alpha","beta"]"""); + var examples = await Assert.That(Generate()["properties"]?["sampled"]?["examples"]).IsNotNull(); + await Assert.That(examples.AsArray().Count).IsEqualTo(1); + await Assert.That(examples[0]?.ToJsonString()).IsEqualTo("""["alpha","beta"]"""); } // Both annotations insert at the head of the node, so the one applied last comes first. [Test] public async Task Generate_PutsDescriptionBeforeExample() { - var sampled = (JsonObject)Generate()["properties"]!["sampled"]!; - var firstTwo = sampled.Select(static member => member.Key).Take(2); + var sampled = await Assert.That(Generate()["properties"]?["sampled"]).IsNotNull(); + var firstTwo = sampled.AsObject().Select(static member => member.Key).Take(2); await Assert.That(firstTwo.SequenceEqual(["description", "examples"])).IsTrue(); } @@ -79,20 +78,20 @@ public async Task Generate_ThrowsOnUnparseableExample() [Test] public async Task Generate_ConstrainsDictionaryToAllowedKeys() { - var map = Generate()["properties"]!["map"]!; - await Assert.That(map["additionalProperties"]!.GetValue()).IsFalse(); - await Assert.That(map["properties"]!["alpha"]).IsNotNull(); - await Assert.That(map["properties"]!["beta"]).IsNotNull(); - await Assert.That((map["properties"] as JsonObject)!.Count).IsEqualTo(2); + var map = await Assert.That(Generate()["properties"]?["map"]).IsNotNull(); + await Assert.That(map["additionalProperties"]?.GetValue()).IsFalse(); + await Assert.That(map["properties"]?["alpha"]).IsNotNull(); + await Assert.That(map["properties"]?["beta"]).IsNotNull(); + await Assert.That(map["properties"]?.AsObject().Count).IsEqualTo(2); } [Test] public async Task Generate_ConstrainsStringToAllowedValues() { - var choice = Generate()["properties"]!["choice"]!; - await Assert.That(choice["type"]!.GetValue()).IsEqualTo("string"); - var allowed = ((JsonArray)choice["enum"]!).Select(static value => value!.GetValue()); - await Assert.That(allowed.SequenceEqual(["alpha", "beta"])).IsTrue(); + var choice = await Assert.That(Generate()["properties"]?["choice"]).IsNotNull(); + await Assert.That(choice["type"]?.GetValue()).IsEqualTo("string"); + var allowed = await Assert.That(choice["enum"]).IsNotNull(); + await Assert.That(allowed.AsArray().Select(static value => value?.GetValue()).SequenceEqual(["alpha", "beta"])).IsTrue(); } // An enumerated set is the whole value space, so a member that keeps "null" in its type has to keep null @@ -100,12 +99,12 @@ public async Task Generate_ConstrainsStringToAllowedValues() [Test] public async Task Generate_KeepsNullInAllowedValuesOfANullableMember() { - var choice = Generate()["properties"]!["maybeChoice"]!; - var type = (JsonArray)choice["type"]!; - await Assert.That(type.Select(static t => t!.GetValue()).SequenceEqual(["string", "null"])).IsTrue(); + var choice = await Assert.That(Generate()["properties"]?["maybeChoice"]).IsNotNull(); + var type = await Assert.That(choice["type"]).IsNotNull(); + await Assert.That(type.AsArray().Select(static t => t?.GetValue()).SequenceEqual(["string", "null"])).IsTrue(); - var allowed = (JsonArray)choice["enum"]!; - await Assert.That(allowed.Count).IsEqualTo(3); + var allowed = await Assert.That(choice["enum"]).IsNotNull(); + await Assert.That(allowed.AsArray().Count).IsEqualTo(3); await Assert.That(allowed[2]).IsNull(); } @@ -114,46 +113,44 @@ public async Task Generate_KeepsNullInAllowedValuesOfANullableMember() [Test] public async Task Generate_AllowedValuesReplaceNonBlankConstraints() { - var properties = Generate()["properties"]!; - var enumerated = properties["requiredChoice"]!; + var properties = await Assert.That(Generate()["properties"]).IsNotNull(); + var enumerated = await Assert.That(properties["requiredChoice"]).IsNotNull(); await Assert.That(enumerated["enum"]).IsNotNull(); await Assert.That(enumerated["minLength"]).IsNull(); await Assert.That(enumerated["pattern"]).IsNull(); - var open = properties["requiredText"]!; - await Assert.That(open["minLength"]!.GetValue()).IsEqualTo(1); - await Assert.That(open["pattern"]!.GetValue()).IsEqualTo(@"\S"); + var open = await Assert.That(properties["requiredText"]).IsNotNull(); + await Assert.That(open["minLength"]?.GetValue()).IsEqualTo(1); + await Assert.That(open["pattern"]?.GetValue()).IsEqualTo(@"\S"); } [Test] public async Task Generate_KeepsNullOnNullableDictionaryValue() { - var type = Generate()["properties"]!["env"]!["additionalProperties"]!["type"]; - await Assert.That(type is JsonArray).IsTrue(); - await Assert.That(((JsonArray)type!).Count).IsEqualTo(2); + var type = await Assert.That(Generate()["properties"]?["env"]?["additionalProperties"]?["type"]).IsNotNull(); + await Assert.That(type.AsArray().Count).IsEqualTo(2); } [Test] public async Task Generate_StripsNullFromNonNullableDictionaryValue() { - var type = Generate()["properties"]!["vars"]!["additionalProperties"]!["type"]; - await Assert.That(type!.GetValueKind()).IsEqualTo(JsonValueKind.String); + var type = await Assert.That(Generate()["properties"]?["vars"]?["additionalProperties"]?["type"]).IsNotNull(); + await Assert.That(type.GetValueKind()).IsEqualTo(JsonValueKind.String); await Assert.That(type.GetValue()).IsEqualTo("string"); } [Test] public async Task Generate_KeepsNullOnNullableArrayElement() { - var type = Generate()["properties"]!["items"]!["items"]!["type"]; - await Assert.That(type is JsonArray).IsTrue(); - await Assert.That(((JsonArray)type!).Count).IsEqualTo(2); + var type = await Assert.That(Generate()["properties"]?["items"]?["items"]?["type"]).IsNotNull(); + await Assert.That(type.AsArray().Count).IsEqualTo(2); } [Test] public async Task Generate_StripsNullFromNonNullableArrayElement() { - var type = Generate()["properties"]!["tags"]!["items"]!["type"]; - await Assert.That(type!.GetValueKind()).IsEqualTo(JsonValueKind.String); + var type = await Assert.That(Generate()["properties"]?["tags"]?["items"]?["type"]).IsNotNull(); + await Assert.That(type.GetValueKind()).IsEqualTo(JsonValueKind.String); await Assert.That(type.GetValue()).IsEqualTo("string"); } @@ -161,16 +158,17 @@ public async Task Generate_StripsNullFromNonNullableArrayElement() public async Task Generate_WithoutDefaults_EmitsNoDefaults() { var schema = JsonSchemaGenerator.Generate(Options); - await Assert.That(schema["properties"]!["text"]!["default"]).IsNull(); + var text = await Assert.That(schema["properties"]?["text"]).IsNotNull(); + await Assert.That(text["default"]).IsNull(); } // The schema comes from one type, the defaults from another: matching is by JSON name alone. [Test] public async Task Generate_WithDefaults_AnnotatesLeafProperties() { - var properties = GenerateWithDefaults()["properties"]!; - await Assert.That(properties["text"]!["default"]!.GetValue()).IsEqualTo("hello"); - await Assert.That(properties["flag"]!["default"]!.GetValue()).IsTrue(); + var properties = await Assert.That(GenerateWithDefaults()["properties"]).IsNotNull(); + await Assert.That(properties["text"]?["default"]?.GetValue()).IsEqualTo("hello"); + await Assert.That(properties["flag"]?["default"]?.GetValue()).IsTrue(); } // The enum default renders exactly as the serializer options render the value ("two", not "Two" or 1), @@ -178,21 +176,21 @@ public async Task Generate_WithDefaults_AnnotatesLeafProperties() [Test] public async Task Generate_WithDefaults_RendersEnumDefaultsThroughOptions() { - var level = GenerateWithDefaults()["properties"]!["level"]!; - await Assert.That(level["default"]!.GetValue()).IsEqualTo("two"); + var level = await Assert.That(GenerateWithDefaults()["properties"]?["level"]).IsNotNull(); + await Assert.That(level["default"]?.GetValue()).IsEqualTo("two"); } [Test] public async Task Generate_WithDefaults_StatesNoDefaultForNullValues() { - var notStated = GenerateWithDefaults()["properties"]!["notStated"]!; + var notStated = await Assert.That(GenerateWithDefaults()["properties"]?["notStated"]).IsNotNull(); await Assert.That(notStated["default"]).IsNull(); } [Test] public async Task Generate_WithDefaults_HonorsNoDefaultAttribute() { - var dynamic = GenerateWithDefaults()["properties"]!["dynamic"]!; + var dynamic = await Assert.That(GenerateWithDefaults()["properties"]?["dynamic"]).IsNotNull(); await Assert.That(dynamic["default"]).IsNull(); } @@ -209,26 +207,26 @@ public async Task Generate_WithDefaults_ThrowsOnUnmatchedSchemaProperty() [Test] public async Task Generate_WithDefaults_StatesNoDefaultForCollections() { - var tags = GenerateWithDefaults()["properties"]!["tags"]!; + var tags = await Assert.That(GenerateWithDefaults()["properties"]?["tags"]).IsNotNull(); await Assert.That(tags["default"]).IsNull(); } [Test] public async Task Generate_WithDefaults_RecursesIntoSections() { - var section = GenerateWithDefaults()["properties"]!["section"]!; + var section = await Assert.That(GenerateWithDefaults()["properties"]?["section"]).IsNotNull(); await Assert.That(section["default"]).IsNull(); - await Assert.That(section["properties"]!["inner"]!["default"]!.GetValue()).IsEqualTo("nested-default"); + await Assert.That(section["properties"]?["inner"]?["default"]?.GetValue()).IsEqualTo("nested-default"); } [Test] public async Task Generate_EmitsRequiredForRequiredMembers() { var schema = JsonSchemaGenerator.Generate(Options); - var required = (JsonArray)schema["required"]!; - await Assert.That(required.Count).IsEqualTo(2); - await Assert.That(required[0]!.GetValue()).IsEqualTo("must"); - await Assert.That(required[1]!.GetValue()).IsEqualTo("mustFlag"); + var required = await Assert.That(schema["required"]).IsNotNull(); + await Assert.That(required.AsArray().Count).IsEqualTo(2); + await Assert.That(required[0]?.GetValue()).IsEqualTo("must"); + await Assert.That(required[1]?.GetValue()).IsEqualTo("mustFlag"); } // `required` only checks presence, so required strings are additionally constrained to non-blank @@ -236,15 +234,15 @@ public async Task Generate_EmitsRequiredForRequiredMembers() [Test] public async Task Generate_ConstrainsRequiredStringsToNonBlank() { - var must = JsonSchemaGenerator.Generate(Options)["properties"]!["must"]!; - await Assert.That(must["minLength"]!.GetValue()).IsEqualTo(1); - await Assert.That(must["pattern"]!.GetValue()).IsEqualTo(@"\S"); + var must = await Assert.That(JsonSchemaGenerator.Generate(Options)["properties"]?["must"]).IsNotNull(); + await Assert.That(must["minLength"]?.GetValue()).IsEqualTo(1); + await Assert.That(must["pattern"]?.GetValue()).IsEqualTo(@"\S"); } [Test] public async Task Generate_LeavesOptionalStringsUnconstrained() { - var may = JsonSchemaGenerator.Generate(Options)["properties"]!["may"]!; + var may = await Assert.That(JsonSchemaGenerator.Generate(Options)["properties"]?["may"]).IsNotNull(); await Assert.That(may["minLength"]).IsNull(); await Assert.That(may["pattern"]).IsNull(); } @@ -252,7 +250,7 @@ public async Task Generate_LeavesOptionalStringsUnconstrained() [Test] public async Task Generate_LeavesRequiredNonStringsUnconstrained() { - var mustFlag = JsonSchemaGenerator.Generate(Options)["properties"]!["mustFlag"]!; + var mustFlag = await Assert.That(JsonSchemaGenerator.Generate(Options)["properties"]?["mustFlag"]).IsNotNull(); await Assert.That(mustFlag["minLength"]).IsNull(); await Assert.That(mustFlag["pattern"]).IsNull(); } @@ -260,25 +258,25 @@ public async Task Generate_LeavesRequiredNonStringsUnconstrained() [Test] public async Task Generate_RendersValueShapeKeyedListAsObject() { - var policies = GenerateKeyed()["properties"]!["policies"]!; - await Assert.That(policies["type"]!.GetValue()).IsEqualTo("object"); + var policies = await Assert.That(GenerateKeyed()["properties"]?["policies"]).IsNotNull(); + await Assert.That(policies["type"]?.GetValue()).IsEqualTo("object"); // The value property's schema, required-string constraints included, becomes additionalProperties. - var values = policies["additionalProperties"]!; - await Assert.That(values["type"]!.GetValue()).IsEqualTo("string"); - await Assert.That(values["minLength"]!.GetValue()).IsEqualTo(1); + var values = await Assert.That(policies["additionalProperties"]).IsNotNull(); + await Assert.That(values["type"]?.GetValue()).IsEqualTo("string"); + await Assert.That(values["minLength"]?.GetValue()).IsEqualTo(1); } [Test] public async Task Generate_RendersRemainingMembersKeyedListAsObject() { - var groups = GenerateKeyed()["properties"]!["groups"]!; - await Assert.That(groups["type"]!.GetValue()).IsEqualTo("object"); + var groups = await Assert.That(GenerateKeyed()["properties"]?["groups"]).IsNotNull(); + await Assert.That(groups["type"]?.GetValue()).IsEqualTo("object"); - var element = groups["additionalProperties"]!; - await Assert.That(element["type"]!.GetValue()).IsEqualTo("object"); - await Assert.That(element["properties"]!["files"]).IsNotNull(); - await Assert.That(element["properties"]!["retries"]).IsNotNull(); + var element = await Assert.That(groups["additionalProperties"]).IsNotNull(); + await Assert.That(element["type"]?.GetValue()).IsEqualTo("object"); + await Assert.That(element["properties"]?["files"]).IsNotNull(); + await Assert.That(element["properties"]?["retries"]).IsNotNull(); } // The key travels as the JSON property name: the converter refuses an element value that restates it, @@ -286,8 +284,8 @@ public async Task Generate_RendersRemainingMembersKeyedListAsObject() [Test] public async Task Generate_ForbidsKeyInsideRemainingMembers() { - var element = GenerateKeyed()["properties"]!["groups"]!["additionalProperties"]!; - await Assert.That(element["properties"]!["caption"]!.GetValue()).IsFalse(); + var element = await Assert.That(GenerateKeyed()["properties"]?["groups"]?["additionalProperties"]).IsNotNull(); + await Assert.That(element["properties"]?["caption"]?.GetValue()).IsFalse(); } // The key travels as the member name, where a property's own constraints cannot reach it, so a required @@ -297,13 +295,13 @@ public async Task Generate_ConstrainsRequiredKeyThroughPropertyNames() { var schema = GenerateKeyed(); - var valueShape = schema["properties"]!["policies"]!["propertyNames"]!; - await Assert.That(valueShape["minLength"]!.GetValue()).IsEqualTo(1); - await Assert.That(valueShape["pattern"]!.GetValue()).IsEqualTo(@"\S"); + var valueShape = await Assert.That(schema["properties"]?["policies"]?["propertyNames"]).IsNotNull(); + await Assert.That(valueShape["minLength"]?.GetValue()).IsEqualTo(1); + await Assert.That(valueShape["pattern"]?.GetValue()).IsEqualTo(@"\S"); - var remainingMembersShape = schema["properties"]!["groups"]!["propertyNames"]!; - await Assert.That(remainingMembersShape["minLength"]!.GetValue()).IsEqualTo(1); - await Assert.That(remainingMembersShape["pattern"]!.GetValue()).IsEqualTo(@"\S"); + var remainingMembersShape = await Assert.That(schema["properties"]?["groups"]?["propertyNames"]).IsNotNull(); + await Assert.That(remainingMembersShape["minLength"]?.GetValue()).IsEqualTo(1); + await Assert.That(remainingMembersShape["pattern"]?.GetValue()).IsEqualTo(@"\S"); } // A key the model does not require states nothing about its own value, member name included. The node @@ -312,9 +310,9 @@ public async Task Generate_ConstrainsRequiredKeyThroughPropertyNames() [Test] public async Task Generate_LeavesAnOptionalKeyUnconstrained() { - var propertyNames = (JsonObject)GenerateKeyed()["properties"]!["optionalKeys"]!["propertyNames"]!; + var propertyNames = await Assert.That(GenerateKeyed()["properties"]?["optionalKeys"]?["propertyNames"]).IsNotNull(); - await Assert.That(propertyNames.Count).IsEqualTo(0); + await Assert.That(propertyNames.AsObject().Count).IsEqualTo(0); } // An example on the key is an example of a member name, so it lands in propertyNames; one on the value @@ -322,13 +320,13 @@ public async Task Generate_LeavesAnOptionalKeyUnconstrained() [Test] public async Task Generate_RoutesKeyAndValueExamplesToTheirOwnNodes() { - var exemplified = GenerateKeyed()["properties"]!["exemplified"]!; + var exemplified = await Assert.That(GenerateKeyed()["properties"]?["exemplified"]).IsNotNull(); - var keyExamples = (JsonArray)exemplified["propertyNames"]!["examples"]!; - await Assert.That(keyExamples[0]!.GetValue()).IsEqualTo("Some.Package.*"); + var keyExamples = await Assert.That(exemplified["propertyNames"]?["examples"]).IsNotNull(); + await Assert.That(keyExamples[0]?.GetValue()).IsEqualTo("Some.Package.*"); - var valueExamples = (JsonArray)exemplified["additionalProperties"]!["examples"]!; - await Assert.That(valueExamples[0]!.GetValue()).IsEqualTo("patch"); + var valueExamples = await Assert.That(exemplified["additionalProperties"]?["examples"]).IsNotNull(); + await Assert.That(valueExamples[0]?.GetValue()).IsEqualTo("patch"); } // A description of the key describes a member name, so it travels where the example does. Both exits @@ -339,14 +337,14 @@ public async Task Generate_RoutesKeyAndValueDescriptionsToTheirOwnNodes() { var schema = GenerateKeyed(); - var exemplified = schema["properties"]!["exemplified"]!; - await Assert.That(exemplified["propertyNames"]!["description"]!.GetValue()) + var exemplified = await Assert.That(schema["properties"]?["exemplified"]).IsNotNull(); + await Assert.That(exemplified["propertyNames"]?["description"]?.GetValue()) .IsEqualTo("Pattern over package ids."); - await Assert.That(exemplified["additionalProperties"]!["description"]!.GetValue()) + await Assert.That(exemplified["additionalProperties"]?["description"]?.GetValue()) .IsEqualTo("Policy governing every matching pin."); - var optionalKeys = schema["properties"]!["exemplifiedOptionalKeys"]!; - await Assert.That(optionalKeys["propertyNames"]!["description"]!.GetValue()) + var optionalKeys = await Assert.That(schema["properties"]?["exemplifiedOptionalKeys"]).IsNotNull(); + await Assert.That(optionalKeys["propertyNames"]?["description"]?.GetValue()) .IsEqualTo("Caption naming the group."); } @@ -355,8 +353,8 @@ await Assert.That(optionalKeys["propertyNames"]!["description"]!.GetValue()) + var propertyNames = await Assert.That(GenerateKeyed()["properties"]?["exemplifiedOptionalKeys"]?["propertyNames"]).IsNotNull(); + await Assert.That(propertyNames["examples"]?[0]?.GetValue()) .IsEqualTo("SDK package injections"); await Assert.That(propertyNames["minLength"]).IsNull(); } @@ -365,39 +363,38 @@ await Assert.That(((JsonArray)propertyNames["examples"]!)[0]!.GetValue() public async Task Generate_PrunesKeyFromRequiredAndKeepsTheRest() { var schema = GenerateKeyed(); - var groups = schema["properties"]!["groups"]!["additionalProperties"]!; + var groups = await Assert.That(schema["properties"]?["groups"]?["additionalProperties"]).IsNotNull(); await Assert.That(groups["required"]).IsNull(); - var requiredGroups = schema["properties"]!["requiredGroups"]!["additionalProperties"]!; - var required = (JsonArray)requiredGroups["required"]!; - await Assert.That(required.Count).IsEqualTo(1); - await Assert.That(required[0]!.GetValue()).IsEqualTo("files"); + var requiredGroups = await Assert.That(schema["properties"]?["requiredGroups"]?["additionalProperties"]).IsNotNull(); + var required = await Assert.That(requiredGroups["required"]).IsNotNull(); + await Assert.That(required.AsArray().Count).IsEqualTo(1); + await Assert.That(required[0]?.GetValue()).IsEqualTo("files"); } [Test] public async Task Generate_RendersNestedKeyedList() { - var policies = GenerateKeyed()["properties"]!["nested"]!["additionalProperties"]!["properties"]!["policies"]!; - await Assert.That(policies["type"]!.GetValue()).IsEqualTo("object"); - await Assert.That(policies["additionalProperties"]!["type"]!.GetValue()).IsEqualTo("string"); + var policies = await Assert.That(GenerateKeyed()["properties"]?["nested"]?["additionalProperties"]?["properties"]?["policies"]) + .IsNotNull(); + await Assert.That(policies["type"]?.GetValue()).IsEqualTo("object"); + await Assert.That(policies["additionalProperties"]?["type"]?.GetValue()).IsEqualTo("string"); } [Test] public async Task Generate_KeepsDescriptionOnKeyedList() { - var description = GenerateKeyed()["properties"]!["policies"]!["description"]; - await Assert.That(description!.GetValue()).IsEqualTo("the policies"); + var description = GenerateKeyed()["properties"]?["policies"]?["description"]; + await Assert.That(description?.GetValue()).IsEqualTo("the policies"); } [Test] public async Task Generate_KeepsNullOnJsonNullableKeyedList() { - var type = GenerateKeyed()["properties"]!["maybePolicies"]!["type"]; - await Assert.That(type is JsonArray).IsTrue(); - var typeArray = (JsonArray)type!; - await Assert.That(typeArray.Count).IsEqualTo(2); - await Assert.That(typeArray[0]!.GetValue()).IsEqualTo("object"); - await Assert.That(typeArray[1]!.GetValue()).IsEqualTo("null"); + var type = await Assert.That(GenerateKeyed()["properties"]?["maybePolicies"]?["type"]).IsNotNull(); + await Assert.That(type.AsArray().Count).IsEqualTo(2); + await Assert.That(type[0]?.GetValue()).IsEqualTo("object"); + await Assert.That(type[1]?.GetValue()).IsEqualTo("null"); } // A keyed-object list is dictionary-valued in JSON: [JsonAllowedKeys] closes its key set the same way it @@ -405,20 +402,20 @@ public async Task Generate_KeepsNullOnJsonNullableKeyedList() [Test] public async Task Generate_ConstrainsKeyedListToAllowedKeys() { - var limited = GenerateKeyed()["properties"]!["limitedPolicies"]!; - await Assert.That(limited["additionalProperties"]!.GetValue()).IsFalse(); - var properties = (JsonObject)limited["properties"]!; - await Assert.That(properties.Count).IsEqualTo(2); - await Assert.That(properties["first"]!["type"]!.GetValue()).IsEqualTo("string"); - await Assert.That(properties["second"]!["type"]!.GetValue()).IsEqualTo("string"); + var limited = await Assert.That(GenerateKeyed()["properties"]?["limitedPolicies"]).IsNotNull(); + await Assert.That(limited["additionalProperties"]?.GetValue()).IsFalse(); + var properties = await Assert.That(limited["properties"]).IsNotNull(); + await Assert.That(properties.AsObject().Count).IsEqualTo(2); + await Assert.That(properties["first"]?["type"]?.GetValue()).IsEqualTo("string"); + await Assert.That(properties["second"]?["type"]?.GetValue()).IsEqualTo("string"); } [Test] public async Task Generate_RendersKeyedListAtRoot() { var schema = JsonSchemaGenerator.Generate>(Options); - await Assert.That(schema["type"]!.GetValue()).IsEqualTo("object"); - await Assert.That(schema["additionalProperties"]!["type"]!.GetValue()).IsEqualTo("string"); + await Assert.That(schema["type"]?.GetValue()).IsEqualTo("object"); + await Assert.That(schema["additionalProperties"]?["type"]?.GetValue()).IsEqualTo("string"); } // Without JsonKeyedObjectConverter in the options, the list deserializes as a plain JSON array, and the @@ -427,18 +424,19 @@ public async Task Generate_RendersKeyedListAtRoot() public async Task Generate_WithoutKeyedConverter_RendersListAsArray() { var schema = JsonSchemaGenerator.Generate(KeyedConverterlessOptions); - var policies = schema["properties"]!["policies"]!; - await Assert.That(policies["type"]!.GetValue()).IsEqualTo("array"); - await Assert.That(policies["items"]!["type"]!.GetValue()).IsEqualTo("object"); + var policies = await Assert.That(schema["properties"]?["policies"]).IsNotNull(); + await Assert.That(policies["type"]?.GetValue()).IsEqualTo("array"); + await Assert.That(policies["items"]?["type"]?.GetValue()).IsEqualTo("object"); } // Object-shaped in the schema, but still a collection: no default, and no recursion into it. [Test] public async Task Generate_WithDefaults_StatesNoDefaultForKeyedObjectLists() { - var policies = GenerateWithDefaults()["properties"]!["policies"]!; + var policies = await Assert.That(GenerateWithDefaults()["properties"]?["policies"]).IsNotNull(); await Assert.That(policies["default"]).IsNull(); - await Assert.That(policies["additionalProperties"]!["default"]).IsNull(); + var values = await Assert.That(policies["additionalProperties"]).IsNotNull(); + await Assert.That(values["default"]).IsNull(); } [Test] diff --git a/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs b/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs index 50be0bef..9a4ecc45 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; @@ -280,7 +281,12 @@ public async Task Validate_ConstrainedKeyedList_ReportsDisallowedKey() await Assert.That(errors[0].JsonPointer).IsEqualTo("/limitedPolicies/third"); } - private static JsonNode Schema(string json) => JsonNode.Parse(json)!; + private static JsonNode Schema(string json) + { + var schema = JsonNode.Parse(json); + Debug.Assert(schema is not null, "Parse returns null only for the null literal, and no test passes it as a schema."); + return schema; + } private static JsonNode KeyedSchema() => JsonSchemaGenerator.Generate(KeyedOptions); From 3c2083dc082e9a52c227e9adac2811d086ee8344 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 09:24:32 +0100 Subject: [PATCH 07/11] Remove the remaining null-forgiving operators from tests Assert nullable results with IsNotNull(), use ?. for value comparisons, and filter lambdas with OfType(). AssemblyExtensions.GetMetadata replaces AssemblyMetadataAttribute lookups. A null passed on purpose to test null detection keeps the operator, under a justification comment. Co-Authored-By: Claude Opus 5.5 --- .../AssemblyExtensions.cs | 37 ++++++++++++++++ .../BuildvanaConfigFactoryTests.cs | 20 +++++---- .../BuildvanaJsonConfigProviderTests.cs | 38 +++++++++------- .../ConfigurationPageTests.cs | 6 +-- .../RepositoryConfigFilesTests.cs | 7 +-- .../ToolDiagnosticsPageTests.cs | 8 ++-- .../ReporterExtensionsTests.cs | 1 + .../HomeDirectoryDiscoveryTests.cs | 5 ++- .../FileFinderTests.cs | 5 ++- .../GitignoreFileTests.cs | 1 + .../GitignorePatternMatchTests.cs | 5 ++- .../GitignorePatternParseTests.cs | 13 +++--- .../UserDirectoryTests.cs | 7 +++ .../Buildvana.Core.IO.Tests/UserFileTests.cs | 5 +++ .../JsonKeyedObjectConverterTests.cs | 30 +++++++------ .../JsonSchemaValidatorTests.cs | 1 + .../PostReleaseHookArgsTests.cs | 8 ++-- .../TestCompilation.cs | 5 ++- .../ComputeVersionTests.cs | 4 +- .../ConvertPfxToSnkTests.cs | 6 +-- .../TaskLoggingHelperReporterTests.cs | 12 +++++ .../WritePackagePinDumpTests.cs | 2 +- .../WriteThisAssemblyConstantsFileTests.cs | 2 +- .../AdditionalAssemblyInfoModuleTests.cs | 6 +-- .../AssemblySigningModuleTests.cs | 6 +-- .../CheckRoslynVersionTests.cs | 6 +-- .../FileBasedAppsModuleTests.cs | 6 +-- .../InnoSetupTargetsTests.cs | 6 +-- tests/Buildvana.Sdk.Tests/ModulePagesTests.cs | 6 +-- .../PackableProjectTargetsTests.cs | 6 +-- tests/Buildvana.Sdk.Tests/PinDumpFixture.cs | 12 ++--- .../PublishToFoldersTargetsTests.cs | 6 +-- .../Buildvana.Sdk.Tests/ReadmeTargetsTests.cs | 6 +-- .../ReleaseAssetListModuleTests.cs | 6 +-- .../SdkDiagnosticsPageTests.cs | 6 +-- tests/Buildvana.Sdk.Tests/SdkPropsFixture.cs | 12 ++--- .../StandardAnalyzersModuleTargetsTests.cs | 6 +-- .../StandardAnalyzersModuleTests.cs | 6 +-- .../TransitiveOverridesFixture.cs | 12 ++--- .../XmlDocumentationModuleTests.cs | 6 +-- .../AdditionalGroupPinReaderTests.cs | 21 +++------ .../BvOptionAttributeTests.cs | 5 +-- tests/Buildvana.Tool.Tests/CIPlatformTests.cs | 14 +++--- .../CommandArgumentValidatorTests.cs | 44 +++++++++---------- .../CommandLineOverridesParserTests.cs | 4 +- .../Buildvana.Tool.Tests/CommandPagesTests.cs | 11 ++--- .../CommandRegistryTests.cs | 4 +- .../ConsoleEncodingScopeTests.cs | 2 +- .../DelegationServiceTests.cs | 8 ++-- .../DependencyResolutionTests.cs | 7 +-- .../DependencyResolverTests.cs | 24 +++++----- .../DirectivePinReaderTests.cs | 25 ++++------- .../FileBasedAppHelperTests.cs | 6 +-- .../GlobalJsonPinReaderTests.cs | 5 ++- tests/Buildvana.Tool.Tests/HookRunnerTests.cs | 3 +- .../OrphanDetectorTests.cs | 5 ++- .../OverrideLifecycleTests.cs | 5 ++- .../PackagePinReaderTests.cs | 33 ++++++-------- .../PostUpdateHookArgsFactoryTests.cs | 9 ++-- .../ReleaseCommandFailureTests.cs | 2 +- .../ReleaseCommandTests.cs | 4 +- tests/Buildvana.Tool.Tests/ReleaseHarness.cs | 5 ++- .../SelfVersionServiceTests.cs | 7 ++- .../ServerReleaseTests.cs | 13 +++--- .../SettingsHelpReflectionTests.cs | 4 +- .../StubHttpMessageHandler.cs | 2 +- .../ToolDiagnosticsPageTests.cs | 8 ++-- .../ToolResolverCacheProbeTests.cs | 6 +-- .../UpdatePolicyEngineTests.cs | 4 ++ .../UsageExitCodeTests.cs | 14 ++++-- .../VerbosityParserTests.cs | 1 + 71 files changed, 337 insertions(+), 316 deletions(-) create mode 100644 src/Buildvana.Core.Testing/AssemblyExtensions.cs diff --git a/src/Buildvana.Core.Testing/AssemblyExtensions.cs b/src/Buildvana.Core.Testing/AssemblyExtensions.cs new file mode 100644 index 00000000..a7a2a7ef --- /dev/null +++ b/src/Buildvana.Core.Testing/AssemblyExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.Reflection; +using CommunityToolkit.Diagnostics; + +namespace Buildvana.Core.Testing; + +/// +/// Provides extension methods for instances. +/// +#pragma warning disable CA1034 // Nested types should not be visible — false positive on C# 14 extension blocks; fixed in .NET 11, backport to .NET 10 requested in https://github.com/dotnet/sdk/issues/53984 +#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716 +public static class AssemblyExtensions +{ + extension(Assembly @this) + { + /// + /// Gets the value of an assembly metadata item, such as one a test project supplies with an + /// AssemblyMetadata item. + /// + /// The key of the metadata item. + /// The value of the first metadata item with the given key. + /// The assembly has no metadata item with the given key, or the + /// item has no value. + public string GetMetadata(string key) + { + Guard.IsNotNull(key); + var value = @this.GetCustomAttributes() + .FirstOrDefault(attribute => string.Equals(attribute.Key, key, StringComparison.Ordinal)) + ?.Value; + return value ?? throw new InvalidOperationException($"{@this.GetName().Name} has no metadata item \"{key}\" with a value."); + } + } +} diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigFactoryTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigFactoryTests.cs index 65b16f5b..6fa50778 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigFactoryTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigFactoryTests.cs @@ -268,10 +268,12 @@ public async Task Create_Feeds_MapVerbatim() var config = BuildvanaConfigFactory.Create(json, null); - await Assert.That(config.NuGet.Feeds.Prerelease!.Source).IsEqualTo("https://prerelease.example/v3/index.json"); - await Assert.That(config.NuGet.Feeds.Prerelease.ApiKeyEnv).IsEqualTo("PRE_KEY"); - await Assert.That(config.NuGet.Feeds.Release!.Source).IsEqualTo("https://nuget.example/v3/index.json"); - await Assert.That(config.NuGet.Feeds.Release.ApiKeyEnv).IsEqualTo("KEY"); + var prerelease = await Assert.That(config.NuGet.Feeds.Prerelease).IsNotNull(); + await Assert.That(prerelease.Source).IsEqualTo("https://prerelease.example/v3/index.json"); + await Assert.That(prerelease.ApiKeyEnv).IsEqualTo("PRE_KEY"); + var release = await Assert.That(config.NuGet.Feeds.Release).IsNotNull(); + await Assert.That(release.Source).IsEqualTo("https://nuget.example/v3/index.json"); + await Assert.That(release.ApiKeyEnv).IsEqualTo("KEY"); } // A prerelease with no feed of its own pushes to the release feed: the factory resolves the fallback, @@ -289,8 +291,9 @@ public async Task Create_Feeds_PrereleaseFallsBackToReleaseFeed() var config = BuildvanaConfigFactory.Create(json, null); - await Assert.That(config.NuGet.Feeds.Prerelease).IsSameReferenceAs(config.NuGet.Feeds.Release!); - await Assert.That(config.NuGet.Feeds.Release!.Source).IsEqualTo("https://nuget.example/v3/index.json"); + var release = await Assert.That(config.NuGet.Feeds.Release).IsNotNull(); + await Assert.That(config.NuGet.Feeds.Prerelease).IsSameReferenceAs(release); + await Assert.That(release.Source).IsEqualTo("https://nuget.example/v3/index.json"); } [Test] @@ -302,8 +305,9 @@ public async Task Create_GitIdentity_MapsNameAndEmail() var config = BuildvanaConfigFactory.Create(json, null); - await Assert.That(config.Git.Identity!.Name).IsEqualTo("Buildvana Bot"); - await Assert.That(config.Git.Identity.Email).IsEqualTo("bot@buildvana.invalid"); + var identity = await Assert.That(config.Git.Identity).IsNotNull(); + await Assert.That(identity.Name).IsEqualTo("Buildvana Bot"); + await Assert.That(identity.Email).IsEqualTo("bot@buildvana.invalid"); } // Configured patterns extend the built-in scope rather than replace it: hooks are file-based apps by diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs index 8982da7a..ad218a2d 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaJsonConfigProviderTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text; using Buildvana.Core; using Buildvana.Core.Configuration; @@ -32,7 +33,7 @@ public async Task Config_ValidConfig_Loads() { Write(dir, "buildvana.jsonc", """{ "release": { "branches": ["main"] } }"""); var config = NewProvider(dir).Config; - await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(config.Release?.Branches?.Count).IsEqualTo(1); } finally { @@ -124,7 +125,7 @@ public async Task Config_ReadBeforePath_FindsTheFile() Write(dir, "buildvana.jsonc", """{ "release": { "branches": ["main"] } }"""); var provider = NewProvider(dir); - await Assert.That(provider.Config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(provider.Config.Release?.Branches?.Count).IsEqualTo(1); await Assert.That(provider.Path).IsEqualTo(Path.Combine(dir, "buildvana.jsonc")); } finally @@ -328,7 +329,7 @@ public async Task LoadFile_KnownPath_LoadsItRegardlessOfSiblings() Write(dir, "buildvana.json", """{ "release": { "branches": ["main"] } }"""); Write(dir, "buildvana.jsonc", "{}"); var config = BuildvanaJsonConfigProvider.LoadFile(Path.Combine(dir, "buildvana.json")); - await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(config.Release?.Branches?.Count).IsEqualTo(1); } finally { @@ -351,8 +352,8 @@ public async Task Config_TypedContent_LoadsEnumsAndNestedSections() """; Write(dir, "buildvana.jsonc", content); var config = NewProvider(dir).Config; - await Assert.That(config.Versioning!.AssemblyVersionPrecision).IsEqualTo(AssemblyVersionPrecision.Minor); - await Assert.That(config.NuGet!.Feeds!.Release!.ApiKeyEnv).IsEqualTo("KEY"); + await Assert.That(config.Versioning?.AssemblyVersionPrecision).IsEqualTo(AssemblyVersionPrecision.Minor); + await Assert.That(config.NuGet?.Feeds?.Release?.ApiKeyEnv).IsEqualTo("KEY"); } finally { @@ -416,17 +417,18 @@ public async Task Config_DependenciesSection_LoadsKeyedListsInDocumentOrder() """; Write(dir, "buildvana.jsonc", content); - var dependencies = NewProvider(dir).Config.Dependencies!; - - await Assert.That(dependencies.Scopes!.NetSdk).IsEqualTo("lts"); - await Assert.That(dependencies.Scopes.Packages).IsEqualTo("major-"); - await Assert.That(dependencies.Scopes.Sdks).IsNull(); - await Assert.That(dependencies.Policies![0].Pattern).IsEqualTo("Microsoft.CodeAnalysis.*"); - await Assert.That(dependencies.Policies[0].Policy).IsEqualTo("minor"); - await Assert.That(dependencies.Policies[1].Pattern).IsEqualTo("StyleCop.Analyzers"); - await Assert.That(dependencies.AdditionalPackages![0].Caption).IsEqualTo("SDK-injected packages"); - await Assert.That(dependencies.AdditionalPackages[0].Items).IsEqualTo("BV_PackageVersion"); - await Assert.That(dependencies.AdditionalPackages[0].Policy).IsNull(); + var dependencies = await Assert.That(NewProvider(dir).Config.Dependencies).IsNotNull(); + + var scopes = await Assert.That(dependencies.Scopes).IsNotNull(); + await Assert.That(scopes.NetSdk).IsEqualTo("lts"); + await Assert.That(scopes.Packages).IsEqualTo("major-"); + await Assert.That(scopes.Sdks).IsNull(); + await Assert.That(dependencies.Policies?[0].Pattern).IsEqualTo("Microsoft.CodeAnalysis.*"); + await Assert.That(dependencies.Policies?[0].Policy).IsEqualTo("minor"); + await Assert.That(dependencies.Policies?[1].Pattern).IsEqualTo("StyleCop.Analyzers"); + await Assert.That(dependencies.AdditionalPackages?[0].Caption).IsEqualTo("SDK-injected packages"); + await Assert.That(dependencies.AdditionalPackages?[0].Items).IsEqualTo("BV_PackageVersion"); + await Assert.That(dependencies.AdditionalPackages?[0].Policy).IsNull(); } finally { @@ -638,7 +640,9 @@ private static string NewDir() private static void Write(string dir, string fileName, string content, bool bom = false) { var path = Path.Combine(dir, fileName); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: bom)); } diff --git a/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs b/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs index e527673f..0f9f334a 100644 --- a/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/ConfigurationPageTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using System.Text.Json.Nodes; using Buildvana.Core.Configuration; using Buildvana.Core.Testing; @@ -17,10 +16,7 @@ internal sealed class ConfigurationPageTests // What the page writes for a setting the schema states no default for. private const string NoDefault = "none"; - private static readonly string RepositoryRoot = typeof(ConfigurationPageTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(ConfigurationPageTests).Assembly.GetMetadata("RepositoryRoot"); private static readonly JsonObject Schema = (JsonObject)BuildvanaJsonConfigSchema.GenerateNode(); diff --git a/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs b/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs index f4ef90ce..87da0dbc 100644 --- a/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/RepositoryConfigFilesTests.cs @@ -1,10 +1,10 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using Buildvana.Core.Configuration; +using Buildvana.Core.Testing; // The repository's own configuration files, checked against the model in this branch. The repository root // reaches the test through an assembly metadata item the test project supplies, as Buildvana.Sdk.Tests does @@ -28,10 +28,7 @@ internal sealed class RepositoryConfigFilesTests AllowTrailingCommas = true, }; - private static readonly string RepositoryRoot = typeof(RepositoryConfigFilesTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(RepositoryConfigFilesTests).Assembly.GetMetadata("RepositoryRoot"); // The schema built from the model in this branch, which is what LoadFile validates against too. No // committed schema file is read here: a rebase moves the model, and every check below moves with it. diff --git a/tests/Buildvana.Core.Configuration.Tests/ToolDiagnosticsPageTests.cs b/tests/Buildvana.Core.Configuration.Tests/ToolDiagnosticsPageTests.cs index 09e58a5a..a3f36f79 100644 --- a/tests/Buildvana.Core.Configuration.Tests/ToolDiagnosticsPageTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/ToolDiagnosticsPageTests.cs @@ -12,10 +12,7 @@ internal sealed class ToolDiagnosticsPageTests { private const string SectionHeading = "Configuration (1100-1199)"; - private static readonly string RepositoryRoot = typeof(ToolDiagnosticsPageTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(ToolDiagnosticsPageTests).Assembly.GetMetadata("RepositoryRoot"); [Test] public async Task ConfigurationSection_ListsTheCodesOfDiagnosticCodes() @@ -24,7 +21,8 @@ public async Task ConfigurationSection_ListsTheCodesOfDiagnosticCodes() var listed = page.GetTableColumn(SectionHeading, "Code"); var declared = typeof(DiagnosticCodes) .GetFields(BindingFlags.Public | BindingFlags.Static) - .Select(static field => (string)field.GetRawConstantValue()!); + .Select(static field => field.GetRawConstantValue()) + .OfType(); await Assert.That(Join(listed)).IsEqualTo(Join(declared)); } diff --git a/tests/Buildvana.Core.ConsoleOutput.Tests/ReporterExtensionsTests.cs b/tests/Buildvana.Core.ConsoleOutput.Tests/ReporterExtensionsTests.cs index 846f3cef..0d7184a2 100644 --- a/tests/Buildvana.Core.ConsoleOutput.Tests/ReporterExtensionsTests.cs +++ b/tests/Buildvana.Core.ConsoleOutput.Tests/ReporterExtensionsTests.cs @@ -68,6 +68,7 @@ public async Task Report_NullFormat_Throws() using var reporter = new StringWriterReporter(Verbosity.Diagnostic); // ReSharper disable once AccessToDisposedClosure // the assertion invokes the delegate before returning + // ! Null on purpose: testing null detection await Assert.That(() => reporter.Report(MessageLevel.Info, (CompositeFormat)null!)).Throws(); } } diff --git a/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs b/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs index e42d2dda..bbc2ecd0 100644 --- a/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs +++ b/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core.HomeDirectory; internal sealed class HomeDirectoryDiscoveryTests @@ -118,7 +119,9 @@ private static string NewDir() private static void WriteFile(string root, string relativePath) { var path = Path.Combine(root, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, string.Empty); } diff --git a/tests/Buildvana.Core.IO.Tests/FileFinderTests.cs b/tests/Buildvana.Core.IO.Tests/FileFinderTests.cs index b078f536..e4e0ff15 100644 --- a/tests/Buildvana.Core.IO.Tests/FileFinderTests.cs +++ b/tests/Buildvana.Core.IO.Tests/FileFinderTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Buildvana.Core.IO; internal sealed class FileFinderTests @@ -272,7 +273,9 @@ private static string CreateTempTree() private static void WriteFile(string root, string relativePath, string content = "") { var path = Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar)); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } } diff --git a/tests/Buildvana.Core.IO.Tests/GitignoreFileTests.cs b/tests/Buildvana.Core.IO.Tests/GitignoreFileTests.cs index 5e52ad8c..5a220c8a 100644 --- a/tests/Buildvana.Core.IO.Tests/GitignoreFileTests.cs +++ b/tests/Buildvana.Core.IO.Tests/GitignoreFileTests.cs @@ -8,6 +8,7 @@ internal sealed class GitignoreFileTests [Test] public async Task Parse_WithNullLines_ThrowsRaw() { + // ! Null on purpose: testing null detection static GitignoreFile Act() => GitignoreFile.Parse(null!); await Assert.That(Act).Throws(); diff --git a/tests/Buildvana.Core.IO.Tests/GitignorePatternMatchTests.cs b/tests/Buildvana.Core.IO.Tests/GitignorePatternMatchTests.cs index d607ee6a..d61b680f 100644 --- a/tests/Buildvana.Core.IO.Tests/GitignorePatternMatchTests.cs +++ b/tests/Buildvana.Core.IO.Tests/GitignorePatternMatchTests.cs @@ -151,14 +151,15 @@ public async Task Matches_EscapedTrailingSpace_IsLiteral() [Test] public async Task Matches_WithEmptyPath_ReturnsFalse() { - var pattern = GitignorePattern.TryParse("*")!; + var pattern = await Assert.That(GitignorePattern.TryParse("*")).IsNotNull(); await Assert.That(pattern.Matches([], isDirectory: false, ignoreCase: false)).IsFalse(); } private static bool Match(string patternText, string path, bool isDirectory = false, bool ignoreCase = false) { - var pattern = GitignorePattern.TryParse(patternText)!; + var pattern = GitignorePattern.TryParse(patternText) + ?? throw new InvalidOperationException($"'{patternText}' parses to no pattern."); return pattern.Matches(path.Split('/'), isDirectory, ignoreCase); } } diff --git a/tests/Buildvana.Core.IO.Tests/GitignorePatternParseTests.cs b/tests/Buildvana.Core.IO.Tests/GitignorePatternParseTests.cs index d3239b8b..19ed3d4e 100644 --- a/tests/Buildvana.Core.IO.Tests/GitignorePatternParseTests.cs +++ b/tests/Buildvana.Core.IO.Tests/GitignorePatternParseTests.cs @@ -8,6 +8,7 @@ internal sealed class GitignorePatternParseTests [Test] public async Task TryParse_WithNullLine_ThrowsRaw() { + // ! Null on purpose: testing null detection static GitignorePattern? Act() => GitignorePattern.TryParse(null!); await Assert.That(Act).Throws(); @@ -50,7 +51,7 @@ public async Task TryParse_WithEscapedHash_YieldsPattern() [Test] public async Task TryParse_WithNegation_SetsIsNegated() { - var pattern = GitignorePattern.TryParse("!foo")!; + var pattern = await Assert.That(GitignorePattern.TryParse("!foo")).IsNotNull(); await Assert.That(pattern.IsNegated).IsTrue(); await Assert.That(pattern.IsDirectoryOnly).IsFalse(); @@ -59,7 +60,7 @@ public async Task TryParse_WithNegation_SetsIsNegated() [Test] public async Task TryParse_WithTrailingSlash_SetsIsDirectoryOnly() { - var pattern = GitignorePattern.TryParse("foo/")!; + var pattern = await Assert.That(GitignorePattern.TryParse("foo/")).IsNotNull(); await Assert.That(pattern.IsDirectoryOnly).IsTrue(); await Assert.That(pattern.IsNegated).IsFalse(); @@ -68,7 +69,7 @@ public async Task TryParse_WithTrailingSlash_SetsIsDirectoryOnly() [Test] public async Task TryParse_PreservesOriginalText() { - var pattern = GitignorePattern.TryParse("!foo/ ")!; + var pattern = await Assert.That(GitignorePattern.TryParse("!foo/ ")).IsNotNull(); await Assert.That(pattern.Text).IsEqualTo("!foo/ "); await Assert.That(pattern.ToString()).IsEqualTo("!foo/ "); @@ -77,7 +78,7 @@ public async Task TryParse_PreservesOriginalText() [Test] public async Task TryParse_WithUnanchoredPattern_PrependsAnyDepth() { - var pattern = GitignorePattern.TryParse("foo")!; + var pattern = await Assert.That(GitignorePattern.TryParse("foo")).IsNotNull(); await Assert.That(pattern.Segments.Count).IsEqualTo(2); await Assert.That(pattern.Segments[0].IsAnyDepth).IsTrue(); @@ -86,7 +87,7 @@ public async Task TryParse_WithUnanchoredPattern_PrependsAnyDepth() [Test] public async Task TryParse_WithAnchoredPattern_DoesNotPrependAnyDepth() { - var pattern = GitignorePattern.TryParse("/foo")!; + var pattern = await Assert.That(GitignorePattern.TryParse("/foo")).IsNotNull(); await Assert.That(pattern.Segments.Count).IsEqualTo(1); await Assert.That(pattern.Segments[0].IsAnyDepth).IsFalse(); @@ -95,7 +96,7 @@ public async Task TryParse_WithAnchoredPattern_DoesNotPrependAnyDepth() [Test] public async Task TryParse_WithTrailingAnyDepth_AppendsAnyComponentSegment() { - var pattern = GitignorePattern.TryParse("abc/**")!; + var pattern = await Assert.That(GitignorePattern.TryParse("abc/**")).IsNotNull(); // "abc", "**", and the synthesized match-one-component segment. await Assert.That(pattern.Segments.Count).IsEqualTo(3); diff --git a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs index 91d8b7fd..03298c60 100644 --- a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs @@ -50,6 +50,7 @@ public async Task CreateDirectory_WithFileAtTargetPath_Fails() [Test] public async Task CreateDirectory_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static DirectoryInfo Act() => UserDirectory.CreateDirectory(null!); await Assert.That(Act).Throws(); @@ -114,6 +115,7 @@ public async Task Delete_Recursive_WithNonEmptyDirectory_DeletesTree() [Test] public async Task Delete_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static void Act() => UserDirectory.Delete(null!); await Assert.That(Act).Throws(); @@ -164,6 +166,7 @@ public async Task DeleteIfExists_WithoutReporter_DeletesDirectory() [Test] public async Task DeleteIfExists_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static void Act() => UserDirectory.DeleteIfExists(null!); await Assert.That(Act).Throws(); @@ -210,6 +213,7 @@ public async Task EnumerateFiles_WithMissingBaseDirectory_ReturnsEmptyList() [Test] public async Task EnumerateFiles_WithNullBaseDirectory_ThrowsRaw() { + // ! Null on purpose: testing null detection static IReadOnlyList Act() => UserDirectory.EnumerateFiles(null!, "*.nupkg"); await Assert.That(Act).Throws(); @@ -218,6 +222,7 @@ public async Task EnumerateFiles_WithNullBaseDirectory_ThrowsRaw() [Test] public async Task EnumerateFiles_WithNullPattern_ThrowsRaw() { + // ! Null on purpose: testing null detection static IReadOnlyList Act() => UserDirectory.EnumerateFiles(Path.GetTempPath(), null!); await Assert.That(Act).Throws(); @@ -234,6 +239,7 @@ public async Task Exists_WithMissingDirectory_ReturnsFalse() [Test] public async Task Exists_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static bool Act() => UserDirectory.Exists(null!); await Assert.That(Act).Throws(); @@ -273,6 +279,7 @@ public async Task SetCurrentDirectory_WithMissingDirectory_Fails() [Test] public async Task SetCurrentDirectory_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static void Act() => UserDirectory.SetCurrentDirectory(null!); await Assert.That(Act).Throws(); diff --git a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs index a5713449..51297238 100644 --- a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs @@ -165,6 +165,7 @@ public async Task WriteAllText_WithMalformedPath_Fails() [Test] public async Task ReadAllText_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static string Act() => UserFile.ReadAllText(null!); await Assert.That(Act).Throws(); @@ -181,6 +182,7 @@ public async Task ReadAllText_WithEmptyPath_ThrowsRaw() [Test] public async Task WriteAllText_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static void Act() => UserFile.WriteAllText(null!, "x"); await Assert.That(Act).Throws(); @@ -199,6 +201,7 @@ public async Task WriteAllText_WithEmptyPath_ThrowsRaw() [Test] public async Task ReadAllTextAsync_WithNullPath_ThrowsRawAndSynchronously() { + // ! Null on purpose: testing null detection static void Act() => _ = UserFile.ReadAllTextAsync(null!); await Assert.That(Act).Throws(); @@ -215,6 +218,7 @@ public async Task ReadAllTextAsync_WithEmptyPath_ThrowsRawAndSynchronously() [Test] public async Task WriteAllTextAsync_WithNullPath_ThrowsRawAndSynchronously() { + // ! Null on purpose: testing null detection static void Act() => _ = UserFile.WriteAllTextAsync(null!, "x"); await Assert.That(Act).Throws(); @@ -526,6 +530,7 @@ public async Task Exists_WithDirectoryAtPath_ReturnsFalse() [Test] public async Task Exists_WithNullPath_ThrowsRaw() { + // ! Null on purpose: testing null detection static bool Act() => UserFile.Exists(null!); await Assert.That(Act).Throws(); diff --git a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs index a6e29f06..923b0f0a 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Buildvana.Core.Json; +using TUnit.Assertions.Enums; internal sealed class JsonKeyedObjectConverterTests { @@ -44,7 +45,7 @@ public async Task Read_MembersShape_ReadsElementsInDocumentOrder() await Assert.That(list[0].Caption).IsEqualTo("G1"); await Assert.That(list[0].Files).IsEqualTo("src/*.props"); await Assert.That(list[0].Retries).IsEqualTo(3); - await Assert.That(list[0].Tags!.SequenceEqual(["a", "b"])).IsTrue(); + await Assert.That(list[0].Tags).IsEquivalentTo(["a", "b"], CollectionOrdering.Matching); await Assert.That(list[1].Caption).IsEqualTo("G2"); await Assert.That(list[1].Files).IsNull(); } @@ -63,10 +64,10 @@ public async Task ReadWrite_AsProperty_RoundTrips() // where the enclosing parse expects it. const string json = """{"policies":{"a":"one","b":"two"},"name":"n"}"""; var options = CreateOptions(); - var holder = JsonSerializer.Deserialize(json, options); - await Assert.That(holder!.Name).IsEqualTo("n"); - await Assert.That(holder.Policies!.Count).IsEqualTo(2); - await Assert.That(holder.Policies[0]).IsEqualTo(new KeyedValueSample { Pattern = "a", Policy = "one" }); + var holder = await Assert.That(JsonSerializer.Deserialize(json, options)).IsNotNull(); + await Assert.That(holder.Name).IsEqualTo("n"); + await Assert.That(holder.Policies?.Count).IsEqualTo(2); + await Assert.That(holder.Policies?[0]).IsEqualTo(new KeyedValueSample { Pattern = "a", Policy = "one" }); await Assert.That(JsonSerializer.Serialize(holder, options)).IsEqualTo(json); } @@ -84,8 +85,8 @@ public async Task ReadWrite_KeyedListInsideKeyedElement_RoundTrips() const string json = """{"outer":{"policies":{"a":"one","b":"two"}}}"""; var list = Deserialize(json); await Assert.That(list[0].Caption).IsEqualTo("outer"); - await Assert.That(list[0].Policies!.Count).IsEqualTo(2); - await Assert.That(list[0].Policies![1]).IsEqualTo(new KeyedValueSample { Pattern = "b", Policy = "two" }); + await Assert.That(list[0].Policies?.Count).IsEqualTo(2); + await Assert.That(list[0].Policies?[1]).IsEqualTo(new KeyedValueSample { Pattern = "b", Policy = "two" }); await Assert.That(Serialize(list)).IsEqualTo(json); } @@ -209,6 +210,7 @@ public async Task Write_RenamedKey_ResolvesJsonPropertyName() [Test] public async Task Write_NullKey_Throws() { + // ! Null on purpose: testing null detection static string Act() => Serialize([new() { Pattern = null!, Policy = "x" }]); await Assert.That(Act).Throws().WithMessageContaining("non-null string", StringComparison.Ordinal); @@ -217,6 +219,7 @@ public async Task Write_NullKey_Throws() [Test] public async Task Write_ValueShape_NullValue_WritesNull() { + // ! Null on purpose: testing null detection var json = Serialize([new() { Pattern = "a", Policy = null! }]); await Assert.That(json).IsEqualTo("""{"a":null}"""); } @@ -240,9 +243,9 @@ public async Task ReadWrite_SourceGeneratedContext_ResolvesGeneratedNames() }; var list = JsonSerializer.Deserialize>("""{"G1":{"files":"x"}}""", options); - await Assert.That(list!.Count).IsEqualTo(1); - await Assert.That(list[0].Caption).IsEqualTo("G1"); - await Assert.That(list[0].Files).IsEqualTo("x"); + await Assert.That(list?.Count).IsEqualTo(1); + await Assert.That(list?[0].Caption).IsEqualTo("G1"); + await Assert.That(list?[0].Files).IsEqualTo("x"); var json = JsonSerializer.Serialize(list, options); await Assert.That(json).IsEqualTo("""{"G1":{"files":"x","retries":0,"tags":null}}"""); @@ -274,8 +277,8 @@ public async Task ReadWrite_CustomizedContract_FollowsTypeInfoNames() }; var list = JsonSerializer.Deserialize>("""{"G1":{"files":"x"}}""", options); - await Assert.That(list![0].Caption).IsEqualTo("G1"); - await Assert.That(list[0].Files).IsEqualTo("x"); + await Assert.That(list?[0].Caption).IsEqualTo("G1"); + await Assert.That(list?[0].Files).IsEqualTo("x"); var json = JsonSerializer.Serialize(list, options); await Assert.That(json).IsEqualTo("""{"G1":{"files":"x","retries":0,"tags":null}}"""); @@ -329,7 +332,8 @@ private static JsonSerializerOptions CreateOptions(bool allowDuplicateProperties }; private static IReadOnlyList Deserialize(string json) - => JsonSerializer.Deserialize>(json, CreateOptions())!; + => JsonSerializer.Deserialize>(json, CreateOptions()) + ?? throw new InvalidOperationException("The JSON deserializes to null."); private static string Serialize(IReadOnlyList list) => JsonSerializer.Serialize(list, CreateOptions()); diff --git a/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs b/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs index 9a4ecc45..9289157f 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonSchemaValidatorTests.cs @@ -207,6 +207,7 @@ public async Task Validate_WithSourceMap_FillsLineAndColumn() await Assert.That(errors[0].Column).IsEqualTo(11); } + // ! Null on purpose: testing null detection [Test] public async Task Validate_WithNullSourceMap_Throws() => await Assert.That(() => JsonSchemaValidator.Validate( diff --git a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs index acfc9636..c78d82bc 100644 --- a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs +++ b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs @@ -36,7 +36,7 @@ public async Task Load_RoundTripsWhatBvWrites(bool withConfigFile) var written = SampleArgs(dir, withConfigFile); var relativePath = WellKnownPaths.GetHookArgsFile(PostReleaseHookArgs.Context, PostReleaseHookArgs.Event); var path = Path.Combine(dir, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _ = Directory.CreateDirectory(await Assert.That(Path.GetDirectoryName(path)).IsNotNull()); object boxed = written; var json = JsonSerializer.Serialize(boxed, boxed.GetType(), BuildvanaJsonContext.Default); await File.WriteAllTextAsync(path, json).ConfigureAwait(false); @@ -75,7 +75,7 @@ public async Task Load_WithoutHomeDirectory_ReadsTheArgsFileOfTheCurrentDirector { var relativePath = WellKnownPaths.GetHookArgsFile(PostReleaseHookArgs.Context, PostReleaseHookArgs.Event); var path = Path.Combine(dir, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _ = Directory.CreateDirectory(await Assert.That(Path.GetDirectoryName(path)).IsNotNull()); object boxed = SampleArgs(dir); var json = JsonSerializer.Serialize(boxed, boxed.GetType(), BuildvanaJsonContext.Default); await File.WriteAllTextAsync(path, json).ConfigureAwait(false); @@ -100,7 +100,7 @@ public async Task Load_InvalidJson_Throws() try { var path = Path.Combine(dir, WellKnownPaths.GetHookArgsFile("release", "post-release")); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _ = Directory.CreateDirectory(await Assert.That(Path.GetDirectoryName(path)).IsNotNull()); await File.WriteAllTextAsync(path, "{ not json ").ConfigureAwait(false); await Assert.That(() => PostReleaseHookArgs.Load(dir)).Throws(); } @@ -120,7 +120,7 @@ public async Task Load_UnreadableArgsFile_Throws() try { var path = Path.Combine(dir, WellKnownPaths.GetHookArgsFile("release", "post-release")); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _ = Directory.CreateDirectory(await Assert.That(Path.GetDirectoryName(path)).IsNotNull()); await File.WriteAllTextAsync(path, "{}").ConfigureAwait(false); await using var locker = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None).ConfigureAwait(false); diff --git a/tests/Buildvana.Sdk.SourceGenerators.Tests/TestCompilation.cs b/tests/Buildvana.Sdk.SourceGenerators.Tests/TestCompilation.cs index 58cd99b7..742ada34 100644 --- a/tests/Buildvana.Sdk.SourceGenerators.Tests/TestCompilation.cs +++ b/tests/Buildvana.Sdk.SourceGenerators.Tests/TestCompilation.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -26,7 +27,9 @@ public static async Task AssertHasNoErrors(Compilation compilation) private static IEnumerable GetReferences() { - var trustedAssemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator); + var trustedAssemblyList = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string; + Debug.Assert(trustedAssemblyList is not null, "The .NET host lists the trusted platform assemblies of every process."); + var trustedAssemblies = trustedAssemblyList.Split(Path.PathSeparator); return trustedAssemblies .Where(static p => Path.GetFileName(p) is "System.Runtime.dll" or "System.Private.CoreLib.dll" or "netstandard.dll") .Select(MetadataReference (p) => MetadataReference.CreateFromFile(p)); diff --git a/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs index 19f80255..c24d5a3e 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs +++ b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs @@ -13,7 +13,7 @@ public async Task Execute_WithMissingHomeDirectory_LogsError() var task = new ComputeVersion { BuildEngine = engine }; await Assert.That(task.Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1050"); } [Test] @@ -146,5 +146,5 @@ private static ComputeVersion RunTask(TempGitRepo repo, RecordingBuildEngine? en } private static int CountVersionMessages(RecordingBuildEngine engine, string semVer) - => engine.Messages.Count(m => m.Message!.Contains($"Computed version {semVer}", StringComparison.Ordinal)); + => engine.Messages.Count(m => m.Message?.Contains($"Computed version {semVer}", StringComparison.Ordinal) is true); } diff --git a/tests/Buildvana.Sdk.Tasks.Tests/ConvertPfxToSnkTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/ConvertPfxToSnkTests.cs index df1d162c..3315ec7a 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/ConvertPfxToSnkTests.cs +++ b/tests/Buildvana.Sdk.Tasks.Tests/ConvertPfxToSnkTests.cs @@ -43,7 +43,7 @@ await RunInTempDirectory(async (engine, directory) => { var outputPath = Path.Combine(directory, "key.snk"); await Assert.That(CreateTask(engine, pfxPath, "wrong", outputPath).Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1201"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1201"); }).ConfigureAwait(false); } @@ -55,7 +55,7 @@ await RunInTempDirectory(async (engine, directory) => { var outputPath = Path.Combine(directory, "key.snk"); await Assert.That(CreateTask(engine, pfxPath, Password, outputPath).Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1202"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1202"); }).ConfigureAwait(false); } @@ -65,7 +65,7 @@ public async Task Execute_WithMissingPfxPath_LogsBVSDK1050() var engine = new RecordingBuildEngine(); await Assert.That(CreateTask(engine, string.Empty, Password, "key.snk").Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1050"); } private static ConvertPfxToSnk CreateTask(IBuildEngine engine, string pfxPath, string password, string outputPath) diff --git a/tests/Buildvana.Sdk.Tasks.Tests/TaskLoggingHelperReporterTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/TaskLoggingHelperReporterTests.cs index 909eeef2..cbc1198b 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/TaskLoggingHelperReporterTests.cs +++ b/tests/Buildvana.Sdk.Tasks.Tests/TaskLoggingHelperReporterTests.cs @@ -11,6 +11,8 @@ internal sealed class TaskLoggingHelperReporterTests public async Task Constructor_NullLog_Throws() { var engine = new RecordingBuildEngine(); + + // ! Null on purpose: testing null detection await Assert.That(() => new TaskLoggingHelperReporter(null!, engine)).Throws(); } @@ -18,6 +20,8 @@ public async Task Constructor_NullLog_Throws() public async Task Constructor_NullEngine_Throws() { var task = new ReporterProbeTask { BuildEngine = new RecordingBuildEngine() }; + + // ! Null on purpose: testing null detection await Assert.That(() => new TaskLoggingHelperReporter(task.Log, null!)).Throws(); } @@ -72,6 +76,8 @@ public async Task Report_UnknownLevel_Throws() public async Task Report_NullMessage_Throws() { var (reporter, _) = CreateReporter(); + + // ! Null on purpose: testing null detection await Assert.That(() => reporter.Report(MessageLevel.Info, null!)).Throws(); } @@ -148,6 +154,8 @@ public async Task BeginActivity_Nested_TracksDepth() public async Task BeginActivity_NullTitle_Throws() { var (reporter, _) = CreateReporter(); + + // ! Null on purpose: testing null detection await Assert.That(() => reporter.BeginActivity(null!)).Throws(); } @@ -162,6 +170,8 @@ public async Task BeginActivity_EmptyTitle_Throws() public async Task ChildOutput_NullLine_Throws() { var (reporter, _) = CreateReporter(); + + // ! Null on purpose: testing null detection await Assert.That(() => reporter.ChildOutput(null!, null)).Throws(); } @@ -169,6 +179,8 @@ public async Task ChildOutput_NullLine_Throws() public async Task ChildError_NullLine_Throws() { var (reporter, _) = CreateReporter(); + + // ! Null on purpose: testing null detection await Assert.That(() => reporter.ChildError(null!, null)).Throws(); } diff --git a/tests/Buildvana.Sdk.Tasks.Tests/WritePackagePinDumpTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/WritePackagePinDumpTests.cs index 08323ffe..577f5b77 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/WritePackagePinDumpTests.cs +++ b/tests/Buildvana.Sdk.Tasks.Tests/WritePackagePinDumpTests.cs @@ -144,7 +144,7 @@ public async Task Execute_WithMissingOutputDirectory_LogsError() var task = CreateTask(engine, string.Empty); await Assert.That(task.Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1050"); } private static WritePackagePinDump CreateTask(IBuildEngine engine, string outputDirectory) diff --git a/tests/Buildvana.Sdk.Tasks.Tests/WriteThisAssemblyConstantsFileTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/WriteThisAssemblyConstantsFileTests.cs index 693dda8b..66c246d1 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/WriteThisAssemblyConstantsFileTests.cs +++ b/tests/Buildvana.Sdk.Tasks.Tests/WriteThisAssemblyConstantsFileTests.cs @@ -71,7 +71,7 @@ public async Task Execute_WithMissingOutputPath_LogsError() var task = CreateTask(engine, string.Empty, ("Answer", "42")); await Assert.That(task.Execute()).IsFalse(); await Assert.That(engine.Errors.Count).IsEqualTo(1); - await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050"); + await Assert.That(engine.Errors[0].Message).Contains("BVSDK1050"); } private static WriteThisAssemblyConstantsFile CreateTask( diff --git a/tests/Buildvana.Sdk.Tests/AdditionalAssemblyInfoModuleTests.cs b/tests/Buildvana.Sdk.Tests/AdditionalAssemblyInfoModuleTests.cs index b01f81cc..5754b970 100644 --- a/tests/Buildvana.Sdk.Tests/AdditionalAssemblyInfoModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/AdditionalAssemblyInfoModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -104,8 +103,5 @@ private static (string Value, string[] CompilerVisibleProperties) Evaluate( } private static string GetRealTargetsPath() - => typeof(AdditionalAssemblyInfoModuleTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealAdditionalAssemblyInfoModuleCoreTargetsPath") - .Value!; + => typeof(AdditionalAssemblyInfoModuleTests).Assembly.GetMetadata("RealAdditionalAssemblyInfoModuleCoreTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/AssemblySigningModuleTests.cs b/tests/Buildvana.Sdk.Tests/AssemblySigningModuleTests.cs index 87a0ef11..bc98c16b 100644 --- a/tests/Buildvana.Sdk.Tests/AssemblySigningModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/AssemblySigningModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Buildvana.Core.Testing; @@ -100,8 +99,5 @@ private static void WritePfx(string path) } private static string GetRealTargetsPath() - => typeof(AssemblySigningModuleTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealAssemblySigningModuleTargetsPath") - .Value!; + => typeof(AssemblySigningModuleTests).Assembly.GetMetadata("RealAssemblySigningModuleTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/CheckRoslynVersionTests.cs b/tests/Buildvana.Sdk.Tests/CheckRoslynVersionTests.cs index 66d6ce5f..210b59c3 100644 --- a/tests/Buildvana.Sdk.Tests/CheckRoslynVersionTests.cs +++ b/tests/Buildvana.Sdk.Tests/CheckRoslynVersionTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -71,8 +70,5 @@ private static (bool Succeeded, IReadOnlyList Errors) Run(string languag } private static string GetRealTargetsPath() - => typeof(CheckRoslynVersionTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealSourceGeneratorsAfterModulesCoreTargetsPath") - .Value!; + => typeof(CheckRoslynVersionTests).Assembly.GetMetadata("RealSourceGeneratorsAfterModulesCoreTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/FileBasedAppsModuleTests.cs b/tests/Buildvana.Sdk.Tests/FileBasedAppsModuleTests.cs index acc86617..db7106ce 100644 --- a/tests/Buildvana.Sdk.Tests/FileBasedAppsModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/FileBasedAppsModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -129,8 +128,5 @@ private static string GetVersion(Project project, string itemType) ?.GetMetadataValue("Version") ?? string.Empty; private static string GetRealTargetsPath() - => typeof(FileBasedAppsModuleTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealFileBasedAppsModuleTargetsPath") - .Value!; + => typeof(FileBasedAppsModuleTests).Assembly.GetMetadata("RealFileBasedAppsModuleTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/InnoSetupTargetsTests.cs b/tests/Buildvana.Sdk.Tests/InnoSetupTargetsTests.cs index 5c1e45cd..b865e553 100644 --- a/tests/Buildvana.Sdk.Tests/InnoSetupTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/InnoSetupTargetsTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -115,8 +114,5 @@ private static (bool Succeeded, Dictionary Items) R } private static string GetRealTargetsPath() - => typeof(InnoSetupTargetsTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealAlternatePackInnoSetupTargetsPath") - .Value!; + => typeof(InnoSetupTargetsTests).Assembly.GetMetadata("RealAlternatePackInnoSetupTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/ModulePagesTests.cs b/tests/Buildvana.Sdk.Tests/ModulePagesTests.cs index 96eede45..00f2c36c 100644 --- a/tests/Buildvana.Sdk.Tests/ModulePagesTests.cs +++ b/tests/Buildvana.Sdk.Tests/ModulePagesTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using System.Text.RegularExpressions; using Buildvana.Core.Testing; @@ -11,10 +10,7 @@ // reads it. internal sealed partial class ModulePagesTests { - private static readonly string RepositoryRoot = typeof(ModulePagesTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(ModulePagesTests).Assembly.GetMetadata("RepositoryRoot"); // Every module has one page whose title names it, and every page names a module. [Test] diff --git a/tests/Buildvana.Sdk.Tests/PackableProjectTargetsTests.cs b/tests/Buildvana.Sdk.Tests/PackableProjectTargetsTests.cs index a523c464..b0e6527c 100644 --- a/tests/Buildvana.Sdk.Tests/PackableProjectTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/PackableProjectTargetsTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -69,8 +68,5 @@ private static (string RequireLicenseAcceptance, IReadOnlyList ErrorCode } private static string GetRealPackableProjectTargetsPath() - => typeof(PackableProjectTargetsTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealNuGetPackPackableProjectTargetsPath") - .Value!; + => typeof(PackableProjectTargetsTests).Assembly.GetMetadata("RealNuGetPackPackableProjectTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/PinDumpFixture.cs b/tests/Buildvana.Sdk.Tests/PinDumpFixture.cs index 3e81af05..b6a4c777 100644 --- a/tests/Buildvana.Sdk.Tests/PinDumpFixture.cs +++ b/tests/Buildvana.Sdk.Tests/PinDumpFixture.cs @@ -1,9 +1,10 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; +using System.Diagnostics; using System.Text.Json; using Buildvana.Core.Dependencies; +using Buildvana.Core.Testing; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -32,7 +33,9 @@ public PinDumpFixture() public string WriteFile(string relativePath, string content) { var path = Path.Combine(ProjectDirectory, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); return path; } @@ -60,10 +63,7 @@ .. Directory.EnumerateFiles(DumpDirectory) public void Dispose() => Directory.Delete(_root, recursive: true); private static string GetMetadata(string key) - => typeof(PinDumpFixture).Assembly - .GetCustomAttributes() - .First(attribute => attribute.Key == key) - .Value!; + => typeof(PinDumpFixture).Assembly.GetMetadata(key); private static PackagePinDump ReadDump(string path) => JsonSerializer.Deserialize(File.ReadAllText(path), PackagePinDumpJsonContext.Default.PackagePinDump) diff --git a/tests/Buildvana.Sdk.Tests/PublishToFoldersTargetsTests.cs b/tests/Buildvana.Sdk.Tests/PublishToFoldersTargetsTests.cs index c1264b44..3fc2ea20 100644 --- a/tests/Buildvana.Sdk.Tests/PublishToFoldersTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/PublishToFoldersTargetsTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; @@ -112,8 +111,5 @@ private static (bool Succeeded, Dictionary Items) R } private static string GetRealTargetsPath() - => typeof(PublishToFoldersTargetsTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealAlternatePackPublishToFoldersTargetsPath") - .Value!; + => typeof(PublishToFoldersTargetsTests).Assembly.GetMetadata("RealAlternatePackPublishToFoldersTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs b/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs index e039b545..67374b7d 100644 --- a/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -54,8 +53,5 @@ private static (string ReadmePath, IReadOnlyList ErrorCodes) Evaluate(Te } private static string GetRealReadmeTargetsPath() - => typeof(ReadmeTargetsTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealNuGetPackReadmeTargetsPath") - .Value!; + => typeof(ReadmeTargetsTests).Assembly.GetMetadata("RealNuGetPackReadmeTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/ReleaseAssetListModuleTests.cs b/tests/Buildvana.Sdk.Tests/ReleaseAssetListModuleTests.cs index 414a8983..2d6496c7 100644 --- a/tests/Buildvana.Sdk.Tests/ReleaseAssetListModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/ReleaseAssetListModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -117,8 +116,5 @@ private static (bool Succeeded, bool ListExists, string[] Lines) Run( } private static string GetRealTargetsPath() - => typeof(ReleaseAssetListModuleTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealReleaseAssetListModuleTargetsPath") - .Value!; + => typeof(ReleaseAssetListModuleTests).Assembly.GetMetadata("RealReleaseAssetListModuleTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/SdkDiagnosticsPageTests.cs b/tests/Buildvana.Sdk.Tests/SdkDiagnosticsPageTests.cs index 0e270e03..6742204e 100644 --- a/tests/Buildvana.Sdk.Tests/SdkDiagnosticsPageTests.cs +++ b/tests/Buildvana.Sdk.Tests/SdkDiagnosticsPageTests.cs @@ -2,7 +2,6 @@ // See the LICENSE file in the project root for full license information. using System.Globalization; -using System.Reflection; using System.Text.RegularExpressions; using Buildvana.Core.Testing; @@ -15,10 +14,7 @@ internal sealed partial class SdkDiagnosticsPageTests { private const string CodePrefix = "BVSDK"; - private static readonly string RepositoryRoot = typeof(SdkDiagnosticsPageTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(SdkDiagnosticsPageTests).Assembly.GetMetadata("RepositoryRoot"); // Every code the SDK raises is on the page, and every code on the page is raised. [Test] diff --git a/tests/Buildvana.Sdk.Tests/SdkPropsFixture.cs b/tests/Buildvana.Sdk.Tests/SdkPropsFixture.cs index 824481a6..febea932 100644 --- a/tests/Buildvana.Sdk.Tests/SdkPropsFixture.cs +++ b/tests/Buildvana.Sdk.Tests/SdkPropsFixture.cs @@ -1,7 +1,8 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; +using System.Diagnostics; +using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -49,7 +50,9 @@ public SdkPropsFixture() public void WriteFile(string relativePath, string content = "") { var path = Path.Combine(RepoDirectory, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } @@ -123,10 +126,7 @@ private static (string SdkPropsPath, string SdkTargetsPath) BuildSdkLayout(strin } private static string GetRealPath(string key) - => typeof(SdkPropsFixture).Assembly - .GetCustomAttributes() - .First(attribute => attribute.Key == key) - .Value!; + => typeof(SdkPropsFixture).Assembly.GetMetadata(key); private static SdkEvaluationResult EvaluateProject(string projectPath) { diff --git a/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTargetsTests.cs b/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTargetsTests.cs index d318a0a0..86bb8941 100644 --- a/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTargetsTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -276,8 +275,5 @@ .. project.GetItems("AdditionalFiles") ]; private static string GetRealTargetsPath() - => typeof(StandardAnalyzersModuleTargetsTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealStandardAnalyzersModuleTargetsPath") - .Value!; + => typeof(StandardAnalyzersModuleTargetsTests).Assembly.GetMetadata("RealStandardAnalyzersModuleTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTests.cs b/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTests.cs index 7cbd80f2..c3771652 100644 --- a/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/StandardAnalyzersModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -227,8 +226,5 @@ private static void WritePair(TempHome home, string directory, string prefix) } private static string GetRealTargetsPath() - => typeof(StandardAnalyzersModuleTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RealStandardAnalyzersUsePublicApiAnalyzersTargetsPath") - .Value!; + => typeof(StandardAnalyzersModuleTests).Assembly.GetMetadata("RealStandardAnalyzersUsePublicApiAnalyzersTargetsPath"); } diff --git a/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs b/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs index ffdddcfb..327732a7 100644 --- a/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs +++ b/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs @@ -1,7 +1,8 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; +using System.Diagnostics; +using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -52,14 +53,13 @@ public IReadOnlyList EvaluateItems(string itemType, bool suppressOverrid public void Dispose() => Directory.Delete(_root, recursive: true); private static string GetMetadata(string key) - => typeof(TransitiveOverridesFixture).Assembly - .GetCustomAttributes() - .First(attribute => attribute.Key == key) - .Value!; + => typeof(TransitiveOverridesFixture).Assembly.GetMetadata(key); private static void Write(string path, string content) { - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } } diff --git a/tests/Buildvana.Sdk.Tests/XmlDocumentationModuleTests.cs b/tests/Buildvana.Sdk.Tests/XmlDocumentationModuleTests.cs index 2057ad3e..de5bfcea 100644 --- a/tests/Buildvana.Sdk.Tests/XmlDocumentationModuleTests.cs +++ b/tests/Buildvana.Sdk.Tests/XmlDocumentationModuleTests.cs @@ -1,7 +1,6 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. -using System.Reflection; using Buildvana.Core.Testing; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; @@ -112,8 +111,5 @@ private static (string GenerateDocumentationFile, string DocumentationFile, stri } private static string GetRealPath(string key) - => typeof(XmlDocumentationModuleTests).Assembly - .GetCustomAttributes() - .First(attribute => attribute.Key == key) - .Value!; + => typeof(XmlDocumentationModuleTests).Assembly.GetMetadata(key); } diff --git a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs index ce7ff411..9b534688 100644 --- a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs @@ -28,7 +28,7 @@ internal sealed class AdditionalGroupPinReaderTests public async Task ReadAsync_StatesThePinsOfEveryGroupFile() { using var home = new TempHome(); - Write(home, GroupFile, GroupFileContent); + home.WriteFile(GroupFile, GroupFileContent); var runner = Answer(home, ("StyleCop.Analyzers", "1.2.0-beta.556", "patch-"), ("Tools.InnoSetup", "7.1.0", null)); var pins = await ReadAsync(home, runner).ConfigureAwait(false); await Assert.That(pins.Select(static pin => pin.Id + " " + pin.VersionText)) @@ -44,7 +44,7 @@ await Assert.That(pins.Select(static pin => pin.Id + " " + pin.VersionText)) public async Task ReadAsync_AsksMsBuildForTheGroupsItemName() { using var home = new TempHome(); - Write(home, GroupFile, GroupFileContent); + home.WriteFile(GroupFile, GroupFileContent); var runner = Answer(home, ("Tools.InnoSetup", "7.1.0", null)); _ = await ReadAsync(home, runner).ConfigureAwait(false); var args = string.Join(" ", runner.Runs.Single().Args); @@ -70,7 +70,7 @@ public async Task ReadAsync_WithNoGroup_EvaluatesNothing() public async Task ReadAsync_LeavesOutAnItemDeclaredOutsideTheGlob() { using var home = new TempHome(); - Write(home, GroupFile, GroupFileContent); + home.WriteFile(GroupFile, GroupFileContent); var elsewhere = home.GetFullPath("src/Other/Imported.props"); var runner = Answer([("Tools.InnoSetup", "7.1.0", null)], declaringFile: elsewhere); await Assert.That(await ReadAsync(home, runner).ConfigureAwait(false)).IsEmpty(); @@ -80,7 +80,7 @@ public async Task ReadAsync_LeavesOutAnItemDeclaredOutsideTheGlob() public async Task ReadAsync_LeavesTheFamilyOut() { using var home = new TempHome(); - Write(home, GroupFile, GroupFileContent); + home.WriteFile(GroupFile, GroupFileContent); var runner = Answer(home, ("Buildvana.Runtime", "2.1.0", null)); await Assert.That(await ReadAsync(home, runner).ConfigureAwait(false)).IsEmpty(); } @@ -102,7 +102,7 @@ public async Task ReadAsync_OfAVersionStatedThroughAProperty_IsIndirect() """; using var home = new TempHome(); - Write(home, GroupFile, content); + home.WriteFile(GroupFile, content); var runner = Answer(home, ("Tools.InnoSetup", "7.1.0", null)); var pins = await ReadAsync(home, runner).ConfigureAwait(false); await Assert.That(pins.Single().Management).IsEqualTo(PinManagement.IndirectVersion); @@ -127,7 +127,7 @@ public async Task ReadAsync_OfAVersionStatedAsAChildElement_IsManaged() """; using var home = new TempHome(); - Write(home, GroupFile, content); + home.WriteFile(GroupFile, content); var runner = Answer(home, ("Tools.InnoSetup", @"\n 7.1.0\n ", null)); var pin = (await ReadAsync(home, runner).ConfigureAwait(false)).Single(); await Assert.That(pin.Management).IsEqualTo(PinManagement.Managed); @@ -138,7 +138,7 @@ public async Task ReadAsync_OfAVersionStatedAsAChildElement_IsManaged() public async Task ReadAsync_WhenEvaluationFails_ReportsAFailedStep() { using var home = new TempHome(); - Write(home, GroupFile, GroupFileContent); + home.WriteFile(GroupFile, GroupFileContent); var runner = new FakeProcessRunner { OnRun = static (executable, _) => new ProcessResult(executable, 1, "error MSB4025: no.", string.Empty, TimeSpan.Zero), @@ -198,11 +198,4 @@ private static FakeProcessRunner Answer( } private static string JsonPath(string path) => "\"" + path.Replace(@"\", @"\\", StringComparison.Ordinal) + "\""; - - private static void Write(TempHome home, string relativePath, string content) - { - var path = home.GetFullPath(relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, content); - } } diff --git a/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs b/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs index 83f1ee77..b43141c5 100644 --- a/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs +++ b/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs @@ -35,8 +35,7 @@ public async Task FlagTemplate_ParsesLongNameAndNoValuePlaceholder() public async Task Attribute_IsDiscoverableViaReflection() { var property = typeof(BvOptionAttributeTests).GetProperty(nameof(SampleValueOption), BindingFlags.Instance | BindingFlags.NonPublic); - var attribute = property!.GetCustomAttribute(); - await Assert.That(attribute).IsNotNull(); - await Assert.That(attribute!.ValueName).IsEqualTo("NAME"); + var attribute = await Assert.That(property?.GetCustomAttribute()).IsNotNull(); + await Assert.That(attribute.ValueName).IsEqualTo("NAME"); } } diff --git a/tests/Buildvana.Tool.Tests/CIPlatformTests.cs b/tests/Buildvana.Tool.Tests/CIPlatformTests.cs index 15c66cca..4109a06d 100644 --- a/tests/Buildvana.Tool.Tests/CIPlatformTests.cs +++ b/tests/Buildvana.Tool.Tests/CIPlatformTests.cs @@ -28,7 +28,7 @@ public async Task Detect_WithGitHubActionsTrue_ReturnsGitHubActionsPlatform(stri await Assert.That(platform).IsTypeOf(); await Assert.That(platform.IsCI).IsTrue(); - await Assert.That(platform.BotIdentity!.Name).IsEqualTo("github-actions[bot]"); + await Assert.That(platform.BotIdentity?.Name).IsEqualTo("github-actions[bot]"); await Assert.That(platform.HasStepOutputs).IsTrue(); } @@ -46,8 +46,8 @@ public async Task Detect_WithGiteaActionsTrue_ReturnsGiteaActionsPlatform(string await Assert.That(platform).IsTypeOf(); await Assert.That(platform.IsCI).IsTrue(); - await Assert.That(platform.BotIdentity!.Name).IsEqualTo("gitea-actions"); - await Assert.That(platform.BotIdentity!.Email).IsEqualTo("gitea-actions@noreply.gitea.example.invalid"); + await Assert.That(platform.BotIdentity?.Name).IsEqualTo("gitea-actions"); + await Assert.That(platform.BotIdentity?.Email).IsEqualTo("gitea-actions@noreply.gitea.example.invalid"); await Assert.That(platform.HasStepOutputs).IsTrue(); } @@ -61,8 +61,8 @@ public async Task Detect_WithForgejoActionsTrue_ReturnsForgejoActionsPlatform(st await Assert.That(platform).IsTypeOf(); await Assert.That(platform.IsCI).IsTrue(); - await Assert.That(platform.BotIdentity!.Name).IsEqualTo("forgejo-actions"); - await Assert.That(platform.BotIdentity!.Email).IsEqualTo("noreply@forgejo.org"); + await Assert.That(platform.BotIdentity?.Name).IsEqualTo("forgejo-actions"); + await Assert.That(platform.BotIdentity?.Email).IsEqualTo("noreply@forgejo.org"); await Assert.That(platform.HasStepOutputs).IsTrue(); } @@ -76,7 +76,7 @@ public async Task Detect_WithGitLabCISet_ReturnsGitLabCIPlatform(string value) await Assert.That(platform).IsTypeOf(); await Assert.That(platform.IsCI).IsTrue(); - await Assert.That(platform.BotIdentity!.Email).IsEqualTo("gitlab-ci@noreply.gitlab.example.invalid"); + await Assert.That(platform.BotIdentity?.Email).IsEqualTo("gitlab-ci@noreply.gitlab.example.invalid"); await Assert.That(platform.HasStepOutputs).IsFalse(); } @@ -202,7 +202,7 @@ public async Task Detect_OnAPlatformWithAServer_TakesTheServerFromItsVariable( { var platform = Detect((marker, "true"), (serverVariable, "https://git.example.invalid")); - await Assert.That(platform.ServerUrl!.Host).IsEqualTo("git.example.invalid"); + await Assert.That(platform.ServerUrl?.Host).IsEqualTo("git.example.invalid"); await Assert.That(platform.ServerKind).IsEqualTo(kind); } diff --git a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs index 9a1cde97..36c54a08 100644 --- a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs @@ -10,7 +10,7 @@ internal sealed class CommandArgumentValidatorTests [Test] public async Task ForwardingCommand_AcceptsTokensAfterSeparator() { - var command = CommandRegistry.Find("build")!; + var command = await Assert.That(CommandRegistry.Find("build")).IsNotNull(); var parsed = CliArgSplitter.Split(["build", "--", "-p:Foo=Bar"]); var parameters = CommandArgumentValidator.Validate(command, parsed, parsed.Positionals); await Assert.That(Join(parameters.Forwarded)).IsEqualTo("-p:Foo=Bar"); @@ -19,7 +19,7 @@ public async Task ForwardingCommand_AcceptsTokensAfterSeparator() [Test] public async Task ForwardingCommand_RejectsOptionsBeforeSeparator() { - var command = CommandRegistry.Find("build")!; + var command = await Assert.That(CommandRegistry.Find("build")).IsNotNull(); var parsed = CliArgSplitter.Split(["build", "-p:Foo"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)).Throws(); } @@ -27,7 +27,7 @@ public async Task ForwardingCommand_RejectsOptionsBeforeSeparator() [Test] public async Task ForwardingCommand_RejectsPositionals() { - var command = CommandRegistry.Find("build")!; + var command = await Assert.That(CommandRegistry.Find("build")).IsNotNull(); var parsed = CliArgSplitter.Split(["build", "extra"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)).Throws(); } @@ -35,7 +35,7 @@ public async Task ForwardingCommand_RejectsPositionals() [Test] public async Task NonForwardingCommand_RejectsTokensAfterSeparator() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "--", "x"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)).Throws(); } @@ -43,7 +43,7 @@ public async Task NonForwardingCommand_RejectsTokensAfterSeparator() [Test] public async Task NonForwardingCommand_AllowsItsOwnOptionTokens() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "-c", "Debug"]); var parameters = CommandArgumentValidator.Validate(command, parsed, parsed.Positionals); await Assert.That(Join(parameters.Options)).IsEqualTo("-c|Debug"); @@ -52,7 +52,7 @@ public async Task NonForwardingCommand_AllowsItsOwnOptionTokens() [Test] public async Task SettingsLessCommand_RejectsUnknownOption() { - var command = CommandRegistry.Find("clean")!; + var command = await Assert.That(CommandRegistry.Find("clean")).IsNotNull(); var parsed = CliArgSplitter.Split(["clean", "--bogus"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) .Throws() @@ -62,7 +62,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parse [Test] public async Task SettingsLessCommand_RejectsUnknownOption_ViaAlias() { - var command = CommandRegistry.Find("version")!; + var command = await Assert.That(CommandRegistry.Find("version")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "--bogus"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) .Throws() @@ -72,7 +72,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parse [Test] public async Task SettingsLessCommand_ReportsOffendingTokensInCommandLineOrder() { - var command = CommandRegistry.Find("clean")!; + var command = await Assert.That(CommandRegistry.Find("clean")).IsNotNull(); var parsed = CliArgSplitter.Split(["clean", "junk", "--bogus"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) .Throws() @@ -82,7 +82,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parse [Test] public async Task SettingsCarryingCommand_RejectsUnknownOption() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "--bogus"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) .Throws() @@ -92,7 +92,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parse [Test] public async Task SettingsCarryingCommand_ConsumesOptionValue() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "--bump", "minor"]); var parameters = CommandArgumentValidator.Validate(command, parsed, parsed.Positionals); await Assert.That(Join(parameters.Options)).IsEqualTo("--bump|minor"); @@ -102,7 +102,7 @@ public async Task SettingsCarryingCommand_ConsumesOptionValue() [Test] public async Task SettingsCarryingCommand_AllowsInlineOptionValue() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "--bump=minor"]); var parameters = CommandArgumentValidator.Validate(command, parsed, parsed.Positionals); await Assert.That(Join(parameters.Options)).IsEqualTo("--bump=minor"); @@ -111,7 +111,7 @@ public async Task SettingsCarryingCommand_AllowsInlineOptionValue() [Test] public async Task SettingsCarryingCommand_AllowsFlagOption() { - var command = CommandRegistry.Find("version advance")!; + var command = await Assert.That(CommandRegistry.Find("version advance")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "advance", "--force"]); var parameters = CommandArgumentValidator.Validate(command, parsed, []); await Assert.That(Join(parameters.Options)).IsEqualTo("--force"); @@ -120,7 +120,7 @@ public async Task SettingsCarryingCommand_AllowsFlagOption() [Test] public async Task SettingsCarryingCommand_RejectsValueOptionWithoutValue() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "--bump"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)) .Throws() @@ -130,7 +130,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parse [Test] public async Task NonForwardingCommand_RejectsPositionals_WhenNoArgumentsDeclared() { - var command = CommandRegistry.Find("release")!; + var command = await Assert.That(CommandRegistry.Find("release")).IsNotNull(); var parsed = CliArgSplitter.Split(["release", "extra"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, parsed.Positionals)).Throws(); } @@ -138,7 +138,7 @@ public async Task NonForwardingCommand_RejectsPositionals_WhenNoArgumentsDeclare [Test] public async Task ArgumentCommand_AcceptsDeclaredPositional() { - var command = CommandRegistry.Find("version advance")!; + var command = await Assert.That(CommandRegistry.Find("version advance")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "advance", "minor"]); var parameters = CommandArgumentValidator.Validate(command, parsed, ["minor"]); await Assert.That(Join(parameters.Positionals)).IsEqualTo("minor"); @@ -148,7 +148,7 @@ public async Task ArgumentCommand_AcceptsDeclaredPositional() [Test] public async Task ArgumentCommand_AcceptsOmittedOptionalArgument() { - var command = CommandRegistry.Find("version advance")!; + var command = await Assert.That(CommandRegistry.Find("version advance")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "advance"]); var parameters = CommandArgumentValidator.Validate(command, parsed, []); await Assert.That(parameters.Positionals.Count).IsEqualTo(0); @@ -158,7 +158,7 @@ public async Task ArgumentCommand_AcceptsOmittedOptionalArgument() [Test] public async Task ArgumentCommand_RejectsExcessPositionals() { - var command = CommandRegistry.Find("version advance")!; + var command = await Assert.That(CommandRegistry.Find("version advance")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "advance", "minor", "extra"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, ["minor", "extra"])).Throws(); } @@ -190,7 +190,7 @@ public async Task VariadicArgumentCommand_AcceptsNoPositionalAtAll() [Test] public async Task OperandAfterFlag_BindsAsPositional() { - var command = CommandRegistry.Find("deps update")!; + var command = await Assert.That(CommandRegistry.Find("deps update")).IsNotNull(); var parsed = CliArgSplitter.Split(["deps", "update", "--check", "CommunityToolkit.Diagnostics"]); var parameters = CommandArgumentValidator.Validate(command, parsed, []); await Assert.That(Join(parameters.Positionals)).IsEqualTo("CommunityToolkit.Diagnostics"); @@ -200,7 +200,7 @@ public async Task OperandAfterFlag_BindsAsPositional() [Test] public async Task OperandAfterValueOption_BindsAsPositional() { - var command = CommandRegistry.Find("deps update")!; + var command = await Assert.That(CommandRegistry.Find("deps update")).IsNotNull(); var parsed = CliArgSplitter.Split(["deps", "update", "--to", "1.2.3", "Serilog"]); var parameters = CommandArgumentValidator.Validate(command, parsed, []); await Assert.That(Join(parameters.Positionals)).IsEqualTo("Serilog"); @@ -210,7 +210,7 @@ public async Task OperandAfterValueOption_BindsAsPositional() [Test] public async Task OperandsOnBothSidesOfAnOption_BindInCommandLineOrder() { - var command = CommandRegistry.Find("deps update")!; + var command = await Assert.That(CommandRegistry.Find("deps update")).IsNotNull(); var parsed = CliArgSplitter.Split(["deps", "update", "Serilog", "--check", "Louis"]); var parameters = CommandArgumentValidator.Validate(command, parsed, ["Serilog"]); await Assert.That(Join(parameters.Positionals)).IsEqualTo("Serilog|Louis"); @@ -220,7 +220,7 @@ public async Task OperandsOnBothSidesOfAnOption_BindInCommandLineOrder() [Test] public async Task UnknownOptionBeforeAnOperand_IsReportedFirst() { - var command = CommandRegistry.Find("deps update")!; + var command = await Assert.That(CommandRegistry.Find("deps update")).IsNotNull(); var parsed = CliArgSplitter.Split(["deps", "update", "--bogus", "Serilog"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) .Throws() @@ -230,7 +230,7 @@ await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) [Test] public async Task ExcessOperandAfterAnOption_IsRejected() { - var command = CommandRegistry.Find("version advance")!; + var command = await Assert.That(CommandRegistry.Find("version advance")).IsNotNull(); var parsed = CliArgSplitter.Split(["version", "advance", "--force", "minor", "extra"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])) .Throws() diff --git a/tests/Buildvana.Tool.Tests/CommandLineOverridesParserTests.cs b/tests/Buildvana.Tool.Tests/CommandLineOverridesParserTests.cs index cc1f8abf..91363ad9 100644 --- a/tests/Buildvana.Tool.Tests/CommandLineOverridesParserTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandLineOverridesParserTests.cs @@ -75,7 +75,7 @@ public async Task Parse_StripsConfigurationTokens_FromForwardedArgs() await Assert.That(overrides.Configuration).IsEqualTo("Debug"); await Assert.That(overrides.ForwardedArgs).IsNotNull(); - await Assert.That(string.Join('|', overrides.ForwardedArgs!)).IsEqualTo("--no-incremental"); + await Assert.That(string.Join('|', overrides.ForwardedArgs ?? [])).IsEqualTo("--no-incremental"); } [Test] @@ -86,7 +86,7 @@ public async Task Parse_KeepsOtherForwardedArgsVerbatim() await Assert.That(overrides.Configuration).IsNull(); await Assert.That(overrides.ForwardedArgs).IsNotNull(); - await Assert.That(string.Join('|', overrides.ForwardedArgs!)).IsEqualTo("-m:8|-v:minimal"); + await Assert.That(string.Join('|', overrides.ForwardedArgs ?? [])).IsEqualTo("-m:8|-v:minimal"); } // The cost of ownership: a trailing `-c` that was meant as another forwarded option's value is read diff --git a/tests/Buildvana.Tool.Tests/CommandPagesTests.cs b/tests/Buildvana.Tool.Tests/CommandPagesTests.cs index 8705fcc5..4971bcf2 100644 --- a/tests/Buildvana.Tool.Tests/CommandPagesTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandPagesTests.cs @@ -12,10 +12,7 @@ // first: `-c, --configuration `. The repository root reaches the tests as ToolDiagnosticsPageTests reads it. internal sealed class CommandPagesTests { - private static readonly string RepositoryRoot = typeof(CommandPagesTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(CommandPagesTests).Assembly.GetMetadata("RepositoryRoot"); // The page under docs/tool-commands of each top-level command name. A group page holds one "Options" table // for all of its subcommands, with a "Subcommand" column. @@ -61,7 +58,7 @@ public async Task GlobalOptionsTable_ListsTheOptionsOfGlobalSettings() [MethodDataSource(nameof(CommandNames))] public async Task OptionsTable_ListsTheOptionsOfTheSettingsType(string name) { - var command = CommandRegistry.Find(name)!; + var command = await Assert.That(CommandRegistry.Find(name)).IsNotNull(); await Assert.That(Pages.Keys).Contains(command.CanonicalPath[0]); var page = LoadPage(Path.Combine("tool-commands", Pages[command.CanonicalPath[0]])); var listed = ListedOptions(page, command); @@ -84,8 +81,8 @@ private static IEnumerable RenderOptions(Type settingsType) => settingsType .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Select(static property => property.GetCustomAttribute()) - .Where(static option => option is not null) - .Select(static option => RenderOption(option!)); + .OfType() + .Select(RenderOption); private static string RenderOption(BvOptionAttribute option) { diff --git a/tests/Buildvana.Tool.Tests/CommandRegistryTests.cs b/tests/Buildvana.Tool.Tests/CommandRegistryTests.cs index 5e266274..5e2db7ba 100644 --- a/tests/Buildvana.Tool.Tests/CommandRegistryTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandRegistryTests.cs @@ -132,8 +132,8 @@ public async Task BuildTree_LinksEveryDepthOfAnAliasPath() { var command = new CommandRegistration([["alpha", "show"], ["al", "show"]], typeof(object), false, null); var topLevelNodes = CommandRegistry.BuildTree([command]); - var alias = topLevelNodes.Single(n => n.Name == "al").FindChild("show")!; - await Assert.That(alias.CanonicalNode?.FullName).IsEqualTo("alpha show"); + var alias = topLevelNodes.Single(n => n.Name == "al").FindChild("show"); + await Assert.That(alias?.CanonicalNode?.FullName).IsEqualTo("alpha show"); } [Test] diff --git a/tests/Buildvana.Tool.Tests/ConsoleEncodingScopeTests.cs b/tests/Buildvana.Tool.Tests/ConsoleEncodingScopeTests.cs index ebe8a4d8..1c8c01d0 100644 --- a/tests/Buildvana.Tool.Tests/ConsoleEncodingScopeTests.cs +++ b/tests/Buildvana.Tool.Tests/ConsoleEncodingScopeTests.cs @@ -55,7 +55,7 @@ public async Task IsDefaultEncodingRequested_AcceptsOnlyTheLiteralOne(string? va [Arguments(typeof(InvalidOperationException), false)] public async Task IsConsoleEncodingFailure_AcceptsOnlyFailuresOfTheConsole(Type exceptionType, bool expected) { - var exception = (Exception)Activator.CreateInstance(exceptionType)!; + var exception = await Assert.That(Activator.CreateInstance(exceptionType) as Exception).IsNotNull(); await Assert.That(ConsoleEncodingScope.IsConsoleEncodingFailure(exception)).IsEqualTo(expected); } } diff --git a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs index c8576241..fffce4e2 100644 --- a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs @@ -254,7 +254,8 @@ public async Task TryDelegate_RestoresThenRunsPinnedBvWithOriginalArguments() await Assert.That(restoreArgs).IsEquivalentTo(["tool", "restore"], CollectionOrdering.Matching); // Home-directory discovery reports the home path with a trailing separator, per its contract. - await Assert.That(Path.TrimEndingDirectorySeparator(restoreDirectory!)).IsEqualTo(home.RootPath); + var directory = await Assert.That(restoreDirectory).IsNotNull(); + await Assert.That(Path.TrimEndingDirectorySeparator(directory)).IsEqualTo(home.RootPath); await Assert.That(runner.InheritedStdioRuns.Count).IsEqualTo(1); var run = runner.InheritedStdioRuns[0]; @@ -264,9 +265,10 @@ public async Task TryDelegate_RestoresThenRunsPinnedBvWithOriginalArguments() await Assert.That(run.Args).IsEquivalentTo( ["tool", "run", "bv", "--", "build", "--verbosity", "detailed", "--", "/p:Answer=42"], CollectionOrdering.Matching); - await Assert.That(Path.TrimEndingDirectorySeparator(run.WorkingDirectory!)).IsEqualTo(home.RootPath); + var workingDirectory = await Assert.That(run.WorkingDirectory).IsNotNull(); + await Assert.That(Path.TrimEndingDirectorySeparator(workingDirectory)).IsEqualTo(home.RootPath); await Assert.That(run.Environment).IsNotNull(); - await Assert.That(run.Environment!["BV_DELEGATED"]).IsEqualTo(OwnVersion); + await Assert.That(run.Environment?["BV_DELEGATED"]).IsEqualTo(OwnVersion); } // The pinned bv is already installed: `dotnet tool run` needs no restore, so none must run — the common diff --git a/tests/Buildvana.Tool.Tests/DependencyResolutionTests.cs b/tests/Buildvana.Tool.Tests/DependencyResolutionTests.cs index a8552cd3..028953ee 100644 --- a/tests/Buildvana.Tool.Tests/DependencyResolutionTests.cs +++ b/tests/Buildvana.Tool.Tests/DependencyResolutionTests.cs @@ -44,9 +44,10 @@ public async Task Skipping_WithANetSdkPin_StatesItSkippedAndWritesNothing() var resolution = Skipping(inventory); - await Assert.That(resolution.NetSdk!.Pin).IsEqualTo(inventory.NetSdk); - await Assert.That(resolution.NetSdk.State).IsEqualTo(PinResolutionState.Skipped); - await Assert.That(resolution.NetSdk.WritesAllowPrerelease).IsFalse(); + var netSdk = await Assert.That(resolution.NetSdk).IsNotNull(); + await Assert.That(netSdk.Pin).IsEqualTo(inventory.NetSdk); + await Assert.That(netSdk.State).IsEqualTo(PinResolutionState.Skipped); + await Assert.That(netSdk.WritesAllowPrerelease).IsFalse(); } [Test] diff --git a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs index a6bfb378..766f29d8 100644 --- a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs @@ -115,9 +115,10 @@ public async Task ResolveAsync_MovesTheNetSdkWithinItsPolicy() { var releases = new FakeNetSdkReleaseSource().Knows(isLts: true, "10.0.100", "10.0.101", "10.0.201"); var resolution = await ResolveNetSdkAsync(releases, NetSdkPin.Create("10.0.100", allowPrerelease: false)).ConfigureAwait(false); - await Assert.That(resolution.NetSdk!.State).IsEqualTo(PinResolutionState.Updated); - await Assert.That(resolution.NetSdk.Target?.ToNormalizedString()).IsEqualTo("10.0.201"); - await Assert.That(resolution.NetSdk.WritesAllowPrerelease).IsFalse(); + var netSdk = await Assert.That(resolution.NetSdk).IsNotNull(); + await Assert.That(netSdk.State).IsEqualTo(PinResolutionState.Updated); + await Assert.That(netSdk.Target?.ToNormalizedString()).IsEqualTo("10.0.201"); + await Assert.That(netSdk.WritesAllowPrerelease).IsFalse(); } [Test] @@ -125,8 +126,9 @@ public async Task ResolveAsync_WhenAllowPrereleaseDisagreesWithThePolicy_SaysItM { var releases = new FakeNetSdkReleaseSource().Knows(isLts: true, "10.0.100"); var resolution = await ResolveNetSdkAsync(releases, NetSdkPin.Create("10.0.100", allowPrerelease: null)).ConfigureAwait(false); - await Assert.That(resolution.NetSdk!.WritesAllowPrerelease).IsTrue(); - await Assert.That(resolution.NetSdk.Note).IsNotEmpty(); + var netSdk = await Assert.That(resolution.NetSdk).IsNotNull(); + await Assert.That(netSdk.WritesAllowPrerelease).IsTrue(); + await Assert.That(netSdk.Note).IsNotEmpty(); } [Test] @@ -287,9 +289,10 @@ public async Task ResolveAsync_AskedForTheLatest_MovesTheNetSdkPastItsPolicy() var request = new DependencyResolutionRequest { Latest = true }; var pin = NetSdkPin.Create("10.0.100", allowPrerelease: false); var resolution = await ResolveNetSdkAsync(releases, pin, request, config).ConfigureAwait(false); - await Assert.That(resolution.NetSdk!.State).IsEqualTo(PinResolutionState.Updated); - await Assert.That(resolution.NetSdk.Target?.ToNormalizedString()).IsEqualTo("11.0.100"); - await Assert.That(resolution.NetSdk.Policy.ToString()).IsEqualTo("lts"); + var netSdk = await Assert.That(resolution.NetSdk).IsNotNull(); + await Assert.That(netSdk.State).IsEqualTo(PinResolutionState.Updated); + await Assert.That(netSdk.Target?.ToNormalizedString()).IsEqualTo("11.0.100"); + await Assert.That(netSdk.Policy.ToString()).IsEqualTo("lts"); } [Test] @@ -299,8 +302,9 @@ public async Task ResolveAsync_WithAStatedVersionForTheNetSdk_TakesIt() var request = new DependencyResolutionRequest { To = NuGetVersion.Parse("10.0.100") }; var pin = NetSdkPin.Create("10.0.201", allowPrerelease: false); var resolution = await ResolveNetSdkAsync(releases, pin, request).ConfigureAwait(false); - await Assert.That(resolution.NetSdk!.State).IsEqualTo(PinResolutionState.Updated); - await Assert.That(resolution.NetSdk.Target?.ToNormalizedString()).IsEqualTo("10.0.100"); + var netSdk = await Assert.That(resolution.NetSdk).IsNotNull(); + await Assert.That(netSdk.State).IsEqualTo(PinResolutionState.Updated); + await Assert.That(netSdk.Target?.ToNormalizedString()).IsEqualTo("10.0.100"); } [Test] diff --git a/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs b/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs index adc9640e..03b1eac9 100644 --- a/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs @@ -21,7 +21,7 @@ internal sealed class DirectivePinReaderTests public async Task Read_StatesOnePinPerVersionedDirective() { using var home = new TempHome(); - Write(home, "tools/report.cs", ToolApp); + home.WriteFile("tools/report.cs", ToolApp); var pins = Read(home); await Assert.That(pins.Select(static pin => pin.Scope + " " + pin.Id + " " + pin.VersionText)) .IsEquivalentTo(["Packages Serilog 4.0.0", "Sdks Microsoft.Build.NoTargets 3.7.0"]); @@ -33,7 +33,7 @@ await Assert.That(pins.Select(static pin => pin.Scope + " " + pin.Id + " " + pin public async Task Read_IgnoresAFileOutsideTheScope() { using var home = new TempHome(); - Write(home, "src/App/Program.cs", ToolApp); + home.WriteFile("src/App/Program.cs", ToolApp); await Assert.That(Read(home)).IsEmpty(); } @@ -41,7 +41,7 @@ public async Task Read_IgnoresAFileOutsideTheScope() public async Task Read_ReadsTheBuiltInHooksScope() { using var home = new TempHome(); - Write(home, ".buildvana/hooks/release/post-release.cs", ToolApp); + home.WriteFile(".buildvana/hooks/release/post-release.cs", ToolApp); var pins = Read(home, new BuildvanaConfig()); await Assert.That(pins.Select(static pin => pin.Id)).Contains("Serilog"); } @@ -51,7 +51,7 @@ public async Task Read_ReadsTheBuiltInHooksScope() public async Task Read_LeavesAFamilyDirectiveOut() { using var home = new TempHome(); - Write(home, "tools/report.cs", "#:sdk Buildvana.Sdk@2.1.0\n#:package Buildvana.Runtime@2.1.0\n"); + home.WriteFile("tools/report.cs", "#:sdk Buildvana.Sdk@2.1.0\n#:package Buildvana.Runtime@2.1.0\n"); await Assert.That(Read(home)).IsEmpty(); } @@ -61,7 +61,7 @@ public async Task Read_LeavesAFamilyDirectiveOut() public async Task Read_StatesADirectiveWithAnEmptyVersionAsUnmanaged() { using var home = new TempHome(); - Write(home, "tools/report.cs", "#:package Serilog@\n"); + home.WriteFile("tools/report.cs", "#:package Serilog@\n"); await Assert.That(Read(home).Single().Management).IsEqualTo(PinManagement.UnreadableVersion); } @@ -71,7 +71,7 @@ public async Task Read_StatesADirectiveWithAnEmptyVersionAsUnmanaged() public async Task Read_StatesAVersionlessPackageDirectiveAsAReference() { using var home = new TempHome(); - Write(home, "tools/report.cs", ToolApp); + home.WriteFile("tools/report.cs", ToolApp); var read = ReadAll(home); await Assert.That(read.References).IsEquivalentTo(["Spectre.Console"]); } @@ -81,8 +81,8 @@ public async Task Read_StatesAVersionlessPackageDirectiveAsAReference() public async Task Read_NamesAReferencedPackageOnce() { using var home = new TempHome(); - Write(home, "tools/first.cs", "#:package Spectre.Console\n"); - Write(home, "tools/second.cs", "#:package spectre.console\n"); + home.WriteFile("tools/first.cs", "#:package Spectre.Console\n"); + home.WriteFile("tools/second.cs", "#:package spectre.console\n"); var read = ReadAll(home); await Assert.That(read.References.Count).IsEqualTo(1); } @@ -91,7 +91,7 @@ public async Task Read_NamesAReferencedPackageOnce() public async Task Read_IgnoresBuildOutput() { using var home = new TempHome(); - Write(home, "tools/obj/generated.cs", ToolApp); + home.WriteFile("tools/obj/generated.cs", ToolApp); await Assert.That(Read(home)).IsEmpty(); } @@ -99,11 +99,4 @@ public async Task Read_IgnoresBuildOutput() private static DirectivePins ReadAll(TempHome home, BuildvanaConfig? config = null) => new DirectivePinReader(home.Provider, config ?? new BuildvanaConfig { FileBasedApps = ["/tools/"] }).Read(); - - private static void Write(TempHome home, string relativePath, string content) - { - var path = home.GetFullPath(relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, content); - } } diff --git a/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs b/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs index 7207fb36..896c4f9a 100644 --- a/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs +++ b/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs @@ -49,8 +49,8 @@ public async Task GetArtifactsDirectory_DistinguishesDifferentPaths() var first = Path.Combine(Path.GetTempPath(), "first", "app.cs"); var second = Path.Combine(Path.GetTempPath(), "second", "app.cs"); - await Assert.That(FileBasedAppHelper.GetArtifactsDirectory(first)!) - .IsNotEqualTo(FileBasedAppHelper.GetArtifactsDirectory(second)!); + await Assert.That(FileBasedAppHelper.GetArtifactsDirectory(first)) + .IsNotEqualTo(FileBasedAppHelper.GetArtifactsDirectory(second)); } [Test] @@ -84,7 +84,7 @@ public async Task GetArtifactsDirectory_AgreesWithInstalledSdk(CancellationToken RedirectStandardError = true, UseShellExecute = false, }; - using var process = Process.Start(startInfo)!; + using var process = await Assert.That(Process.Start(startInfo)).IsNotNull(); var output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); diff --git a/tests/Buildvana.Tool.Tests/GlobalJsonPinReaderTests.cs b/tests/Buildvana.Tool.Tests/GlobalJsonPinReaderTests.cs index b985e207..0039ffae 100644 --- a/tests/Buildvana.Tool.Tests/GlobalJsonPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/GlobalJsonPinReaderTests.cs @@ -26,7 +26,7 @@ public async Task Read_StatesTheNetSdkBaselineAndItsPrereleaseSetting() """; using var home = new TempHome(); Write(home, content); - var netSdk = CreateReader(home).Read().NetSdk!; + var netSdk = await Assert.That(CreateReader(home).Read().NetSdk).IsNotNull(); await Assert.That(netSdk.VersionText).IsEqualTo("10.0.100"); await Assert.That(netSdk.Version?.ToNormalizedString()).IsEqualTo("10.0.100"); await Assert.That(netSdk.AllowPrerelease).IsTrue(); @@ -39,7 +39,8 @@ public async Task Read_WithNoPrereleaseSetting_LeavesItUnstated() { using var home = new TempHome(); Write(home, """{ "sdk": { "version": "10.0.100" } }"""); - await Assert.That(CreateReader(home).Read().NetSdk!.AllowPrerelease).IsNull(); + var netSdk = await Assert.That(CreateReader(home).Read().NetSdk).IsNotNull(); + await Assert.That(netSdk.AllowPrerelease).IsNull(); } [Test] diff --git a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs index b3e5d7e6..81278389 100644 --- a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs +++ b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs @@ -58,8 +58,7 @@ public async Task RunHookAsync_WritesCamelCaseArgsFileAtWellKnownPath() _ = await runner.RunHookAsync(args).ConfigureAwait(false); - await Assert.That(argsJson).IsNotNull(); - using var document = JsonDocument.Parse(argsJson!); + using var document = JsonDocument.Parse(await Assert.That(argsJson).IsNotNull()); var root = document.RootElement; var runtimeInfo = root.GetProperty("runtimeInfo"); await Assert.That(runtimeInfo.GetProperty("version").GetString()).IsEqualTo("1.2.3-preview"); diff --git a/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs b/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs index a192cf3b..67fffcd6 100644 --- a/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs +++ b/tests/Buildvana.Tool.Tests/OrphanDetectorTests.cs @@ -8,6 +8,7 @@ using Buildvana.Tool.Services; using Buildvana.Tool.Services.Dependencies; using Buildvana.Tool.Services.Solution; +using Microsoft.VisualStudio.SolutionPersistence.Model; internal sealed class OrphanDetectorTests { @@ -229,8 +230,8 @@ private static Task> DetectAsync( IDependencyRestorer? restorer = null, bool restoreOverridesAfterwards = false) { - // The restorer is the only thing that would use the solution, and it is faked. - var solution = new Lazy(static () => null!); + // The restorer is the only thing that would use the solution, and it is faked, so an empty one does. + var solution = new Lazy(static () => new SolutionContext("Test.slnx", new SolutionModel())); var detector = new OrphanDetector(solution, restorer ?? new FakeDependencyRestorer(), new CaptureReporter()); return detector.DetectAsync(inventory, restoreOverridesAfterwards); } diff --git a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs index 87219875..b9248a7d 100644 --- a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs +++ b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs @@ -10,6 +10,7 @@ using Buildvana.Runtime; using Buildvana.Tool.Services.Dependencies; using Buildvana.Tool.Services.Solution; +using Microsoft.VisualStudio.SolutionPersistence.Model; using NuGet.Versioning; internal sealed class OverrideLifecycleTests @@ -306,8 +307,8 @@ private static Task RunAsync( { var actualReporter = reporter ?? new CaptureReporter(); - // The restorer is the only thing that would use the solution, and it is faked. - var solution = new Lazy(static () => null!); + // The restorer is the only thing that would use the solution, and it is faked, so an empty one does. + var solution = new Lazy(static () => new SolutionContext("Test.slnx", new SolutionModel())); var lifecycle = new OverrideLifecycle( solution, home.Provider, diff --git a/tests/Buildvana.Tool.Tests/PackagePinReaderTests.cs b/tests/Buildvana.Tool.Tests/PackagePinReaderTests.cs index f302fff0..e1b9d418 100644 --- a/tests/Buildvana.Tool.Tests/PackagePinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/PackagePinReaderTests.cs @@ -25,7 +25,7 @@ public async Task Read_StatesOnePinPerEvaluatedItem() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var pins = Read( home, Dump( @@ -47,7 +47,7 @@ await Assert.That(pins.Select(static pin => pin.ItemType + " " + pin.Id + " " + public async Task Read_OfOneDeclarationEvaluatedTwice_StatesOnePin() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); var item = Item("PackageReference", "Serilog", "4.0.0"); await Assert.That(Read(home, Dump(home, item), Dump(home, item)).Count).IsEqualTo(1); } @@ -67,7 +67,7 @@ public async Task Read_OfTwoVersionsOfOneId_StatesTwoPins() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var pins = Read( home, Dump(home, Item("PackageReference", "Serilog", "4.0.0")), @@ -80,7 +80,7 @@ public async Task Read_OfTwoVersionsOfOneId_StatesTwoPins() public async Task Read_LeavesOutWhatIsNotTheRepositorysToMove() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); var pins = Read( home, Dump( @@ -110,7 +110,7 @@ public async Task Read_OfAVersionStatedThroughAProperty_IsIndirect() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var pins = Read(home, Dump(home, Item("PackageReference", "Serilog", "4.0.0"))); await Assert.That(pins.Single().Management).IsEqualTo(PinManagement.IndirectVersion); } @@ -133,7 +133,7 @@ public async Task Read_OfAVersionStatedAsAChildElement_IsManaged() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var pin = Read(home, Dump(home, Item("PackageVersion", "Serilog", "\n 4.0.0\n "))).Single(); await Assert.That(pin.Management).IsEqualTo(PinManagement.Managed); await Assert.That(pin.VersionText).IsEqualTo("4.0.0"); @@ -144,7 +144,7 @@ public async Task Read_OfAVersionStatedAsAChildElement_IsManaged() public async Task Read_OfAPolicyStatedAsAChildElement_StatesThePolicyAlone() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageVersion", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageVersion", "Serilog", "4.0.0")); var item = Item("PackageVersion", "Serilog", "4.0.0", updatePolicy: "\n patch-\n "); await Assert.That(Read(home, Dump(home, item)).Single().MetadataPolicy).IsEqualTo("patch-"); } @@ -155,7 +155,7 @@ public async Task Read_OfAPolicyStatedAsAChildElement_StatesThePolicyAlone() public async Task Read_OfAnEmptyPolicyElement_StatesNoPolicy() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageVersion", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageVersion", "Serilog", "4.0.0")); var item = Item("PackageVersion", "Serilog", "4.0.0", updatePolicy: "\n \n "); await Assert.That(Read(home, Dump(home, item)).Single().MetadataPolicy).IsNull(); } @@ -166,7 +166,7 @@ public async Task Read_OfAnEmptyPolicyElement_StatesNoPolicy() public async Task Read_OfAnEmptyVersionElement_IsNoPin() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); await Assert.That(Read(home, Dump(home, Item("PackageReference", "Serilog", "\n \n ")))).IsEmpty(); } @@ -176,7 +176,7 @@ public async Task Read_OfAnEmptyVersionElement_IsNoPin() public async Task Read_OfAnEmptyVersionOverrideElement_IsNotAnOverride() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); + home.WriteFile(ProjectFileName, OneItemProject("PackageReference", "Serilog", "4.0.0")); var item = Item("PackageReference", "Serilog", "4.0.0", versionOverride: "\n \n "); var pin = Read(home, Dump(home, item)).Single(); await Assert.That(pin.Management).IsEqualTo(PinManagement.Managed); @@ -197,7 +197,7 @@ public async Task Read_OfAVersionAppliedByAnUpdate_IsIndirect() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var pins = Read(home, Dump(home, Item("PackageReference", "Serilog", "4.0.0"))); await Assert.That(pins.Single().Management).IsEqualTo(PinManagement.IndirectVersion); } @@ -214,7 +214,7 @@ public async Task Read_OfAVersionOverride_SaysSo() """; using var home = new TempHome(); - Write(home, ProjectFileName, project); + home.WriteFile(ProjectFileName, project); var item = Item("PackageReference", "Serilog", null, versionOverride: "4.1.0"); var pin = Read(home, Dump(home, item)).Single(); await Assert.That(pin.Management).IsEqualTo(PinManagement.VersionOverride); @@ -225,7 +225,7 @@ public async Task Read_OfAVersionOverride_SaysSo() public async Task Read_OfAnUnmanagedVersionForm_KeepsTheFormsReason() { using var home = new TempHome(); - Write(home, ProjectFileName, OneItemProject("PackageVersion", "Serilog", "[4.0.0]")); + home.WriteFile(ProjectFileName, OneItemProject("PackageVersion", "Serilog", "[4.0.0]")); var pins = Read(home, Dump(home, Item("PackageVersion", "Serilog", "[4.0.0]"))); await Assert.That(pins.Single().Management).IsEqualTo(PinManagement.BracketExactVersion); } @@ -282,11 +282,4 @@ private static string OneItemProject(string itemType, string id, string version) """; - - private static void Write(TempHome home, string relativePath, string content) - { - var path = home.GetFullPath(relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, content); - } } diff --git a/tests/Buildvana.Tool.Tests/PostUpdateHookArgsFactoryTests.cs b/tests/Buildvana.Tool.Tests/PostUpdateHookArgsFactoryTests.cs index 3b3180db..f7040d80 100644 --- a/tests/Buildvana.Tool.Tests/PostUpdateHookArgsFactoryTests.cs +++ b/tests/Buildvana.Tool.Tests/PostUpdateHookArgsFactoryTests.cs @@ -29,10 +29,11 @@ public async Task Create_StatesWhatTheRunMadeOfEveryScope() var args = Create(home, resolution, check: true); await Assert.That(args.Check).IsTrue(); - await Assert.That(args.NetSdk!.Id).IsNull(); - await Assert.That(args.NetSdk.DeclaringFile).IsEqualTo("global.json"); - await Assert.That(args.NetSdk.Target).IsEqualTo("10.0.201"); - await Assert.That(args.NetSdk.State).IsEqualTo(DependencyResultState.Updated); + var netSdk = await Assert.That(args.NetSdk).IsNotNull(); + await Assert.That(netSdk.Id).IsNull(); + await Assert.That(netSdk.DeclaringFile).IsEqualTo("global.json"); + await Assert.That(netSdk.Target).IsEqualTo("10.0.201"); + await Assert.That(netSdk.State).IsEqualTo(DependencyResultState.Updated); await Assert.That(args.Tools.Single().Id).IsEqualTo("ngbv"); await Assert.That(args.Packages.Single().Id).IsEqualTo("Serilog"); await Assert.That(args.Packages.Single().CurrentVersion).IsEqualTo("3.0.0"); diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs index 0be11d1e..fbf701c8 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandFailureTests.cs @@ -70,7 +70,7 @@ public async Task Release_WhenPublicationFails_ResetsAndForcePushesTheBranch() // The tag is gone too, locally and on the remote, and the host is asked to undo what it created. await Assert.That(harness.Repo.TagNames.Count).IsEqualTo(0); await Assert.That(harness.Repo.GetRemoteTagSha(ReleasedVersion)).IsNull(); - var publisher = harness.Publisher!; + var publisher = await Assert.That(harness.Publisher).IsNotNull(); await Assert.That(publisher.IsPublished).IsFalse(); await Assert.That(publisher.IsUndone).IsTrue(); } diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandTests.cs index f4678cd2..43a1436c 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandTests.cs @@ -81,7 +81,7 @@ public async Task Release_TagsTheReleaseCommit_PushesTheTag_AndPublishesOnIt() await Assert.That(releaseCommit.Message).IsEqualTo(ReleaseCommitMessage); await Assert.That(harness.Repo.TagNames).IsEquivalentTo([ReleasedVersion]); await Assert.That(harness.Repo.GetRemoteTagSha(ReleasedVersion)).IsEqualTo(releaseCommit.Sha); - var publisher = harness.Publisher!; + var publisher = await Assert.That(harness.Publisher).IsNotNull(); await Assert.That(publisher.PublishedTag).IsEqualTo(ReleasedVersion); await Assert.That(publisher.PublishedSha).IsEqualTo(releaseCommit.Sha); } @@ -201,7 +201,7 @@ public async Task Release_PublishesEveryProducedPackage_AndItsSymbols() _ = await harness.RunAsync().ConfigureAwait(false); - var publisher = harness.Publisher!; + var publisher = await Assert.That(harness.Publisher).IsNotNull(); await Assert.That(publisher.IsPublished).IsTrue(); var assets = publisher.PublishedAssets; var names = assets.Select(x => Path.GetFileName(x.Path)).ToArray(); diff --git a/tests/Buildvana.Tool.Tests/ReleaseHarness.cs b/tests/Buildvana.Tool.Tests/ReleaseHarness.cs index 84d6d45a..8a12f9d6 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseHarness.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseHarness.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; using Buildvana.Core.ConsoleOutput; @@ -234,7 +235,9 @@ public string ComputeVersion() public void WriteFile(string relativePath, string content) { var path = Path.Combine(Repo.RootPath, relativePath); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + Debug.Assert(directory is not null, "GetDirectoryName returns null only for a root, and a file path is not one."); + _ = Directory.CreateDirectory(directory); File.WriteAllText(path, content); } diff --git a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs index e2570f9a..b9d9f729 100644 --- a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs @@ -566,7 +566,7 @@ await Assert.That(handoff.Args).IsEquivalentTo( ["tool", "run", "bv", "--", "--nologo", "self-update", "--to", "2.1.45-preview"], CollectionOrdering.Matching); await Assert.That(handoff.Environment).IsNotNull(); - await Assert.That(handoff.Environment!["BV_SELF_UPDATE_FROM"]).IsEqualTo("2.1.40-preview"); + await Assert.That(handoff.Environment?["BV_SELF_UPDATE_FROM"]).IsEqualTo("2.1.40-preview"); await Assert.That(handoff.WorkingDirectory).IsEqualTo(home.RootPath); await Assert.That(home.ReadFile("global.json")).IsEqualTo(GlobalJsonText("2.1.40-preview")); await Assert.That(result.Summary).IsNull(); @@ -625,7 +625,7 @@ public async Task UpdateRepository_WhenHandingOff_WithoutManifestEntry_PassesAnE await Assert.That(runner.Runs[0].Args).IsEquivalentTo( ["tool", "install", "bv", "--version", "2.1.45-preview", "--tool-manifest", "dotnet-tools.json"], CollectionOrdering.Matching); - await Assert.That(runner.InheritedStdioRuns[0].Environment!["BV_SELF_UPDATE_FROM"]).IsEqualTo(string.Empty); + await Assert.That(runner.InheritedStdioRuns[0].Environment?["BV_SELF_UPDATE_FROM"]).IsEqualTo(string.Empty); } [Test] @@ -1013,8 +1013,7 @@ private static SelfUpdateRequest To(string version, bool force = false) private static async Task UpdateInPlaceAsync(SelfVersionService service, SelfUpdateRequest request) { var result = await service.UpdateRepositoryAsync(request).ConfigureAwait(false); - await Assert.That(result.Summary).IsNotNull(); - return result.Summary!; + return await Assert.That(result.Summary).IsNotNull(); } private static List WarningsOf(CaptureReporter reporter) => MessagesOf(reporter, MessageLevel.Warning); diff --git a/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs b/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs index 8772c144..452938aa 100644 --- a/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs +++ b/tests/Buildvana.Tool.Tests/ServerReleaseTests.cs @@ -183,7 +183,8 @@ public async Task PushUpdates_HasThePublisherCheckTheAssets() release.PushUpdates(); ReleaseAsset[] expected = [new("artifacts/setup.exe", "Setup program", "application/vnd.microsoft.portable-executable")]; - await Assert.That(harness.Publisher!.CheckedAssets).IsEquivalentTo(expected); + var publisher = await Assert.That(harness.Publisher).IsNotNull(); + await Assert.That(publisher.CheckedAssets).IsEquivalentTo(expected); } } @@ -201,7 +202,8 @@ public async Task PublishAsync_BeforePushingUpdates_Throws() await Assert.That(Act) .Throws() .WithMessageContaining("before pushing updates", StringComparison.Ordinal); - await Assert.That(harness.Publisher!.IsPublished).IsFalse(); + var publisher = await Assert.That(harness.Publisher).IsNotNull(); + await Assert.That(publisher.IsPublished).IsFalse(); } } @@ -216,14 +218,14 @@ public async Task PublishAsync_PublishesOnTheTag_AtTheReleaseCommit() release.PushUpdates(); await release.PublishAsync().ConfigureAwait(false); - var publisher = harness.Publisher!; + var publisher = await Assert.That(harness.Publisher).IsNotNull(); await Assert.That(publisher.IsBegun).IsTrue(); await Assert.That(publisher.PublishedTag).IsEqualTo(harness.ComputeVersion()); await Assert.That(publisher.PublishedSha).IsEqualTo(release.ReleaseCommitSha); } // A published release is final: disposing it undoes nothing. - await Assert.That(harness.Publisher!.IsUndone).IsFalse(); + await Assert.That(harness.Publisher?.IsUndone).IsFalse(); await Assert.That(harness.Repo.TagNames.Count).IsEqualTo(1); } @@ -268,7 +270,8 @@ public async Task DisposeAsync_AfterPushingUpdates_DeletesTheTag_AndUndoesThePub await Assert.That(harness.Repo.TagNames.Count).IsEqualTo(0); await Assert.That(harness.Repo.GetRemoteTagSha(tag)).IsNull(); await Assert.That(harness.Repo.GetRemoteTipSha()).IsEqualTo(initialSha); - await Assert.That(harness.Publisher!.IsUndone).IsTrue(); + var publisher = await Assert.That(harness.Publisher).IsNotNull(); + await Assert.That(publisher.IsUndone).IsTrue(); var notices = harness.Notices.ToArray(); await Assert.That(Array.IndexOf(notices, $"Deleted tag '{tag}' from 'origin'.")) .IsLessThan(Array.IndexOf(notices, "Undid the release commit.")); diff --git a/tests/Buildvana.Tool.Tests/SettingsHelpReflectionTests.cs b/tests/Buildvana.Tool.Tests/SettingsHelpReflectionTests.cs index 1ea6d99c..0008c4ec 100644 --- a/tests/Buildvana.Tool.Tests/SettingsHelpReflectionTests.cs +++ b/tests/Buildvana.Tool.Tests/SettingsHelpReflectionTests.cs @@ -33,8 +33,8 @@ private static string OptionLongNames(Type type) var longNames = type .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Select(static p => p.GetCustomAttribute()) - .Where(static a => a is not null) - .Select(static a => a!.LongNames[0]); + .OfType() + .Select(static a => a.LongNames[0]); return string.Join(",", longNames); } } diff --git a/tests/Buildvana.Tool.Tests/StubHttpMessageHandler.cs b/tests/Buildvana.Tool.Tests/StubHttpMessageHandler.cs index 35b68103..cbdab365 100644 --- a/tests/Buildvana.Tool.Tests/StubHttpMessageHandler.cs +++ b/tests/Buildvana.Tool.Tests/StubHttpMessageHandler.cs @@ -52,7 +52,7 @@ protected override async Task SendAsync( CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var url = request.RequestUri!.AbsoluteUri; + var url = request.RequestUri?.AbsoluteUri ?? throw new InvalidOperationException("The request has no URI."); var body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); Requests.Add(new StubHttpRequest(request.Method, url, CollectHeaders(request), body)); diff --git a/tests/Buildvana.Tool.Tests/ToolDiagnosticsPageTests.cs b/tests/Buildvana.Tool.Tests/ToolDiagnosticsPageTests.cs index a6dda439..afb75b7c 100644 --- a/tests/Buildvana.Tool.Tests/ToolDiagnosticsPageTests.cs +++ b/tests/Buildvana.Tool.Tests/ToolDiagnosticsPageTests.cs @@ -13,10 +13,7 @@ internal sealed class ToolDiagnosticsPageTests { private const string SectionHeading = "Dependency management (1200-1299)"; - private static readonly string RepositoryRoot = typeof(ToolDiagnosticsPageTests).Assembly - .GetCustomAttributes() - .First(static attribute => attribute.Key == "RepositoryRoot") - .Value!; + private static readonly string RepositoryRoot = typeof(ToolDiagnosticsPageTests).Assembly.GetMetadata("RepositoryRoot"); [Test] public async Task DependencyManagementSection_ListsTheCodesOfDiagnosticCodes() @@ -25,7 +22,8 @@ public async Task DependencyManagementSection_ListsTheCodesOfDiagnosticCodes() var listed = page.GetTableColumn(SectionHeading, "Code"); var declared = typeof(DiagnosticCodes) .GetFields(BindingFlags.Public | BindingFlags.Static) - .Select(static field => (string)field.GetRawConstantValue()!); + .Select(static field => field.GetRawConstantValue()) + .OfType(); await Assert.That(Join(listed)).IsEqualTo(Join(declared)); } diff --git a/tests/Buildvana.Tool.Tests/ToolResolverCacheProbeTests.cs b/tests/Buildvana.Tool.Tests/ToolResolverCacheProbeTests.cs index 575986a1..025f1934 100644 --- a/tests/Buildvana.Tool.Tests/ToolResolverCacheProbeTests.cs +++ b/tests/Buildvana.Tool.Tests/ToolResolverCacheProbeTests.cs @@ -105,10 +105,8 @@ private static void WriteCacheFile(TempHome home, string content) private static string CreateExecutableFile(TempHome home) { - var path = Path.Combine(home.RootPath, "tools", "Buildvana.Tool.dll"); - _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, string.Empty); - return path; + home.WriteFile("tools/Buildvana.Tool.dll", string.Empty); + return home.GetFullPath("tools/Buildvana.Tool.dll"); } // The real cache row shape; the executable path goes through the JSON serializer because Windows paths diff --git a/tests/Buildvana.Tool.Tests/UpdatePolicyEngineTests.cs b/tests/Buildvana.Tool.Tests/UpdatePolicyEngineTests.cs index 1a13beca..d86463a9 100644 --- a/tests/Buildvana.Tool.Tests/UpdatePolicyEngineTests.cs +++ b/tests/Buildvana.Tool.Tests/UpdatePolicyEngineTests.cs @@ -251,6 +251,7 @@ public async Task SelectPackage_WithUndefinedKind_Holds() [Test] public async Task SelectPackage_WithNullCurrent_Throws() { + // ! Null on purpose: testing null detection static TargetSelection Act() => UpdatePolicyEngine.Select(null!, Versions("1.0.0"), new(PackageUpdatePolicyKind.Minor, false)); @@ -260,6 +261,7 @@ static TargetSelection Act() [Test] public async Task SelectPackage_WithNullCandidates_Throws() { + // ! Null on purpose: testing null detection static TargetSelection Act() => UpdatePolicyEngine.Select( NuGetVersion.Parse("1.0.0"), @@ -381,6 +383,7 @@ public async Task SelectNetSdk_WithUndefinedKind_Holds() [Test] public async Task SelectNetSdk_WithNullCurrent_Throws() { + // ! Null on purpose: testing null detection static TargetSelection Act() => UpdatePolicyEngine.Select(null!, [Lts("10.0.402")], new(NetSdkUpdatePolicyKind.Major, false)); @@ -390,6 +393,7 @@ static TargetSelection Act() [Test] public async Task SelectNetSdk_WithNullCandidates_Throws() { + // ! Null on purpose: testing null detection static TargetSelection Act() => UpdatePolicyEngine.Select( NuGetVersion.Parse("10.0.402"), diff --git a/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs b/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs index cb75f839..d6bcb9bc 100644 --- a/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs +++ b/tests/Buildvana.Tool.Tests/UsageExitCodeTests.cs @@ -58,12 +58,20 @@ public Task InvalidChangeArgument_ExitsWithUsageCode() => AssertUsageExitCode(() => VersionAdvanceSettings.Parse(["bogus"], []).ResolveChange()); // The dispatch path of Program, up to the point where the command would run: resolve the command, then - // validate what was given to it. + // validate what was given to it. Where Program would print help instead, nothing throws, and the test fails. private static void ParseCommandLine(string commandLine) { var parsed = CliArgSplitter.Split(commandLine.Split(' ')); - var (node, positionals) = CommandRegistry.Resolve(parsed.Subcommand!, parsed.Positionals); - CommandArgumentValidator.Validate(node.Command!, parsed, positionals); + if (parsed.Subcommand is null) + { + return; + } + + var (node, positionals) = CommandRegistry.Resolve(parsed.Subcommand, parsed.Positionals); + if (node.Command is { } command) + { + CommandArgumentValidator.Validate(command, parsed, positionals); + } } private static async Task AssertUsageExitCode(Action action) diff --git a/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs b/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs index d5c96b63..1f299484 100644 --- a/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs +++ b/tests/Buildvana.Tool.Tests/VerbosityParserTests.cs @@ -39,6 +39,7 @@ await Assert.That(() => VerbosityParser.Parse("loud")) [Test] public async Task Parse_Null_ThrowsArgumentNullException() { + // ! Null on purpose: testing null detection await Assert.That(() => VerbosityParser.Parse(null!)).Throws(); } } From e61260717e8341f7e0cf78197f73dfc14d0c1b09 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 23:17:26 +0100 Subject: [PATCH 08/11] Wrap test lines over 140 characters and declarations over 120 The lines were pre-existing, but we touched the files in the same PR so they get the fixes. Co-Authored-By: Claude Opus 5.5 --- .../JsonHelperTests.cs | 4 +- .../JsonKeyedObjectConverterTests.cs | 6 ++- .../Buildvana.Sdk.Tests/ReadmeTargetsTests.cs | 4 +- .../TransitiveOverridesFixture.cs | 3 +- .../AdditionalGroupPinReaderTests.cs | 17 +++--- .../BvOptionAttributeTests.cs | 4 +- .../CommandArgumentValidatorTests.cs | 12 ++++- .../DelegationServiceTests.cs | 33 +++++++++--- .../DependencyResolverTests.cs | 3 +- .../DirectivePinReaderTests.cs | 3 +- .../EffectivePolicyResolverTests.cs | 3 +- .../ForgejoApiClientTests.cs | 6 ++- .../GitHubApiClientTests.cs | 3 +- tests/Buildvana.Tool.Tests/GitHubHostTests.cs | 5 +- .../GitLabApiClientTests.cs | 3 +- tests/Buildvana.Tool.Tests/GitLabHostTests.cs | 3 +- .../GitLabReleasePublisherTests.cs | 3 +- tests/Buildvana.Tool.Tests/GitServiceTests.cs | 2 +- .../GiteaApiClientTests.cs | 3 +- .../OverrideLifecycleTests.cs | 3 +- .../ProjectAssetsReaderTests.cs | 4 +- .../ReleaseCommandChangelogTests.cs | 3 +- .../SelfReferenceUpdaterTests.cs | 3 +- .../SelfUpdateTargetResolverTests.cs | 5 +- .../SelfVersionServiceTests.cs | 54 ++++++++++++++----- .../SolutionPinReaderTests.cs | 9 +++- 26 files changed, 144 insertions(+), 57 deletions(-) diff --git a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs index 2c740a04..af145eea 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs @@ -265,7 +265,9 @@ public async Task RewriteBooleanValues_SplicesTheLiteral_LeavingEverythingElseAl """); - var rewritten = new JsonHelper().RewriteBooleanValues(file.Path, static (path, _) => path is ["sdk", "allowPrerelease"] ? true : null); + var rewritten = new JsonHelper().RewriteBooleanValues( + file.Path, + static (path, _) => path is ["sdk", "allowPrerelease"] ? true : null); await Assert.That(rewritten).IsTrue(); await Assert.That(file.ReadText()).IsEqualTo( diff --git a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs index 923b0f0a..1d81b1d6 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonKeyedObjectConverterTests.cs @@ -125,7 +125,8 @@ public async Task Read_DuplicateKey_ByDefault_KeepsFirstPositionTakesLastValue() public async Task Read_DuplicateKey_WhenDisallowed_Throws() { var options = CreateOptions(allowDuplicateProperties: false); - object? Act() => JsonSerializer.Deserialize>("""{"a":"one","a":"two"}""", options); + object? Act() + => JsonSerializer.Deserialize>("""{"a":"one","a":"two"}""", options); await Assert.That(Act).Throws().WithMessageContaining("Duplicate key 'a'", StringComparison.Ordinal); } @@ -159,7 +160,8 @@ public async Task Read_MembersShape_DuplicateInnerMember_TakesLastValue() public async Task Read_MembersShape_DuplicateInnerMemberWhenDisallowed_Throws() { var options = CreateOptions(allowDuplicateProperties: false); - object? Act() => JsonSerializer.Deserialize>("""{"G1":{"retries":1,"retries":2}}""", options); + object? Act() + => JsonSerializer.Deserialize>("""{"G1":{"retries":1,"retries":2}}""", options); _ = await Assert.That(Act).Throws(); } diff --git a/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs b/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs index 67374b7d..14797711 100644 --- a/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs +++ b/tests/Buildvana.Sdk.Tests/ReadmeTargetsTests.cs @@ -33,7 +33,9 @@ public async Task Evaluate_PackageReadmeFileNamesMissingFile_ReportsBVSDK1510() await Assert.That(errors).IsEquivalentTo(["BVSDK1510"]); } - private static (string ReadmePath, IReadOnlyList ErrorCodes) Evaluate(TempHome home, string packageReadmeFile) + private static (string ReadmePath, IReadOnlyList ErrorCodes) Evaluate( + TempHome home, + string packageReadmeFile) { var projectPath = home.GetFullPath("src/Test/Test.proj"); var projectText = $""" diff --git a/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs b/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs index 327732a7..acbac242 100644 --- a/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs +++ b/tests/Buildvana.Sdk.Tests/TransitiveOverridesFixture.cs @@ -24,7 +24,8 @@ public TransitiveOverridesFixture() public void WriteHomeFile(string fileName, string content) => Write(Path.Combine(_root, fileName), content); - public void WriteProjectFile(string fileName, string content) => Write(Path.Combine(ProjectDirectory, fileName), content); + public void WriteProjectFile(string fileName, string content) + => Write(Path.Combine(ProjectDirectory, fileName), content); // The identities of the items of one type the evaluation ends up with, in evaluation order. public IReadOnlyList EvaluateItems(string itemType, bool suppressOverrides = false) diff --git a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs index 9b534688..f090dd9f 100644 --- a/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/AdditionalGroupPinReaderTests.cs @@ -15,14 +15,15 @@ internal sealed class AdditionalGroupPinReaderTests { private const string GroupFile = "src/Sdk/PackageVersions.props"; - private const string GroupFileContent = """ - - - - - - - """; + private const string GroupFileContent = + """ + + + + + + + """; [Test] public async Task ReadAsync_StatesThePinsOfEveryGroupFile() diff --git a/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs b/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs index b43141c5..eeb7c86f 100644 --- a/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs +++ b/tests/Buildvana.Tool.Tests/BvOptionAttributeTests.cs @@ -34,7 +34,9 @@ public async Task FlagTemplate_ParsesLongNameAndNoValuePlaceholder() [Test] public async Task Attribute_IsDiscoverableViaReflection() { - var property = typeof(BvOptionAttributeTests).GetProperty(nameof(SampleValueOption), BindingFlags.Instance | BindingFlags.NonPublic); + var property = typeof(BvOptionAttributeTests).GetProperty( + nameof(SampleValueOption), + BindingFlags.Instance | BindingFlags.NonPublic); var attribute = await Assert.That(property?.GetCustomAttribute()).IsNotNull(); await Assert.That(attribute.ValueName).IsEqualTo("NAME"); } diff --git a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs index 36c54a08..c8d4da55 100644 --- a/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs +++ b/tests/Buildvana.Tool.Tests/CommandArgumentValidatorTests.cs @@ -174,7 +174,11 @@ public async Task ArgumentCommand_RejectsMissingRequiredArgument() [Test] public async Task VariadicArgumentCommand_AcceptsMorePositionalsThanDeclaredArguments() { - var command = new CommandRegistration([["fake"]], typeof(FakeVariadicArgumentSettings), false, typeof(FakeVariadicArgumentSettings)); + var command = new CommandRegistration( + [["fake"]], + typeof(FakeVariadicArgumentSettings), + false, + typeof(FakeVariadicArgumentSettings)); var parsed = CliArgSplitter.Split(["fake", "mode", "one", "two", "three"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, ["mode", "one", "two", "three"])).ThrowsNothing(); } @@ -182,7 +186,11 @@ public async Task VariadicArgumentCommand_AcceptsMorePositionalsThanDeclaredArgu [Test] public async Task VariadicArgumentCommand_AcceptsNoPositionalAtAll() { - var command = new CommandRegistration([["fake"]], typeof(FakeVariadicArgumentSettings), false, typeof(FakeVariadicArgumentSettings)); + var command = new CommandRegistration( + [["fake"]], + typeof(FakeVariadicArgumentSettings), + false, + typeof(FakeVariadicArgumentSettings)); var parsed = CliArgSplitter.Split(["fake"]); await Assert.That(() => CommandArgumentValidator.Validate(command, parsed, [])).ThrowsNothing(); } diff --git a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs index fffce4e2..cc2bd2f9 100644 --- a/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/DelegationServiceTests.cs @@ -147,9 +147,15 @@ public async Task TryDelegate_WithUnreadableManifest_RunsInPlaceAndWarns() // A bv entry the dotnet CLI itself could not use (see ToolManifest.ReadBvPin) reads as "no pin", but // deserves a warning — the repository clearly meant to pin something. [Test] - [Arguments("""{ "version": 1, "isRoot": true, "tools": { "bv": { "version": "not-a-version", "commands": [ "bv" ] } } }""", "not-a-version")] - [Arguments("""{ "version": 1, "isRoot": true, "tools": { "bv": { "commands": [ "bv" ] } } }""", "has no version")] - public async Task TryDelegate_WithUnusableManifestEntry_RunsInPlaceAndWarns(string manifestContent, string expectedDetail) + [Arguments( + """{ "version": 1, "isRoot": true, "tools": { "bv": { "version": "not-a-version", "commands": [ "bv" ] } } }""", + "not-a-version")] + [Arguments( + """{ "version": 1, "isRoot": true, "tools": { "bv": { "commands": [ "bv" ] } } }""", + "has no version")] + public async Task TryDelegate_WithUnusableManifestEntry_RunsInPlaceAndWarns( + string manifestContent, + string expectedDetail) { using var home = new TempHome(); MarkAsHome(home); @@ -173,7 +179,9 @@ public async Task TryDelegate_WithDifferentlyCasedManifestKey_ReadsThePin() { using var home = new TempHome(); MarkAsHome(home); - WriteToolManifest(home, """{ "version": 1, "isRoot": true, "tools": { "BV": { "version": "2.1.40-preview", "commands": [ "bv" ] } } }"""); + WriteToolManifest( + home, + """{ "version": 1, "isRoot": true, "tools": { "BV": { "version": "2.1.40-preview", "commands": [ "bv" ] } } }"""); var runner = new FakeProcessRunner(); var output = new StringWriter(); var service = CreateService(runner, output); @@ -308,7 +316,12 @@ public async Task TryDelegate_StreamsRestoreOutput() { using var home = CreateDelegatableHome("2.1.40-preview"); var runner = new FakeProcessRunner { - OnRun = static (executable, args) => new ProcessResult($"{executable} {string.Join(' ', args)}", 0, "Restored bv.\nRestored ngbv.", string.Empty, TimeSpan.Zero), + OnRun = static (executable, args) => new ProcessResult( + $"{executable} {string.Join(' ', args)}", + 0, + "Restored bv.\nRestored ngbv.", + string.Empty, + TimeSpan.Zero), }; var output = new StringWriter(); var service = CreateService(runner, output); @@ -327,7 +340,12 @@ public async Task TryDelegate_WhenRestoreFails_WarnsAndRunsAnyway() { using var home = CreateDelegatableHome("2.1.40-preview"); var runner = new FakeProcessRunner { - OnRun = static (executable, args) => new ProcessResult($"{executable} {string.Join(' ', args)}", 1, string.Empty, "simulated failure", TimeSpan.Zero), + OnRun = static (executable, args) => new ProcessResult( + $"{executable} {string.Join(' ', args)}", + 1, + string.Empty, + "simulated failure", + TimeSpan.Zero), OnRunWithInheritedStdio = static (_, _) => 42, }; var output = new StringWriter(); @@ -415,5 +433,6 @@ private static TempHome CreateDelegatableHome(string pinnedVersion) return home; } - private static void WriteToolManifest(TempHome home, string content) => home.WriteFile("dotnet-tools.json", content); + private static void WriteToolManifest(TempHome home, string content) + => home.WriteFile("dotnet-tools.json", content); } diff --git a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs index 766f29d8..6aded477 100644 --- a/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/DependencyResolverTests.cs @@ -135,8 +135,9 @@ public async Task ResolveAsync_WhenAllowPrereleaseDisagreesWithThePolicy_SaysItM public async Task ResolveAsync_ANetSdkVersionTheIndexHasNot_Fails() { var releases = new FakeNetSdkReleaseSource().Knows(isLts: true, "10.0.100"); + var pin = NetSdkPin.Create("10.0.999", allowPrerelease: false); var exception = await Assert - .That(async () => await ResolveNetSdkAsync(releases, NetSdkPin.Create("10.0.999", allowPrerelease: false)).ConfigureAwait(false)) + .That(async () => await ResolveNetSdkAsync(releases, pin).ConfigureAwait(false)) .Throws(); Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple."); var diagnostic = exception.Diagnostics.Single(); diff --git a/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs b/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs index 03b1eac9..06c6762b 100644 --- a/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/DirectivePinReaderTests.cs @@ -95,7 +95,8 @@ public async Task Read_IgnoresBuildOutput() await Assert.That(Read(home)).IsEmpty(); } - private static IReadOnlyList Read(TempHome home, BuildvanaConfig? config = null) => ReadAll(home, config).Pins; + private static IReadOnlyList Read(TempHome home, BuildvanaConfig? config = null) + => ReadAll(home, config).Pins; private static DirectivePins ReadAll(TempHome home, BuildvanaConfig? config = null) => new DirectivePinReader(home.Provider, config ?? new BuildvanaConfig { FileBasedApps = ["/tools/"] }).Read(); diff --git a/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs b/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs index 4a0a51a5..7b8075e8 100644 --- a/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/EffectivePolicyResolverTests.cs @@ -106,7 +106,8 @@ public async Task Resolve_OfAPinStatingNonsense_Fails() await Assert.That(exception.Message).Contains("Directory.Packages.props"); } - private static string Resolve(EffectivePolicyResolver resolver, DependencyPin pin) => resolver.Resolve(pin).ToString(); + private static string Resolve(EffectivePolicyResolver resolver, DependencyPin pin) + => resolver.Resolve(pin).ToString(); private static DependencyPin Pin(DependencyScope scope, string id = "Serilog") => DependencyPin.Create(scope, id, "4.0.0", "Directory.Packages.props"); diff --git a/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs b/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs index d6d379b1..9f91e5b1 100644 --- a/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/ForgejoApiClientTests.cs @@ -240,9 +240,11 @@ await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).Confi .WithMessageMatching("*with 502 Bad Gateway."); } - private static ForgejoApiClient NewClient(StubHttpMessageHandler handler) => new("forgejo.example.com", Token, handler); + private static ForgejoApiClient NewClient(StubHttpMessageHandler handler) + => new("forgejo.example.com", Token, handler); - private static StubHttpMessageHandler Answering() => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); + private static StubHttpMessageHandler Answering() + => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); private static StubHttpMessageHandler AnsweringEverything(StubHttpAnswer answer) => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>(), answer); diff --git a/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs b/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs index 55fe8a9c..edff0564 100644 --- a/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GitHubApiClientTests.cs @@ -329,7 +329,8 @@ await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo, cance private static GitHubApiClient NewClient(StubHttpMessageHandler handler) => new("github.com", Token, handler); - private static StubHttpMessageHandler Answering() => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); + private static StubHttpMessageHandler Answering() + => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); private static StubHttpMessageHandler AnsweringEverything(StubHttpAnswer answer) => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>(), answer); diff --git a/tests/Buildvana.Tool.Tests/GitHubHostTests.cs b/tests/Buildvana.Tool.Tests/GitHubHostTests.cs index 5cfe4e18..4b27425e 100644 --- a/tests/Buildvana.Tool.Tests/GitHubHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GitHubHostTests.cs @@ -59,7 +59,7 @@ public async Task Constructor_ForAPathOfThreeSegments_Throws() var services = Services(); await using (services.ConfigureAwait(false)) { - // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes the lambda before services is disposed + // ReSharper disable once AccessToDisposedClosure // False positive: the assertion runs the lambda before services is disposed await Assert.That(() => new GitHubHost(services, Parse("https://github.com/a/b/c"))) .Throws() .WithMessageContaining("not a valid GitHub repository URL", StringComparison.Ordinal); @@ -110,7 +110,8 @@ await Assert.That(host.IsPrivateRepositoryAsync) } } - private static GitUrlInfo Parse(string url) => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); + private static GitUrlInfo Parse(string url) + => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); private static ServiceProvider Services() => new ServiceCollection() diff --git a/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs b/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs index 8a76dfd7..8e93204a 100644 --- a/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabApiClientTests.cs @@ -379,7 +379,8 @@ await Assert.That(async () => await client.GetProjectAsync(Project).ConfigureAwa private static GitLabApiClient NewClient(StubHttpMessageHandler handler) => new("gitlab.com", Token, GitLabTokenKind.AccessToken, handler); - private static StubHttpMessageHandler Answering() => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); + private static StubHttpMessageHandler Answering() + => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); private static StubHttpMessageHandler AnsweringEverything(StubHttpAnswer answer) => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>(), answer); diff --git a/tests/Buildvana.Tool.Tests/GitLabHostTests.cs b/tests/Buildvana.Tool.Tests/GitLabHostTests.cs index 05a98f08..c71072fb 100644 --- a/tests/Buildvana.Tool.Tests/GitLabHostTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabHostTests.cs @@ -173,7 +173,8 @@ await Assert.That(host.IsPrivateRepositoryAsync) } } - private static GitUrlInfo Parse(string url) => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); + private static GitUrlInfo Parse(string url) + => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); private static ServiceProvider Services(string tokenEnv) => new ServiceCollection() diff --git a/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs b/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs index 8329f8ca..07d606d8 100644 --- a/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs +++ b/tests/Buildvana.Tool.Tests/GitLabReleasePublisherTests.cs @@ -318,5 +318,6 @@ private static async Task RunAsync( } } - private static GitUrlInfo Parse(string url) => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); + private static GitUrlInfo Parse(string url) + => GitUrlInfo.TryCreate(url, out var info) ? info : throw new InvalidOperationException(url); } diff --git a/tests/Buildvana.Tool.Tests/GitServiceTests.cs b/tests/Buildvana.Tool.Tests/GitServiceTests.cs index 7476fc6e..1199f95b 100644 --- a/tests/Buildvana.Tool.Tests/GitServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/GitServiceTests.cs @@ -116,7 +116,7 @@ public async Task TrackChangesAsync_WhenOperationThrows_Propagates() using var repo = CreateRepoWithCommit(); using var git = CreateGitService(repo); - // ReSharper disable once AccessToDisposedClosure // False positive: the assertion invokes Act before the enclosing scope disposes git + // ReSharper disable once AccessToDisposedClosure // False positive: the assertion runs Act before the enclosing scope disposes git Task<(bool Result, IReadOnlyList ChangedFiles)> Act() => git.TrackChangesAsync(() => throw new BuildFailedException("Operation failed.")); diff --git a/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs b/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs index 14cfdb1d..9323eefd 100644 --- a/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs +++ b/tests/Buildvana.Tool.Tests/GiteaApiClientTests.cs @@ -222,7 +222,8 @@ await Assert.That(async () => await client.GetRepositoryAsync(Owner, Repo).Confi private static GiteaApiClient NewClient(StubHttpMessageHandler handler) => new("gitea.example.com", Token, handler); - private static StubHttpMessageHandler Answering() => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); + private static StubHttpMessageHandler Answering() + => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>()); private static StubHttpMessageHandler AnsweringEverything(StubHttpAnswer answer) => new(new Dictionary<(HttpMethod Method, string Url), StubHttpAnswer>(), answer); diff --git a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs index b9248a7d..c874296e 100644 --- a/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs +++ b/tests/Buildvana.Tool.Tests/OverrideLifecycleTests.cs @@ -265,7 +265,8 @@ private static AssetsFile Finding(string version) private static AssetsFile NeverSettling(int pass) => new AssetsFile().Resolves($"Contoso.Package{pass}", "1.0.0").Reports("NU1902", $"Contoso.Package{pass}"); - private static FakeVulnerabilityDataSource Advisories() => new FakeVulnerabilityDataSource().Knows(Vulnerable, "(, 12.0.2]"); + private static FakeVulnerabilityDataSource Advisories() + => new FakeVulnerabilityDataSource().Knows(Vulnerable, "(, 12.0.2]"); private static FakePackageVersionSource Versions() => new FakePackageVersionSource().Knows(Vulnerable, ["12.0.1", "12.0.2", "12.0.3"]); diff --git a/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs b/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs index 2ce3109c..8cdaef47 100644 --- a/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/ProjectAssetsReaderTests.cs @@ -75,7 +75,9 @@ public async Task Read_StatesThePackagesTheRestoreResolved() { using var home = new TempHome(); var assets = Read(home); - var packages = assets.Packages.Select(static package => package.Id + " " + package.Version.ToNormalizedString()).Order(StringComparer.Ordinal); + var packages = assets.Packages + .Select(static package => package.Id + " " + package.Version.ToNormalizedString()) + .Order(StringComparer.Ordinal); await Assert.That(string.Join(", ", packages)).IsEqualTo("Newtonsoft.Json 12.0.2, Serilog 4.0.0"); } diff --git a/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs b/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs index fc15be69..45e63888 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseCommandChangelogTests.cs @@ -209,7 +209,8 @@ public async Task Release_PinsFileLinksOfNewSection_ToTheTagActuallyReleased() // the one released, not the one computed before the commit bumped the Git height. The section released // before this one is left as it is. var changelog = harness.ReadFile("CHANGELOG.md"); - const string pinnedUrl = $"https://git.example.invalid/tenacom/test-repo/blob/{ReleasedVersion}/docs/hooks.md#the-build-environment"; + const string pinnedUrl = + $"https://git.example.invalid/tenacom/test-repo/blob/{ReleasedVersion}/docs/hooks.md#the-build-environment"; await Assert.That(changelog).Contains($"[runs from the home directory]({pinnedUrl})"); await Assert.That(changelog).DoesNotContain("blob/2.3.1-preview/"); await Assert.That(changelog).Contains("[the page](docs/released.md)"); diff --git a/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs b/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs index d6359f39..8ef976de 100644 --- a/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfReferenceUpdaterTests.cs @@ -38,5 +38,6 @@ await Assert await Assert.That(home.ReadFile(".config/dotnet-tools.json")).IsEqualTo(Manifest); } - private static SelfReferenceUpdater CreateUpdater(TempHome home) => new(NullReporter.Instance, home.Provider, new JsonHelper()); + private static SelfReferenceUpdater CreateUpdater(TempHome home) + => new(NullReporter.Instance, home.Provider, new JsonHelper()); } diff --git a/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs b/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs index ad2ed9d2..73e574e6 100644 --- a/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfUpdateTargetResolverTests.cs @@ -212,6 +212,9 @@ public async Task Resolve_WhenNoSourceKnowsBv_Fails() private static BvManifestPin Pin(string version) => new(true, version, NuGetVersion.Parse(version)); - private static Task Resolve(FakePackageVersionSource source, SelfUpdateRequest request, BvManifestPin pin) + private static Task Resolve( + FakePackageVersionSource source, + SelfUpdateRequest request, + BvManifestPin pin) => new SelfUpdateTargetResolver(source, NuGetVersion.Parse(OwnVersion)).ResolveAsync(request, pin); } diff --git a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs index b9d9f729..086b4288 100644 --- a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs @@ -99,7 +99,9 @@ await Assert.That(service.EnsureSdkVersionMatch) [Arguments("2.1.41-preview", "2.1.41-preview")] [Arguments("2.1.41-preview+g0123abc", "2.1.41-preview")] [Arguments("2.1.41-preview", "2.1.41-preview+g0123abc")] - public async Task UpdateRepository_WhenEverythingCurrent_RunsToolUpdateButChangesNoFile(string ownVersion, string pin) + public async Task UpdateRepository_WhenEverythingCurrent_RunsToolUpdateButChangesNoFile( + string ownVersion, + string pin) { using var home = new TempHome(); WriteGlobalJson(home, pin); @@ -341,7 +343,8 @@ public async Task UpdateRepository_WithoutGlobalJson_CreatesIt() _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false); - await Assert.That(home.ReadFile("global.json")).IsEqualTo("{\n \"msbuild-sdks\": {\n \"Buildvana.Sdk\": \"2.1.41-preview\"\n }\n}\n"); + await Assert.That(home.ReadFile("global.json")) + .IsEqualTo("{\n \"msbuild-sdks\": {\n \"Buildvana.Sdk\": \"2.1.41-preview\"\n }\n}\n"); } // A directory named global.json is invisible to File.Exists, so the update takes the create-new-file path @@ -367,7 +370,10 @@ public async Task UpdateRepository_WithDeniedGlobalJsonWrite_Fails() [Arguments("2.1.42-preview", "2.1.41-preview", "tool manifest")] [Arguments("2.1.41-preview", "2.1.42-preview", "global.json")] [Arguments("2.1.42-preview", "2.1.42-preview", "tool manifest")] - public async Task UpdateRepository_WithNewerPin_FailsWithoutForce(string manifestPin, string sdkPin, string offender) + public async Task UpdateRepository_WithNewerPin_FailsWithoutForce( + string manifestPin, + string sdkPin, + string offender) { using var home = new TempHome(); WriteGlobalJson(home, sdkPin); @@ -652,7 +658,9 @@ public async Task UpdateRepository_WhenHandingOff_ReportsTheVersionThatTakesOver [Arguments("", "bv: 2.1.41-preview (tool manifest, added)")] [Arguments("2.1.41-preview", "bv: 2.1.41-preview (tool manifest, unchanged)")] [Arguments("not-a-version", "bv: 2.1.41-preview (tool manifest, unchanged)")] - public async Task UpdateRepository_WhenHandedOff_ReportsTheManifestMoveOfTheHandingOffRun(string from, string expectedLine) + public async Task UpdateRepository_WhenHandedOff_ReportsTheManifestMoveOfTheHandingOffRun( + string from, + string expectedLine) { using var home = new TempHome(); WriteGlobalJson(home, "2.1.40-preview"); @@ -736,8 +744,10 @@ public async Task UpdateRepository_StampsFamilyPins() var summary = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false); - await Assert.That(home.ReadFile("App.csproj")).IsEqualTo(project.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); - await Assert.That(home.ReadFile(hookPath)).IsEqualTo(hook.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); + await Assert.That(home.ReadFile("App.csproj")) + .IsEqualTo(project.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); + await Assert.That(home.ReadFile(hookPath)) + .IsEqualTo(hook.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); await Assert.That(summary.FamilyPinLines).IsEquivalentTo([ "Buildvana.Runtime: 2.1.40-preview -> 2.1.41-preview (.buildvana/hooks/hook.cs)", "Buildvana.Runtime: 2.1.40-preview -> 2.1.41-preview (App.csproj)", @@ -776,9 +786,15 @@ public async Task UpdateRepository_WithNewerFamilyPin_FailsWithoutForce() // An entry whose version the dotnet CLI cannot parse is beyond any `dotnet tool` verb's reach — update must // fail up front with a message pointing at the file, before touching anything. [Test] - [Arguments("""{ "version": 1, "isRoot": true, "tools": { "bv": { "version": "not-a-version", "commands": [ "bv" ] } } }""", "not-a-version")] - [Arguments("""{ "version": 1, "isRoot": true, "tools": { "bv": { "commands": [ "bv" ] } } }""", "has no version")] - public async Task UpdateRepository_WithUnusableManifestEntry_FailsBeforeChangingAnything(string manifestContent, string expectedDetail) + [Arguments( + """{ "version": 1, "isRoot": true, "tools": { "bv": { "version": "not-a-version", "commands": [ "bv" ] } } }""", + "not-a-version")] + [Arguments( + """{ "version": 1, "isRoot": true, "tools": { "bv": { "commands": [ "bv" ] } } }""", + "has no version")] + public async Task UpdateRepository_WithUnusableManifestEntry_FailsBeforeChangingAnything( + string manifestContent, + string expectedDetail) { using var home = new TempHome(); WriteGlobalJson(home, "2.1.40-preview"); @@ -825,7 +841,12 @@ public async Task UpdateRepository_WhenToolUpdateFails_Propagates() WriteGlobalJson(home, "2.1.41-preview"); WriteToolManifest(home, "2.1.40-preview"); var runner = new FakeProcessRunner { - OnRun = static (executable, args) => new ProcessResult($"{executable} {string.Join(' ', args)}", 1, string.Empty, "simulated failure", TimeSpan.Zero), + OnRun = static (executable, args) => new ProcessResult( + $"{executable} {string.Join(' ', args)}", + 1, + string.Empty, + "simulated failure", + TimeSpan.Zero), }; var service = CreateService(home, "2.1.41-preview", runner); @@ -872,7 +893,8 @@ public async Task UpdateRepository_WithSchemaReference_PreservesCommentsAndForma _ = await UpdateInPlaceAsync(service, SelfUpdateRequest.Default).ConfigureAwait(false); - await Assert.That(home.ReadFile("buildvana.jsonc")).IsEqualTo(content.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); + await Assert.That(home.ReadFile("buildvana.jsonc")) + .IsEqualTo(content.Replace("2.1.40-preview", "2.1.41-preview", StringComparison.Ordinal)); } [Test] @@ -1010,7 +1032,9 @@ private static SelfUpdateRequest To(string version, bool force = false) => new() { To = NuGetVersion.Parse(version), Force = force }; // Most tests exercise an update this bv performs itself, and read its summary. - private static async Task UpdateInPlaceAsync(SelfVersionService service, SelfUpdateRequest request) + private static async Task UpdateInPlaceAsync( + SelfVersionService service, + SelfUpdateRequest request) { var result = await service.UpdateRepositoryAsync(request).ConfigureAwait(false); return await Assert.That(result.Summary).IsNotNull(); @@ -1021,7 +1045,8 @@ private static async Task UpdateInPlaceAsync(SelfVersionServi private static List MessagesOf(CaptureReporter reporter, MessageLevel level) => [.. reporter.Messages.Where(m => m.Level == level).Select(static m => m.Message)]; - private static void WriteGlobalJson(TempHome home, string pin) => home.WriteFile("global.json", GlobalJsonText(pin)); + private static void WriteGlobalJson(TempHome home, string pin) + => home.WriteFile("global.json", GlobalJsonText(pin)); private static string SchemaConfigText(string version) => $$""" { @@ -1063,5 +1088,6 @@ private static void WriteToolManifest(TempHome home, string version) WriteRawToolManifest(home, content); } - private static void WriteRawToolManifest(TempHome home, string content) => home.WriteFile("dotnet-tools.json", content); + private static void WriteRawToolManifest(TempHome home, string content) + => home.WriteFile("dotnet-tools.json", content); } diff --git a/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs b/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs index c4c7ee4d..1b84b1bf 100644 --- a/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs +++ b/tests/Buildvana.Tool.Tests/SolutionPinReaderTests.cs @@ -107,7 +107,9 @@ public async Task ReadAsync_WithAnUnreadableDump_ReportsAFailedStep() private static SolutionPinReader CreateReader(TempHome home, IProcessRunner runner) => new(home.Provider, runner, NullReporter.Instance); - private static Func, ProcessResult> WriteDumps(string projectName, params string[] targetFrameworks) + private static Func, ProcessResult> WriteDumps( + string projectName, + params string[] targetFrameworks) => (executable, args) => { var directory = DumpDirectoryOf(args); foreach (var targetFramework in targetFrameworks) @@ -145,5 +147,8 @@ private static void Write(string directory, string fileName, string content) } private static string DumpDirectoryOf(IReadOnlyList args) - => args.Single(static arg => arg.StartsWith(DumpDirectoryArgumentPrefix, StringComparison.Ordinal))[DumpDirectoryArgumentPrefix.Length..]; + { + var argument = args.Single(static arg => arg.StartsWith(DumpDirectoryArgumentPrefix, StringComparison.Ordinal)); + return argument[DumpDirectoryArgumentPrefix.Length..]; + } } From 6dc19bebd97f9c73ffb32ea95055ee055037d43a Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 23:25:57 +0100 Subject: [PATCH 09/11] Enable Nullable.Extended.Analyzer Common.props turned the analyzer off while the null-forgiving operators lacked a fix. Now we can enable it. Co-Authored-By: Claude Opus 5.5 --- .claude/rules/common/csharp-style-guide.md | 34 ++++++++++++++++++++++ Common.props | 3 -- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.claude/rules/common/csharp-style-guide.md b/.claude/rules/common/csharp-style-guide.md index 260bd8a4..f018e0ff 100644 --- a/.claude/rules/common/csharp-style-guide.md +++ b/.claude/rules/common/csharp-style-guide.md @@ -51,6 +51,40 @@ Semantic differences to keep in mind when converting: - List, property, and recursive patterns imply a null test. `propertyPath is [PropertyName, PackageId]` is `false` when `propertyPath` is null, where `propertyPath.Count == 2` would throw. That is usually an improvement, but it is a behavior change, not a pure rewrite. - Constant patterns never use the type's `==` operator. A non-null constant compares with `Equals`. `null` is a plain reference test, or a `HasValue` check for a nullable value type. With `float` and `double` the pattern is what you want: `x is double.NaN` matches NaN, while `x == double.NaN` is always `false`. Exception: when a type overloads `==` so that `x == null` and `x is null` disagree, keep the operator form. +## The null-forgiving operator + +Do not write the null-forgiving operator, `!`. The operator tells the compiler that a value is not null, and the compiler stops checking it. The fact behind the operator then lives in no type, and a reader has to rebuild it from code elsewhere. `Nullable.Extended.Analyzer` reports every operator as NX0001 to NX0004, and every warning is an error here. + +Remove the cause of the operator instead. Try these fixes in order: + +1. Let a type state the fact. Make the member non-nullable, or return a type that carries the non-null value next to the rest. A method that filters items on a nullable member returns each item with its non-null value. +2. Keep a value the code built. Do not read it back through a nullable API. +3. Write a check that flow analysis follows: a pattern such as `is { } value` or `is JsonObject obj`, or a call to a `[NotNullWhen]` method inside the condition. Flow analysis does not follow a `bool` local that records the outcome of a check. +4. When no type and no check removes the operator, state the fact with `Debug.Assert(value is not null, "")`. The message says why the value cannot be null. Store the value in a local first, when it has none. + +Do not suppress the analyzer with its justification comment, a `// !` line above the statement. The comment states the fact where no tool checks it. `Debug.Assert` checks it in every Debug build, tests included. + +### In tests + +A test checks the null with the test framework, so that a null fails the test instead of throwing: + +- `await Assert.That(value).IsNotNull()` returns the value, non-null. +- A comparison through `?.` fails on a null: `await Assert.That(slash["type"]?.GetValue()).IsEqualTo("string")`. +- A test that asserts a value reached through `?.` is null first asserts the parent non-null. Otherwise a missing parent passes the test. +- Chain `WithMessage` or `WithMessageContaining` onto `Throws()`, instead of reading `Message` from the exception it returns. +- To read another member of the exception, use `Assert.Throws(Action)` for a synchronous act, which returns the exception non-null. After an awaited `Throws()`, write `Debug.Assert(exception is not null, "Throws returns null only inside Assert.Multiple.")`. + +A synchronous helper cannot await an assertion. When the null comes from the code under test, the helper throws `InvalidOperationException`. When the fact does not depend on the code under test, such as `Path.GetDirectoryName` of a file path, the helper uses `Debug.Assert`. Keep `Debug.Assert` for such facts, because a failed `Debug.Assert` ends the test process through `Environment.FailFast`. + +One case keeps the operator. When a test passes `null!` or `default!` on purpose, as the input under test, put this comment on the line above the statement: + +```csharp +// ! Null on purpose: testing null detection +static GitignorePattern? Act() => GitignorePattern.TryParse(null!); +``` + +Use this text and no other, so that a search finds every such input. + ## Partial classes When a class exceeds 500 lines of code, or has nested types, split it into partial class files. diff --git a/Common.props b/Common.props index 77c9dbed..26f44f2e 100644 --- a/Common.props +++ b/Common.props @@ -39,9 +39,6 @@ false false false - - - false From 741a697a00770ab7faf66442999bf8fbb511f61b Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Wed, 28 Oct 2026 23:42:09 +0100 Subject: [PATCH 10/11] Fix six ReSharper findings in tests Five Act local functions returned a value that the lambda passed to Assert.Throws discarded. FileBasedAppHelperTests compared two string? values with IsNotEqualTo, which ReSharper reports as CS8620. Co-Authored-By: Claude Opus 5.5 --- tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs | 4 ++-- tests/Buildvana.Core.IO.Tests/UserFileTests.cs | 4 ++-- tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs | 12 ++++++------ .../Buildvana.Tool.Tests/FileBasedAppHelperTests.cs | 5 +++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs index 03298c60..85a8fc30 100644 --- a/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserDirectoryTests.cs @@ -35,9 +35,9 @@ public async Task CreateDirectory_WithFileAtTargetPath_Fails() await File.WriteAllTextAsync(path, string.Empty).ConfigureAwait(false); try { - DirectoryInfo Act() => UserDirectory.CreateDirectory(path); + void Act() => UserDirectory.CreateDirectory(path); - var exception = Assert.Throws(() => Act()); + var exception = Assert.Throws(Act); await Assert.That(exception.Message).Contains("Could not create directory"); await Assert.That(exception.InnerException).IsTypeOf(); } diff --git a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs index 51297238..6854e894 100644 --- a/tests/Buildvana.Core.IO.Tests/UserFileTests.cs +++ b/tests/Buildvana.Core.IO.Tests/UserFileTests.cs @@ -143,9 +143,9 @@ public Task OpenTextWithEncoding_WithDeniedAccess_Fails() [Test] public async Task ReadAllText_WithMalformedPath_Fails() { - static string Act() => UserFile.ReadAllText("a\0b"); + static void Act() => UserFile.ReadAllText("a\0b"); - var exception = Assert.Throws(() => Act()); + var exception = Assert.Throws(Act); await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } diff --git a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs index af145eea..d63d455f 100644 --- a/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs +++ b/tests/Buildvana.Core.Json.Tests/JsonHelperTests.cs @@ -16,9 +16,9 @@ internal sealed partial class JsonHelperTests [Test] public async Task LoadObject_WithDeniedAccess_Fails() { - static JsonObject Act() => new JsonHelper().LoadObject(DeniedAccessPath); + static void Act() => new JsonHelper().LoadObject(DeniedAccessPath); - var exception = Assert.Throws(() => Act()); + var exception = Assert.Throws(Act); await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -36,9 +36,9 @@ public async Task SaveObject_WithDeniedAccess_Fails() [Test] public async Task RewriteStringValues_WithDeniedAccess_Fails() { - static bool Act() => new JsonHelper().RewriteStringValues(DeniedAccessPath, (_, _) => null); + static void Act() => new JsonHelper().RewriteStringValues(DeniedAccessPath, (_, _) => null); - var exception = Assert.Throws(() => Act()); + var exception = Assert.Throws(Act); await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } @@ -46,9 +46,9 @@ public async Task RewriteStringValues_WithDeniedAccess_Fails() [Test] public async Task InsertProperty_WithDeniedAccess_Fails() { - static bool Act() => new JsonHelper().InsertProperty(DeniedAccessPath, [], "a", JsonValue.Create("x")); + static void Act() => new JsonHelper().InsertProperty(DeniedAccessPath, [], "a", JsonValue.Create("x")); - var exception = Assert.Throws(() => Act()); + var exception = Assert.Throws(Act); await Assert.That(exception.Message).Contains("Could not read from"); await Assert.That(exception.InnerException).IsTypeOf(); } diff --git a/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs b/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs index 896c4f9a..1d7ffc9a 100644 --- a/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs +++ b/tests/Buildvana.Tool.Tests/FileBasedAppHelperTests.cs @@ -49,8 +49,9 @@ public async Task GetArtifactsDirectory_DistinguishesDifferentPaths() var first = Path.Combine(Path.GetTempPath(), "first", "app.cs"); var second = Path.Combine(Path.GetTempPath(), "second", "app.cs"); - await Assert.That(FileBasedAppHelper.GetArtifactsDirectory(first)) - .IsNotEqualTo(FileBasedAppHelper.GetArtifactsDirectory(second)); + var firstDirectory = await Assert.That(FileBasedAppHelper.GetArtifactsDirectory(first)).IsNotNull(); + var secondDirectory = await Assert.That(FileBasedAppHelper.GetArtifactsDirectory(second)).IsNotNull(); + await Assert.That(firstDirectory).IsNotEqualTo(secondDirectory); } [Test] From c5f4235ae1d4149620d2aa8afa2dbcda48d2cabd Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Thu, 29 Oct 2026 00:21:45 +0100 Subject: [PATCH 11/11] Fail the GetLatestVersions test on a pick by tag order GetLatestVersions_WithTwoVersionTagsOnOneCommit_ReadsBoth had one stable tag and two candidates for Latest, sorted alike by name and by version, so a pick by list order passed it. GetLatestVersions_WithSeveralVersionTagsOnOneCommit_ReadsTheHighest replaces it, and puts three stable and three prerelease tags on one commit. The highest version of each kind is neither first nor last, by name or by creation order. Co-Authored-By: Claude Opus 5.5 --- tests/Buildvana.Tool.Tests/GitServiceTests.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/Buildvana.Tool.Tests/GitServiceTests.cs b/tests/Buildvana.Tool.Tests/GitServiceTests.cs index 1199f95b..834c0b9b 100644 --- a/tests/Buildvana.Tool.Tests/GitServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/GitServiceTests.cs @@ -165,19 +165,24 @@ public async Task GetLatestVersions_WalksHistory_UntilAStableVersion() await Assert.That(latestStable).IsEqualTo(SemanticVersion.Parse("1.0.0")); } - // One commit carries a prerelease tag and a stable tag. Each one counts for its own result. + // One commit carries three stable tags and three prerelease tags. The highest version of each kind is neither the + // first nor the last, in name order and in creation order. [Test] - public async Task GetLatestVersions_WithTwoVersionTagsOnOneCommit_ReadsBoth() + public async Task GetLatestVersions_WithSeveralVersionTagsOnOneCommit_ReadsTheHighest() { using var repo = CreateRepoWithCommit(); - repo.CreateTag("2.0.0-preview"); - repo.CreateTag("1.5.0"); + repo.CreateTag("1.9.0"); + repo.CreateTag("1.11.0"); + repo.CreateTag("1.10.0"); + repo.CreateTag("2.9.0-preview"); + repo.CreateTag("2.11.0-preview"); + repo.CreateTag("2.10.0-preview"); using var git = CreateGitService(repo); var (latest, latestStable) = git.GetLatestVersions(); - await Assert.That(latest).IsEqualTo(SemanticVersion.Parse("2.0.0-preview")); - await Assert.That(latestStable).IsEqualTo(SemanticVersion.Parse("1.5.0")); + await Assert.That(latest).IsEqualTo(SemanticVersion.Parse("2.11.0-preview")); + await Assert.That(latestStable).IsEqualTo(SemanticVersion.Parse("1.11.0")); } [Test]