Skip to content

Commit

Permalink
Support .NET 9.0 (#30)
Browse files Browse the repository at this point in the history
* Support .NET 9.0 (net9.0)

* Add net9.0 support to workflows

* Add net9.0 support to code coverage workflow

* Fix creation of tool package, update README.md
  • Loading branch information
cklutz authored Oct 22, 2024
1 parent 7ebdd75 commit 6942080
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 32 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
matrix:
runs-on: ['ubuntu-latest']
configuration: [Debug, Release]
dotnet-version: ['8.0.x']
tfm: ['net8.0']
dotnet-version: ['9.0.x']
tfm: ['net8.0', 'net9.0']
uses: ./.github/workflows/dotnet-reusable-workflow.yml
with:
runs-on: ${{ matrix.runs-on }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
matrix:
runs-on: ['windows-latest']
configuration: [Debug, Release]
dotnet-version: ['8.0.x']
tfm: ['net8.0', 'net481']
dotnet-version: ['9.0.x']
tfm: ['net8.0', 'net9.0', 'net481']
uses: ./.github/workflows/dotnet-reusable-workflow.yml
with:
runs-on: ${{ matrix.runs-on }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/upload-coverage-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ jobs:
env:
OUTPUT_DIR: _site/coverage
WORKFLOW_SPEC: >
dotnet-windows.yml: coverage-report-windows-net8.0-Release,coverage-report-windows-net481-Release |
dotnet-ubuntu.yml: coverage-report-ubuntu-net8.0-Release
dotnet-windows.yml: coverage-report-windows-net8.0-Release,coverage-report-windows-net9.0-Release,coverage-report-windows-net481-Release |
dotnet-ubuntu.yml: coverage-report-ubuntu-net8.0-Release,coverage-report-ubuntu-net9.0-Release
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
10 changes: 5 additions & 5 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<UseArtifactsOutput>true</UseArtifactsOutput>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>

<TargetFrameworks>net8.0;net481</TargetFrameworks>

<Nullable Condition="'$(TargetFramework)' == 'net8.0'">enable</Nullable>
<Nullable Condition="'$(TargetFramework)' == 'net481'">annotations</Nullable>
<TargetFrameworks>net8.0;net9.0;net481</TargetFrameworks>

<IsNetFramework>false</IsNetFramework>
<IsNetFramework Condition="'$(TargetFramework)' == 'net481'">true</IsNetFramework>


<Nullable Condition="'$(IsNetFramework)' != 'true'">enable</Nullable>
<Nullable Condition="'$(IsNetFramework)' == 'true'">annotations</Nullable>

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);CA1031;CA1303;CA1416;CA1801;CA1716;NU5105</NoWarn>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ Example:

| Build [^1] | Coverage [^2] |
| -----------| --------------|
| [![](https://github.com/cklutz/LockCheck/workflows/Windows/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AWindows) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net481-release%2FSummary.json&query=%24.summary.linecoverage&label=net%204.8&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net481-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Ubuntu/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AUbuntu) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net8.0-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Windows/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AWindows) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net9.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%209.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net9.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net481-release%2FSummary.json&query=%24.summary.linecoverage&label=net%204.8&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net481-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Ubuntu/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AUbuntu) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net9.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%209.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net9.0-release) |





[^1]: A build is done for every supported target framework for that platform (currently for Windows this is .NET 8.0 and .NET Framework 4.8, for Linux/Ubuntu this is .NET 8.0) in every supported configuration (Release and Debug).
[^2]: Code coverage is generated separately for every supported target framework for a platform, but only for the Release configuration. It is updated nightly from the latest build of the main branch.
Expand Down
63 changes: 61 additions & 2 deletions src/LockCheck/Linux/InodeInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;

namespace LockCheck.Linux
Expand All @@ -7,9 +7,67 @@ internal readonly struct InodeInfo
{
public static bool TryParse(ReadOnlySpan<char> field, out InodeInfo value)
{
#if NET9_0_OR_GREATER
// This implementation *look* more complex and thus slower, but is actually faster:
//
// | Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Allocated | Alloc Ratio |
// |------- |---------:|---------:|---------:|------:|--------:|----------:|----------:|------------:|
// | Old | 71.97 ns | 1.462 ns | 1.900 ns | 1.00 | 0.04 | 1,604 B | - | NA |
// | NET9.0 | 51.40 ns | 0.800 ns | 0.668 ns | 0.71 | 0.02 | 1,355 B | - | NA |

int num = 0;
int major = 0;
int minor = 0;
long number = 0;

foreach (var range in field.Split(':'))
{
var fieldContent = field[range].Trim();

switch (num)
{
case 0:
if (!int.TryParse(fieldContent, NumberStyles.HexNumber, null, out major))
{
goto fail;
}
break;
case 1:
if (!int.TryParse(fieldContent, NumberStyles.HexNumber, null, out minor))
{
goto fail;
}
break;
case 2:
if (!long.TryParse(fieldContent, NumberStyles.Integer, null, out number))
{
goto fail;
}
break;
}

if (++num > 2)
{
// Ignore additional fields
break;
}
}

if (num < 2)
{
// Not enough fields
goto fail;
}

value = new InodeInfo(major, minor, number);
return true;
fail:
value = default;
return false;
#else
int count = field.Count(':') + 1;
Span<Range> ranges = count < 128 ? stackalloc Range[count] : new Range[count];
int num = MemoryExtensions.Split(field, ranges, ':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
int num = MemoryExtensions.Split(field, ranges, ':', StringSplitOptions.TrimEntries);
if (num < 3 ||
!int.TryParse(field[ranges[0]], NumberStyles.HexNumber, null, out int major) ||
!int.TryParse(field[ranges[1]], NumberStyles.HexNumber, null, out int minor) ||
Expand All @@ -21,6 +79,7 @@ public static bool TryParse(ReadOnlySpan<char> field, out InodeInfo value)

value = new InodeInfo(major, minor, number);
return true;
#endif
}

private InodeInfo(int majorDeviceId, int minorDeviceId, long iNodeNumber)
Expand Down
50 changes: 44 additions & 6 deletions src/LockCheck/Linux/ProcFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,16 +385,54 @@ internal static string[] ConvertToArgs(ref Span<byte> buffer, int maxArgs = -1)
return args?.Length > 0 ? args[0] : null;
}

private static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
internal static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
{
int count = content.Count(delimiter) + 1;
Span<Range> ranges = count < 128 ? stackalloc Range[count] : new Range[count];
int num = MemoryExtensions.Split(content, ranges, delimiter);
if (index >= num)
if (index < 0)
{
throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {num} fields available.");
throw new ArgumentOutOfRangeException(nameof(index), index, $"Field index cannot be negative.");
}

#if NET9_0_OR_GREATER
// PERF NOTE: For larger index values this will perform actually worse than the manual usage of
// Count()/MemoryExtensions.Split() below. However, currently we use rather small indexes (5 out of 52)
// where this performs actually better.
// Also, this is cleaner an less error prone.
// In .NET 10+ the ref struct enumerator returned here will implement IEnumerable<> so that we could
// try using LINQ here, to make things even more simple (and possibly performant, as LINQ is getting
// improved also!)

int count = 0;
foreach (var range in content.Split(delimiter))
{
if (count < index)
{
count++;
continue;
}
return content[range];
}

throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {count} fields available.");
#else
int fieldCount = content.Count(delimiter) + 1;
if (fieldCount <= index)
{
throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {fieldCount} fields available.");
}

// We need to split into N+1 fields, where N is the field denoted by the index.
// The extra field will receive the remainder of content, that doesn't need to
// be split further, because we're not interested. That also means, that if we
// are supposed to read the last field of content, we don't need that extra field.
int rangeCount = index == fieldCount - 1 ? index + 1 : index + 2;
Span<Range> ranges = rangeCount < 128 ? stackalloc Range[rangeCount] : new Range[rangeCount];
int num = MemoryExtensions.Split(content, ranges, delimiter);

// Shouldn't trigger, because of pre-checks done above.
Debug.Assert(num == rangeCount);

return content[ranges[index]];
#endif
}
}
}
25 changes: 20 additions & 5 deletions src/LockCheckTool/LockCheckTool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable Condition="'$(TargetFramework)' != 'net8.0'">false</IsPackable>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

<PropertyGroup>
<RollForward>major</RollForward>
<Version>1.0.1</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>lockcheck</ToolCommandName>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>lockchecktool</PackageId>
<Authors>cklutz</Authors>
<Description>A tool to list processes locking a given file.</Description>
Expand All @@ -24,4 +22,21 @@
<ProjectReference Include="..\LockCheck\LockCheck.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.143" PrivateAssets="all" />
</ItemGroup>

<Target Name="AdjustPackagingDirectives" AfterTargets="CoreCompile">
<!-- Prevent "error NETSDK1054: only supports .NET Core."
Since we still multi target net481 as well, we'd get this error otherwise.
We cannot set these directly, because they are need to be set for cross-targeting
and not outer builds.
-->
<PropertyGroup>
<PackAsTool Condition="'$(TargetFramework)' == 'net481'">false</PackAsTool>
<IsPackable Condition="'$(TargetFramework)' == 'net481'">false</IsPackable>
<GeneratePackageOnBuild Condition="'$(TargetFramework)' == 'net481'">false</GeneratePackageOnBuild>
</PropertyGroup>
</Target>

</Project>
22 changes: 22 additions & 0 deletions test/LockCheck.Tests/Linux/ProcFileSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,27 @@ public void ConvertToArgs_ShouldReturnMaxArgs_WhenMaxArgsIsSet(int maxArgs, stri
Assert.AreEqual(expected.Length, args.Length);
CollectionAssert.AreEqual(expected, args);
}

[DataTestMethod]
[DataRow(0, "223424")]
[DataRow(1, "(bash)")]
[DataRow(2, "S")]
[DataRow(3, "223420")]
public void GetField_ShouldReturnField_WhenIndexIsInBounds(int index, string expected)
{
var result = ProcFileSystem.GetField("223424 (bash) S 223420".AsSpan(), ' ', index);
Assert.AreEqual(expected, result.ToString());
}

[DataTestMethod]
[DataRow(-1)]
[DataRow(4)]
[DataRow(int.MinValue)]
[DataRow(int.MaxValue)]
public void GetField_ShouldThrowArgumentOutOfRangeException_WhenIndexIsOutOfBounds(int index)
{
var ex = Assert.ThrowsException<ArgumentOutOfRangeException>(() => ProcFileSystem.GetField("223424 (bash) S 223420".AsSpan(), ' ', index));
Console.WriteLine(ex);
}
}
}
37 changes: 36 additions & 1 deletion test/test-docker-linux.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
docker run --rm --name LockCheck.Tests -v ${PSScriptRoot}/..:/mnt/lc -w /mnt/lc mcr.microsoft.com/dotnet/sdk:8.0 bash /mnt/lc/test/test-linux.sh
$ErrorActionPreference = 'Stop'

$SourcesRootDir=((Get-Item $PSScriptRoot).Parent.FullName)
$ContainerWorkDir="/mnt/lc"
$ImageName="$env:USERNAME-lockcheck-tests"
$ContainerName="LockCheck.Tests"
$ContextDir="$SourcesRootDir\artifacts\TestContainer"
$DockerFileContent=@'
FROM mcr.microsoft.com/dotnet/sdk:9.0 as build
# Copy .NET 8.0 runtime files
COPY --from=mcr.microsoft.com/dotnet/sdk:8.0 /usr/share/dotnet/shared /usr/share/dotnet/shared
'@

# Create a docker image that combines multiple dotnet versions
mkdir -Force $ContextDir | Out-Null
echo $DockerFileContent > "$ContextDir\Dockerfile"
docker build -q -t $ImageName $ContextDir
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to create container"
exit 1
}

# Run tests
docker run --rm --name $ContainerName -v ${SourcesRootDir}:$ContainerWorkDir -w $ContainerWorkDir $ImageName bash $ContainerWorkDir/test/test-linux.sh
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to run tests"
exit 1
}

