Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions Containers.Benchmarks/Containers.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project>
<Sdk Name="Microsoft.NET.Sdk" />
<Sdk Name="ktsu.Sdk" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Containers.Benchmarks</AssemblyName>
<RootNamespace>ktsu.Containers.Benchmarks</RootNamespace>
<TargetFrameworks></TargetFrameworks>
<IsPackable>false</IsPackable>
<!--
Exclude the benchmark harness from SonarQube/SonarCloud analysis. Benchmarks are
deliberately written in a non-idiomatic style: explicit foreach/counter loops are kept
Expand All @@ -15,10 +16,22 @@
new-code coverage gate to 0%. It is not shipped in the package.
-->
<SonarQubeExclude>true</SonarQubeExclude>
<!--
CA5394: benchmarks seed System.Random with a fixed value (new Random(42)) so every run
measures the same input sequence. Reproducibility is the point; a cryptographic RNG would
defeat it, and no benchmark input is security-sensitive.
KTSU0002: InternalsVisibleTo targets the test project. Nothing tests the benchmark harness,
and it exposes no internals worth reaching.
-->
<NoWarn>$(NoWarn);CA5394;KTSU0002</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<!-- Benchmark attributes ([MemoryDiagnoser], [SimpleJob], [Params]) live here; referenced
directly rather than relied on transitively through BenchmarkDotNet (KTSU0006). -->
<PackageReference Include="BenchmarkDotNet.Annotations" />
<PackageReference Include="Polyfill" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Containers.Test/OrderedSetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
112 changes: 112 additions & 0 deletions Containers.Test/ProjectConventionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Containers.Tests;

using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Containers.Benchmarks drifted off ktsu.Sdk and hand-wrote both, which left its assembly named
/// <c>Containers.Benchmarks</c> while its namespace was <c>ktsu.Containers.Benchmarks</c>.
/// </remarks>
[TestClass]
public class ProjectConventionTests
{
/// <summary>
/// Walks up from the test binary until the directory holding the solution file is found.
/// </summary>
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;
}

/// <summary>
/// Every project file in the repository, excluding build output.
/// </summary>
private static List<FileInfo> EnumerateProjects()
{
string binSegment = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}";
string objSegment = $"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}";

List<FileInfo> 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<string> 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<string> violations = [];

foreach (FileInfo project in EnumerateProjects())
{
XDocument document = XDocument.Load(project.FullName);
List<string> 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)}");
}
}
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="BenchmarkDotNet.Annotations" Version="0.15.8" />
<PackageVersion Include="Microsoft.Testing.Extensions.CrashDump" Version="1.7.2" />
<PackageVersion Include="Microsoft.Testing.Extensions.Fakes" Version="17.14.1" />
<PackageVersion Include="Microsoft.Testing.Extensions.HangDump" Version="1.7.2" />
Expand Down