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
192 changes: 192 additions & 0 deletions .github/scripts/run-native-aot-smoke.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
[CmdletBinding(DefaultParameterSetName = 'Run')]
param(
[Parameter(Mandatory, ParameterSetName = 'List')]
[switch] $List,

[Parameter(Mandatory, ParameterSetName = 'Run')]
[string] $Scenario,

[Parameter(Mandatory, ParameterSetName = 'Run')]
[ValidateSet('win-x64', 'linux-x64')]
[string] $RuntimeIdentifier,

[Parameter(ParameterSetName = 'Run')]
[string] $ResultsDirectory,

[Parameter(ParameterSetName = 'Run')]
[switch] $UseEnvironmentalTools,

[Parameter()]
[string] $RepositoryRoot
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
if ([string]::IsNullOrWhiteSpace($RepositoryRoot))
{
$RepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
}

$RepositoryRoot = [System.IO.Path]::GetFullPath($RepositoryRoot)
$projectDirectory = Join-Path (Join-Path $repositoryRoot 'test') 'Orleans.NativeAotSmoke'
$manifests = @(Get-ChildItem -LiteralPath $projectDirectory -Filter '*.smoke.json' -File | Sort-Object Name)
if ($manifests.Count -eq 0)
{
throw "No native smoke scenario manifests were found in '$projectDirectory'."
}

$names = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$scenarios = foreach ($file in $manifests)
{
$manifest = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json -AsHashtable
foreach ($property in @('scenario', 'diagnostics', 'rejectDiagnostics'))
{
if (-not $manifest.ContainsKey($property))
{
throw "Manifest '$($file.Name)' is missing '$property'."
}
}

foreach ($property in $manifest.Keys)
{
if ($property -notin @('scenario', 'diagnostics', 'rejectDiagnostics'))
{
throw "Manifest '$($file.Name)' has unknown property '$property'."
}
}

if ($manifest.scenario -isnot [string] -or
$manifest.scenario -cnotmatch '^[A-Za-z][A-Za-z0-9]*$' -or
$file.Name -cne "$($manifest.scenario).smoke.json")
{
throw "Manifest '$($file.Name)' must name its alphanumeric scenario exactly."
}

if (-not $names.Add($manifest.scenario))
{
throw "Native smoke scenario '$($manifest.scenario)' is registered more than once."
}

if ($manifest.diagnostics -notin @('strict', 'legacy-visible') -or
$manifest.rejectDiagnostics -isnot [array])
{
throw "Manifest '$($file.Name)' must specify a diagnostic policy and an array of rejection patterns."
}

if ($manifest.diagnostics -eq 'legacy-visible' -and $manifest.rejectDiagnostics.Count -eq 0)
{
throw "Manifest '$($file.Name)' must gate its supported path while retaining legacy diagnostics."
}

foreach ($pattern in $manifest.rejectDiagnostics)
{
if ($pattern -isnot [string] -or [string]::IsNullOrWhiteSpace($pattern))
{
throw "Manifest '$($file.Name)' contains an empty diagnostic rejection pattern."
}

[void] [regex]::new($pattern)
}

if ($manifest.scenario -ne 'DependencyInjection' -and
@(Get-ChildItem -LiteralPath $projectDirectory -Filter "$($manifest.scenario)*.cs" -File).Count -eq 0)
{
throw "Manifest '$($file.Name)' has no matching scenario source files."
}

$manifest
}

if ($List)
{
$include = foreach ($manifest in $scenarios)
{
[ordered] @{ scenario = $manifest.scenario; runner = 'ubuntu-latest'; runtime = 'linux-x64' }
[ordered] @{ scenario = $manifest.scenario; runner = 'windows-latest'; runtime = 'win-x64' }
}

[ordered] @{ include = @($include) } | ConvertTo-Json -Depth 4 -Compress
return
}

$selected = @($scenarios | Where-Object { $_.scenario -ceq $Scenario })
if ($selected.Count -ne 1)
{
throw "Native smoke scenario '$Scenario' is not registered."
}

$selected = $selected[0]
if ([string]::IsNullOrWhiteSpace($ResultsDirectory))
{
$ResultsDirectory = Join-Path (Join-Path (Join-Path $repositoryRoot 'Artifacts') 'NativeAotSmoke') "$Scenario-$RuntimeIdentifier"
}

$ResultsDirectory = [System.IO.Path]::GetFullPath($ResultsDirectory)
New-Item -ItemType Directory -Force -Path $ResultsDirectory | Out-Null
$output = Join-Path $ResultsDirectory 'native'
$publishLog = Join-Path $ResultsDirectory 'publish.log'
$runtimeLog = Join-Path $ResultsDirectory 'runtime.log'
$publishArguments = @(
'publish', (Join-Path $projectDirectory 'Orleans.NativeAotSmoke.csproj'),
'--configuration', 'Release',
'--framework', 'net10.0',
'--runtime', $RuntimeIdentifier,
'--self-contained', 'true',
'--output', $output,
"-p:NativeAotSmokeScenario=$Scenario",
'-p:TrimmerSingleWarn=false',
'-m:1',
"-bl:$(Join-Path $ResultsDirectory 'publish.binlog')"
)

if ($UseEnvironmentalTools)
{
$publishArguments += '-p:IlcUseEnvironmentalTools=true'
}

if ($selected.diagnostics -eq 'legacy-visible')
{
$publishArguments += @(
'-p:ILLinkTreatWarningsAsErrors=false',
'-p:IlcTreatWarningsAsErrors=false',
'-p:WarningsNotAsErrors=IL2026%3BIL2055%3BIL2057%3BIL2060%3BIL2067%3BIL2070%3BIL2071%3BIL2077%3BIL2090%3BIL2091%3BIL2096%3BIL3050%3BIL4000'
)
}
else
{
$publishArguments += @(
'-p:TreatWarningsAsErrors=true',
'-p:ILLinkTreatWarningsAsErrors=true',
'-p:IlcTreatWarningsAsErrors=true'
)
}

& dotnet @publishArguments *>&1 | Tee-Object -FilePath $publishLog
$publishExitCode = $LASTEXITCODE
if ($publishExitCode -ne 0)
{
throw "Native smoke '$Scenario' publish failed with exit code $publishExitCode. See '$publishLog'."
}

$diagnostics = @(Select-String -LiteralPath $publishLog -Pattern '(warning|error)\s+IL[0-9]+')
foreach ($pattern in $selected.rejectDiagnostics)
{
$rejected = @($diagnostics | Where-Object { $_.Line -match $pattern })
if ($rejected.Count -gt 0)
{
throw "Native smoke '$Scenario' emitted supported-path diagnostics:`n$($rejected.Line -join [Environment]::NewLine)"
}
}

$executable = Join-Path $output $(if ($IsWindows) { 'Orleans.NativeAotSmoke.exe' } else { 'Orleans.NativeAotSmoke' })
if (-not (Test-Path -LiteralPath $executable -PathType Leaf))
{
throw "Native smoke '$Scenario' publish produced no executable at '$executable'."
}

& $executable *>&1 | Tee-Object -FilePath $runtimeLog
$runtimeExitCode = $LASTEXITCODE
if ($runtimeExitCode -ne 0)
{
throw "Native smoke '$Scenario' execution failed with exit code $runtimeExitCode. See '$runtimeLog'."
}
66 changes: 65 additions & 1 deletion .github/workflows/analyzer-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ jobs:
name: Orleans.DependencyInjection trimmed smoke for selected generic DI constructor flows (net10.0)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RESULTS_DIRECTORY: Artifacts/AnalyzerAudit/DependencyInjection/trimmed
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Setup .NET
Expand All @@ -208,10 +210,72 @@ jobs:
shell: pwsh
run: |
$output = Join-Path $env:RUNNER_TEMP 'orleans-dependency-injection-trimmed-smoke'
New-Item -ItemType Directory -Force $env:RESULTS_DIRECTORY | Out-Null
dotnet publish test/Orleans.DependencyInjection.TrimmedSmoke/Orleans.DependencyInjection.TrimmedSmoke.csproj `
--framework net10.0 `
--configuration Release `
--runtime linux-x64 `
--self-contained true `
--output $output
--output $output `
-bl:$env:RESULTS_DIRECTORY/publish.binlog
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& (Join-Path $output 'Orleans.DependencyInjection.TrimmedSmoke')
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Upload dependency injection smoke diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: dependency-injection-trimmed-smoke
retention-days: 1
if-no-files-found: warn
path: ${{ env.RESULTS_DIRECTORY }}

native-aot-smoke-matrix:
name: Discover NativeAOT smoke scenarios
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.scenarios.outputs.matrix }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Discover registered scenarios
id: scenarios
shell: pwsh
run: |
$matrix = ./.github/scripts/run-native-aot-smoke.ps1 -List
"matrix=$matrix" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8NoBOM

native-aot-smoke:
name: NativeAOT ${{ matrix.scenario }} smoke (${{ matrix.runtime }}, net10.0)
needs: native-aot-smoke-matrix
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.native-aot-smoke-matrix.outputs.matrix) }}
env:
RESULTS_DIRECTORY: Artifacts/AnalyzerAudit/NativeAot/${{ matrix.scenario }}/${{ matrix.runtime }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Setup .NET
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
with:
global-json-file: global.json
- name: Publish, check diagnostics, and execute native scenario
shell: pwsh
run: |
./.github/scripts/run-native-aot-smoke.ps1 `
-Scenario '${{ matrix.scenario }}' `
-RuntimeIdentifier '${{ matrix.runtime }}' `
-ResultsDirectory $env:RESULTS_DIRECTORY
- name: Upload native smoke diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: native-aot-${{ matrix.scenario }}-${{ matrix.runtime }}
retention-days: 1
if-no-files-found: warn
path: |
${{ env.RESULTS_DIRECTORY }}/publish.binlog
${{ env.RESULTS_DIRECTORY }}/publish.log
${{ env.RESULTS_DIRECTORY }}/runtime.log
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ dotnet test --solution Orleans.slnx --framework net10.0 --filter-trait "Category
dotnet test --project test/Orleans.Core.Tests/Orleans.Core.Tests.csproj --framework net10.0 --filter-class "*MyTestClass*" --filter-method "*MyTestMethod*" --minimum-expected-tests 1
```

### NativeAOT smoke tests

The reusable `test\Orleans.NativeAotSmoke` project publishes selected scenarios as native executables. Its default dependency-injection scenario shares the existing trimmed smoke's source and exercises placement strategy and director registration, keyed service resolution, constructor dependency identity, and lifecycle-driven startup task activation. The static analysis workflow publishes and executes these flows with both trimming and NativeAOT.

To execute the native smoke on Windows, install the [NativeAOT C++ toolchain prerequisites](https://learn.microsoft.com/dotnet/core/deploying/native-aot/), then run:

```console
dotnet publish test\Orleans.NativeAotSmoke\Orleans.NativeAotSmoke.csproj --configuration Release --runtime win-x64 --output Artifacts\NativeAotSmoke
Artifacts\NativeAotSmoke\Orleans.NativeAotSmoke.exe
```

`PublishAot` is enabled within the native smoke project, keeping build-time analyzers and source generators on their managed target frameworks. The executable verifies that runtime code generation is disabled before exercising the selected flows. Additional scenarios use `-p:NativeAotSmokeScenario=<name>` and share the same project; each build compiles that scenario's `<name>*.cs` files. The existing `Orleans.DependencyInjection.TrimmedSmoke` project exercises the same DI flows in a self-contained trimmed application.

Register each native scenario with a `<name>.smoke.json` manifest beside its source. The existing static analysis workflow discovers these manifests and runs one centralized matrix on Windows and Linux. The shared runner publishes the scenario, checks linker/AOT diagnostics, executes the native binary, and retains diagnostic logs and a binlog:

```powershell
.\.github\scripts\run-native-aot-smoke.ps1 -List
.\.github\scripts\run-native-aot-smoke.ps1 -Scenario DependencyInjection -RuntimeIdentifier win-x64
```

Each manifest declares `scenario`, `diagnostics`, and `rejectDiagnostics`. Use `strict` for warning-free native paths. A bounded compatibility rollout can use `legacy-visible` with nonempty diagnostic rejection patterns for the supported path; the runner retains the existing backlog visibly and rejects matching new-path warnings and errors. Feature PRs add scenario sources and manifests to this shared harness.

### Work on an application with local source

For an application which needs to exercise an unreleased change, reference the relevant project under `src` instead of the published NuGet package. For example, reference `src/Orleans.Core/Orleans.Core.csproj` from the application project and build the application together with the Orleans solution. This lets the debugger step into the local Orleans source.
Expand Down
2 changes: 1 addition & 1 deletion test/Orleans.DependencyInjection.TrimmedSmoke/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private static async Task Main()
ValidateRuntimeRegistration();
await ValidateStartupTaskActivation();

Console.WriteLine("Selected generic DI constructor flows survived the self-contained trimmed smoke.");
Console.WriteLine("Selected generic DI constructor flows activated successfully.");
}

private static void ValidateCoreRegistration()
Expand Down
5 changes: 5 additions & 0 deletions test/Orleans.NativeAotSmoke/DependencyInjection.smoke.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"scenario": "DependencyInjection",
"diagnostics": "strict",
"rejectDiagnostics": []
}
16 changes: 16 additions & 0 deletions test/Orleans.NativeAotSmoke/NativeAotEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Runtime.CompilerServices;

namespace Orleans.NativeAotSmoke;

internal static class NativeAotEnvironment
{
[ModuleInitializer]
internal static void Validate()
{
if (RuntimeFeature.IsDynamicCodeSupported)
{
throw new InvalidOperationException("NativeAOT smoke scenarios require runtime code generation to be disabled.");
}
}
}
32 changes: 32 additions & 0 deletions test/Orleans.NativeAotSmoke/Orleans.NativeAotSmoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<IsOrleansFrameworkPart>false</IsOrleansFrameworkPart>
<NativeAotSmokeScenario Condition="'$(NativeAotSmokeScenario)' == ''">DependencyInjection</NativeAotSmokeScenario>
<OrleansBuildTimeCodeGen Condition="'$(NativeAotSmokeScenario)' != 'DependencyInjection'">true</OrleansBuildTimeCodeGen>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DefineConstants>$(DefineConstants);NATIVE_AOT_SMOKE</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(SourceRoot)src\Orleans.Core\Orleans.Core.csproj"
Condition="'$(NativeAotSmokeScenario)' == 'DependencyInjection' or '$(NativeAotSmokeScenario)' == 'GrainReferences' or '$(NativeAotSmokeScenario)' == 'Silo'" />
<ProjectReference Include="$(SourceRoot)src\Orleans.Runtime\Orleans.Runtime.csproj"
Condition="'$(NativeAotSmokeScenario)' == 'DependencyInjection' or '$(NativeAotSmokeScenario)' == 'Silo'" />
<ProjectReference Include="$(SourceRoot)src\Orleans.Serialization\Orleans.Serialization.csproj" />
<Compile Include="NativeAotEnvironment.cs" />
<Compile Include="..\Orleans.DependencyInjection.TrimmedSmoke\Program.cs"
Link="DependencyInjection.cs"
Condition="'$(NativeAotSmokeScenario)' == 'DependencyInjection'" />
<Compile Include="$(NativeAotSmokeScenario)*.cs"
Condition="'$(NativeAotSmokeScenario)' != 'DependencyInjection'" />
</ItemGroup>

</Project>
Loading