# Don't rely on "prune" to be run eventually.
# If tests were successfull we don't need it anymore.
docker image rm $ImageName
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to remove image $ImageName"
exit 1
}
15 changes: 13 additions & 2 deletions test/test-linux.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
#!/bin/bash
#
# Run test cycle on linux. This script can be run directly from within Linux (e.g. WSL),
# and also serves as the driver to be run inside a docker container (see test-docker-linux.ps1).
#

frameworks=('net8.0')
frameworks=('net8.0' 'net9.0')
configurations=('Release' 'Debug')
platforms=('x64')
project="$(dirname $0)/LockCheck.Tests/LockCheck.Tests.csproj"
resultsDir="$(dirname $0)/../artifacts/TestResults"

export DOTNET_CLI_TELEMETRY_OPTOUT=1

# TODO: This issue https://github.com/dotnet/sdk/issues/29742 prevents us from running
# the build separately, like on Windows, and thus less often then with every test
# combination.
for framework in "${frameworks[@]}"; do
for configuration in "${configurations[@]}"; do
for platform in "${platforms[@]}"; do
echo -e "\n\033[34m[$framework - $configuration - $platform]\033[0m"
/usr/share/dotnet/dotnet test --logger console -c $configuration -f $framework -a $platform $project || exit 1
runPivot=$(echo "${configuration}_${framework}_linux-${platform}" | tr '[:upper:]' '[:lower:]')
/usr/share/dotnet/dotnet test --logger console --results-directory "$resultsDir/$runPivot" -c $configuration -f $framework -a $platform $project || exit 1
done
done
done
19 changes: 16 additions & 3 deletions test/test-windows.ps1
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
$frameworks = ('net481', 'net8.0')
$frameworks = ('net481', 'net8.0', 'net9.0')
$configurations = ('Debug', 'Release')
$platforms = ('x86', 'x64')
$project = "$PSScriptRoot\LockCheck.Tests\LockCheck.Tests.csproj"
$resultsDir = "$((Get-Item $PSScriptRoot).Parent.FullName)\artifacts\TestResults"

$env:DOTNET_CLI_TELEMETRY_OPTOUT=1

# Build once for every configuration (platforms and frameworks will be handled automatically)
# foreach ($configuration in $configurations) {
# & dotnet build -c $configuration $project
# if ($LASTEXITCODE -ne 0) {
# exit 1
# }
# }

# Run tests; dedicated per framework/configuration/platform so that the test runner itself can
# also uses the desired platform.
foreach ($framework in $frameworks) {
foreach ($platform in $platforms) {
foreach ($configuration in $configurations) {
Write-Host -Foreground DarkBlue "`n[$framework - $configuration - $platform]"

& dotnet test -c $configuration -f $framework -a $platform $project
$runPivot = "${configuration}_${framework}_win-${platform}".ToLowerInvariant()
& dotnet test --results-directory "$resultsDir\$runPivot" -c $configuration -f $framework -a $platform "$project"
if ($LASTEXITCODE -ne 0) {
exit 1
}
Expand Down
File renamed without changes.

0 comments on commit 6942080

Please sign in to comment.