diff --git a/Containers.Benchmarks/Containers.Benchmarks.csproj b/Containers.Benchmarks/Containers.Benchmarks.csproj index 68b7a11..a2c6d3e 100644 --- a/Containers.Benchmarks/Containers.Benchmarks.csproj +++ b/Containers.Benchmarks/Containers.Benchmarks.csproj @@ -1,11 +1,12 @@ - + + + + Exe net10.0 - enable - enable - Containers.Benchmarks - ktsu.Containers.Benchmarks + + false true + + $(NoWarn);CA5394;KTSU0002 + + + diff --git a/Containers.Test/OrderedSetTests.cs b/Containers.Test/OrderedSetTests.cs index f43d5f0..dad6539 100644 --- a/Containers.Test/OrderedSetTests.cs +++ b/Containers.Test/OrderedSetTests.cs @@ -176,7 +176,7 @@ public void CopyTo_ValidParameters_CopiesElements() set.CopyTo(array, 0); int[] expected = [1, 3, 4, 5]; // Note: only 4 unique elements - Assert.AreSequenceEqual(expected, array[0..4]); + Assert.AreSequenceEqual(expected, array.Take(4)); } [TestMethod] diff --git a/Containers.Test/ProjectConventionTests.cs b/Containers.Test/ProjectConventionTests.cs new file mode 100644 index 0000000..cba3273 --- /dev/null +++ b/Containers.Test/ProjectConventionTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Containers.Tests; + +using System.Xml.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Guards the project conventions behind the estate-wide naming audit: every project in the +/// repository imports ktsu.Sdk and lets it derive the assembly name and root namespace from the +/// solution-relative folder path, instead of hand-writing either value. +/// +/// +/// Containers.Benchmarks drifted off ktsu.Sdk and hand-wrote both, which left its assembly named +/// Containers.Benchmarks while its namespace was ktsu.Containers.Benchmarks. +/// +[TestClass] +public class ProjectConventionTests +{ + /// + /// Walks up from the test binary until the directory holding the solution file is found. + /// + private static DirectoryInfo FindRepositoryRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Containers.sln"))) + { + directory = directory.Parent; + } + + Assert.IsNotNull(directory, $"Could not locate Containers.sln above '{AppContext.BaseDirectory}'."); + return directory; + } + + /// + /// Every project file in the repository, excluding build output. + /// + private static List EnumerateProjects() + { + string binSegment = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}"; + string objSegment = $"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}"; + + List projects = + [ + .. FindRepositoryRoot() + .EnumerateFiles("*.csproj", SearchOption.AllDirectories) + .Where(file => !file.FullName.Contains(binSegment, StringComparison.Ordinal)) + .Where(file => !file.FullName.Contains(objSegment, StringComparison.Ordinal)) + .OrderBy(file => file.FullName, StringComparer.Ordinal) + ]; + + Assert.IsGreaterThan(0, projects.Count, "Expected at least one project file in the repository."); + return projects; + } + + private static bool ImportsKtsuSdk(XDocument project) + { + string? sdkAttribute = project.Root?.Attribute("Sdk")?.Value; + if (sdkAttribute is not null && sdkAttribute.Contains("ktsu.Sdk", StringComparison.Ordinal)) + { + return true; + } + + return project.Root? + .Elements() + .Where(element => element.Name.LocalName == "Sdk") + .Select(element => element.Attribute("Name")?.Value) + .Any(name => name is not null && name.StartsWith("ktsu.Sdk", StringComparison.Ordinal)) ?? false; + } + + [TestMethod] + public void EveryProject_ImportsKtsuSdk() + { + List violations = []; + + foreach (FileInfo project in EnumerateProjects()) + { + if (!ImportsKtsuSdk(XDocument.Load(project.FullName))) + { + violations.Add(project.Name); + } + } + + Assert.IsEmpty(violations, $"These projects do not import ktsu.Sdk, so their identity is not derived: {string.Join(", ", violations)}"); + } + + [TestMethod] + public void NoProject_HandWritesAssemblyNameOrRootNamespace() + { + List violations = []; + + foreach (FileInfo project in EnumerateProjects()) + { + XDocument document = XDocument.Load(project.FullName); + List overrides = + [ + .. document.Descendants() + .Select(element => element.Name.LocalName) + .Where(name => name is "AssemblyName" or "RootNamespace") + .Distinct(StringComparer.Ordinal) + .OrderBy(name => name, StringComparer.Ordinal) + ]; + + if (overrides.Count > 0) + { + violations.Add($"{project.Name} ({string.Join(" and ", overrides)})"); + } + } + + Assert.IsEmpty(violations, $"ktsu.Sdk derives both values from the folder path; these projects override them: {string.Join(", ", violations)}"); + } +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 03037d1..d30e9de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ +