Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 5 additions & 1 deletion .claude/docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ dotnet build TUnit.Dev.slnx --no-restore -graphBuild:True

Use `TUnit.slnx` for the full solution, including Roslyn version variants. Run `dotnet test` from the relevant test project directory. Core generator snapshots live in `tests/TUnit.Core.SourceGenerator.Tests`; public API snapshots live in `tests/TUnit.PublicAPI`.

Test projects target net8.0, net9.0 and net10.0; only installed runtimes can run, so pass `--framework net10.0` (or another installed one) when the machine lacks the others. The PublicAPI and generator snapshot projects add a net472 leg on Windows only; off Windows, `.Net4_7` snapshot updates come from the `ReceivedSnapshots-windows-latest` CI artifact.

Scripts in `scripts/` need PowerShell 7. On Linux and macOS, quote any argument containing `*` when calling a native command from PowerShell: unquoted arguments are glob-expanded, so a treenode filter becomes a list of file paths.

For the intentionally failing test application, always select the class or method under test:

```sh
cd tests/TUnit.TestProject
dotnet test --treenode-filter "/*/*/ClassName/*"
dotnet test --framework net10.0 --treenode-filter "/*/*/ClassName/*"
```

A single-method filter has the form `/Assembly/Namespace/ClassName/TestMethodName`. Run one filter per command; do not join paths with `|`.
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"ms-dotnettools.csdevkit"
],
"settings": {
"dotnet.defaultSolution": "TUnit.sln"
"dotnet.defaultSolution": "TUnit.Dev.slnx"
}
}
},
"postCreateCommand": "dotnet restore && dotnet build"
"postCreateCommand": "bash .devcontainer/install-runtimes.sh && dotnet build TUnit.Dev.slnx"
}
18 changes: 18 additions & 0 deletions .devcontainer/install-runtimes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# The SDK image ships a single runtime, but the test projects target net8.0, net9.0 and
# net10.0 (eng/TestProject.props). Install every runtime they need next to the image's SDK.
set -euo pipefail

dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)")")"
installer="$(mktemp)"
trap 'rm -f "$installer"' EXIT

curl -sSL https://dot.net/v1/dotnet-install.sh -o "$installer"

for channel in 8.0 9.0 10.0; do
for runtime in dotnet aspnetcore; do
bash "$installer" --channel "$channel" --runtime "$runtime" --install-dir "$dotnet_root" --no-path
done
done

dotnet --list-runtimes
7 changes: 6 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@
###############################################################################
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

###############################################################################
# PowerShell - always LF: pwsh reads LF on every OS (Windows PowerShell too), and
# a CRLF checkout breaks the #!/usr/bin/env pwsh shebang on Linux and macOS.
###############################################################################
*.ps1 text eol=lf

###############################################################################
# Verify snapshots - LF for consistent cross-platform diffs
Expand Down
35 changes: 32 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,38 @@ If want to provide sample code for complicated or useful different test suite se

## Building TUnit Locally

TUnit builds and tests on Windows, Linux and macOS.

### Prerequisites

- **.NET SDK** matching [`global.json`](../global.json).
- **.NET 8, 9 and 10 runtimes.** The test projects target all three, and a target framework whose
runtime is missing fails with "You must install or update .NET". Either run a single framework
(`--framework net10.0`, see below) or install the others, including the ASP.NET Core runtime, with
[dotnet-install](https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script)
(`--channel 8.0 --runtime dotnet`, `--channel 8.0 --runtime aspnetcore`, and so on). Homebrew's
`dotnet` formula ships one runtime only.
- **PowerShell 7** (`pwsh`) for the scripts in `scripts/`. On Linux and macOS they run directly, e.g.
`./scripts/run-reflection-tests.ps1`, or as `pwsh scripts/run-reflection-tests.ps1`.
- **Docker** for the ASP.NET Core and Aspire tests.
- **A Native AOT toolchain** for AOT publishing: the Visual Studio "Desktop development with C++"
workload on Windows, the Xcode Command Line Tools on macOS, and `clang` plus the zlib development
package on Linux. If .NET itself was installed with Homebrew, its AOT runtime links against
Homebrew's OpenSSL and Brotli:
`export LIBRARY_PATH=/opt/homebrew/opt/openssl@3/lib:/opt/homebrew/opt/brotli/lib`.

Any of Visual Studio, Rider, or VS Code with C# Dev Kit works; the repository's VS Code settings and
the dev container open `TUnit.Dev.slnx`.

### Solutions

TUnit ships several solution files. Pick the right one for your task — the
full `TUnit.slnx` is large enough that a cold build can take 20+ minutes,
which is rarely what contributors actually need:

| Solution | Projects | When to use |
| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `TUnit.Dev.slnx` | 44 | **Recommended for contributors.** Drops the per-Roslyn-version generator variants and example projects. Open this in Visual Studio / Rider for day-to-day work. |
| `TUnit.Dev.slnx` | 44 | **Recommended for contributors.** Drops the per-Roslyn-version generator variants and example projects. Open this in your IDE for day-to-day work. |
| `TUnit.CI.slnx` | 73 | What CI builds. Use only when you're verifying CI-specific changes. |
| `TUnit.slnx` | 96 | Full shipping graph including all per-Roslyn-version analyzer/generator variants. Slow; rarely needed locally. |

Expand All @@ -132,7 +157,7 @@ $env:MSBUILDUSESERVER = '1' # PowerShell

Source-generated files are not written to disk by default (saves significant
disk I/O on every build). To dump them under each test project's
`SourceGeneratedViewer\` folder, build with:
`SourceGeneratedViewer/` folder, build with:

```bash
dotnet build -p:EmitCompilerGeneratedFiles=true
Expand All @@ -153,11 +178,15 @@ TUnit supports both source-generated and reflection-based test discovery. **All

If your changes affect the source generator output or public APIs:

1. Run the relevant tests: `dotnet test tests/TUnit.Core.SourceGenerator.Tests` or `dotnet test tests/TUnit.PublicAPI`
1. Run the relevant tests: `dotnet test --project tests/TUnit.Core.SourceGenerator.Tests --framework net10.0` or `dotnet test --project tests/TUnit.PublicAPI --framework net10.0` (repeat for each framework you have installed)
2. Review any `.received.txt` files generated
3. If the changes are intentional, rename them to `.verified.txt`
4. Commit the `.verified.txt` files with your changes

The `.Net4_7` snapshots come from a .NET Framework leg that only builds on Windows. On Linux and macOS,
push your change and take the `*.received.*` files from the `ReceivedSnapshots-windows-latest` artifact
of the failed CI run.

#### Performance Considerations

TUnit is designed to handle millions of tests. When contributing:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ jobs:
name: TestingPlatformDiagnosticLogs${{matrix.os}}
path: '**/log_*.diag'

# Snapshot mismatches leave *.received.txt next to the *.verified.txt they failed against.
# The .Net4_7 snapshots can only be produced on Windows, so contributors on Linux/macOS
# take them from this artifact.
- name: Upload Received Snapshots
uses: actions/upload-artifact@v7.0.1
if: failure()
with:
name: ReceivedSnapshots-${{matrix.os}}
path: |
**/*.received.*
**/*.received/**
if-no-files-found: ignore

- name: Upload Hang Dumps
uses: actions/upload-artifact@v7.0.1
if: always()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# macOS Finder metadata
.DS_Store

# Mono auto generated files
mono_crash.*

Expand Down
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-dotnettools.csdevkit"
]
}
15 changes: 12 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"preLaunchTask": "build TUnit.TestProject",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/tests/TUnit.TestProject/bin/Debug/net8.0/TUnit.TestProject.dll",
"args": [],
"program": "${workspaceFolder}/tests/TUnit.TestProject/bin/Debug/net10.0/TUnit.TestProject.dll",
// TUnit.TestProject contains intentionally failing tests: always run a targeted filter.
"args": ["--treenode-filter", "${input:treenodeFilter}"],
"cwd": "${workspaceFolder}/tests/TUnit.TestProject",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
Expand All @@ -22,5 +23,13 @@
"type": "coreclr",
"request": "attach"
}
],
"inputs": [
{
"id": "treenodeFilter",
"type": "promptString",
"description": "Tree-node filter, e.g. /*/*/ClassName/*",
"default": "/*/*/ClassName/*"
}
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// The repository root holds three solutions; C# Dev Kit needs to be told which to load.
"dotnet.defaultSolution": "TUnit.Dev.slnx"
}
15 changes: 10 additions & 5 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/TUnit.slnx",
"${workspaceFolder}/TUnit.Dev.slnx",
Comment thread
Quattordici marked this conversation as resolved.
Resolved
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
// TUnit.Dev.slnx does not include TUnit.TestProject, so the debug launch builds it directly.
"label": "build TUnit.TestProject",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/TUnit.slnx",
"build",
"${workspaceFolder}/tests/TUnit.TestProject",
"--framework",
"net10.0",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
Expand All @@ -33,7 +36,9 @@
"watch",
"run",
"--project",
"${workspaceFolder}/TUnit.slnx"
"${workspaceFolder}/tests/TUnit.UnitTests",
"--framework",
"net10.0"
],
"problemMatcher": "$msCompile"
}
Expand Down
Empty file modified benchmarks/TUnit.Performance.Tests/run-benchmarks.ps1
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

param(
[int]$Scale = 1000,
[string]$OutputDir = ".\Results"
[string]$OutputDir = "Results"
)

$ErrorActionPreference = "Stop"
Expand All @@ -17,15 +17,17 @@ try {

# Regenerate tests for the specified scale
Write-Host "Generating tests for scale: $Scale" -ForegroundColor Cyan
& pwsh -ExecutionPolicy Bypass -File "$projectDir\generate-tests.ps1" -Scale $Scale
& pwsh -ExecutionPolicy Bypass -File (Join-Path $projectDir "generate-tests.ps1") -Scale $Scale

# Build the project
Write-Host "Building project..." -ForegroundColor Cyan
dotnet build -c Release --no-restore

# Generate timestamp for output files
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$traceFile = "$OutputDir\cpu-trace-$Scale-$timestamp.nettrace"
# Join-Path, not "\": dotnet-trace is a native command, so PowerShell does not translate the
# separator for it on Linux/macOS.
$traceFile = Join-Path $OutputDir "cpu-trace-$Scale-$timestamp.nettrace"

Write-Host "Starting CPU profile capture..." -ForegroundColor Cyan
Write-Host "Trace will be saved to: $traceFile" -ForegroundColor Yellow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

param(
[int]$Scale = 1000,
[string]$OutputDir = ".\Results"
[string]$OutputDir = "Results"
)

$ErrorActionPreference = "Stop"
Expand All @@ -17,15 +17,17 @@ try {

# Regenerate tests for the specified scale
Write-Host "Generating tests for scale: $Scale" -ForegroundColor Cyan
& pwsh -ExecutionPolicy Bypass -File "$projectDir\generate-tests.ps1" -Scale $Scale
& pwsh -ExecutionPolicy Bypass -File (Join-Path $projectDir "generate-tests.ps1") -Scale $Scale

# Build the project
Write-Host "Building project..." -ForegroundColor Cyan
dotnet build -c Release --no-restore

# Generate timestamp for output files
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$traceFile = "$OutputDir\gc-trace-$Scale-$timestamp.nettrace"
# Join-Path, not "\": dotnet-trace is a native command, so PowerShell does not translate the
# separator for it on Linux/macOS.
$traceFile = Join-Path $OutputDir "gc-trace-$Scale-$timestamp.nettrace"

Write-Host "Starting GC/allocation profile capture..." -ForegroundColor Cyan
Write-Host "Trace will be saved to: $traceFile" -ForegroundColor Yellow
Expand Down
19 changes: 15 additions & 4 deletions benchmarks/TUnit.PerformanceBenchmarks/Profiles/run-baseline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Usage: .\run-baseline.ps1

param(
[string]$OutputDir = ".\Results"
[string]$OutputDir = "Results"
)

$ErrorActionPreference = "Stop"
Expand All @@ -11,7 +11,18 @@ $projectDir = Split-Path -Parent $scriptDir

$scales = @(100, 500, 1000, 5000, 10000)
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$resultsFile = "$OutputDir\baseline-$timestamp.md"
$resultsFile = Join-Path $OutputDir "baseline-$timestamp.md"

# Get-CimInstance is Windows-only; ask each platform in its own way.
function Get-CpuName {
try {
if ($IsMacOS) { return (sysctl -n machdep.cpu.brand_string) }
if ($IsLinux) { return ((Get-Content /proc/cpuinfo | Select-String -Pattern "^model name\s*:\s*(.+)$" | Select-Object -First 1).Matches.Groups[1].Value) }
return (Get-CimInstance Win32_Processor).Name
} catch {
return "unknown"
}
}

Push-Location $projectDir
try {
Expand All @@ -25,7 +36,7 @@ Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")

## Environment
- OS: $([System.Environment]::OSVersion.VersionString)
- CPU: $((Get-CimInstance Win32_Processor).Name)
- CPU: $(Get-CpuName)
- .NET: $(dotnet --version)
- TUnit: $(git describe --tags --always 2>$null || 'unknown')

Expand All @@ -41,7 +52,7 @@ Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Write-Host "========================================`n" -ForegroundColor Cyan

# Regenerate tests for the specified scale
& pwsh -ExecutionPolicy Bypass -File "$projectDir\generate-tests.ps1" -Scale $scale
& pwsh -ExecutionPolicy Bypass -File (Join-Path $projectDir "generate-tests.ps1") -Scale $scale

# Build the project
Write-Host "Building project..." -ForegroundColor Yellow
Expand Down
Empty file modified benchmarks/TUnit.PerformanceBenchmarks/compare-branches.ps1
100644 → 100755
Empty file.
Empty file.
Empty file modified benchmarks/TUnit.PerformanceBenchmarks/measure-jit.ps1
100644 → 100755
Empty file.
Empty file modified benchmarks/TUnit.PerformanceBenchmarks/measure-startup.ps1
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion scripts/Remove-MergedWorktrees.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ try {
if ($gitdir -notlike "$mainNorm/.git/worktrees/*") { continue }
if (Test-Path -LiteralPath $gitdir) { continue }
if ($WhatIf) { Write-Host "sweep: WOULD remove orphaned dir $($dir.FullName) (dangling gitdir: $gitdir)"; continue }
Remove-Item -LiteralPath ('\\?\' + ($dir.FullName -replace '/', '\')) -Recurse -Force -ErrorAction SilentlyContinue
Remove-DirectoryTree -Path $dir.FullName
if (Test-Path -LiteralPath $dir.FullName) {
Write-Host "sweep: WARNING could not fully remove orphaned dir $($dir.FullName)"
}
Expand Down
19 changes: 19 additions & 0 deletions scripts/Test-WorktreeCleanup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ try {
New-Item -ItemType Directory -Path (Split-Path $generatedFile -Parent) | Out-Null
Set-Content -LiteralPath $generatedFile -Value 'generated'

# A link to the worktree must not be followed: removing through it would delete the target.
$linkToWorktree = Join-Path $testRoot 'link-to-artifacts'
$linkCreated = $true
try { New-Item -ItemType SymbolicLink -Path $linkToWorktree -Target $artifactWorktree | Out-Null }
catch { $linkCreated = $false; Write-Host "Skipping link check: cannot create symbolic links here ($($_.Exception.Message))" }
if ($linkCreated) {
Remove-MergedWorktree -Repo $repo -Worktree $linkToWorktree -Label 'link fixture'
Assert-True (Test-Path -LiteralPath $artifactWorktree) 'Removing through a link deleted the linked worktree.'
Remove-Item -LiteralPath $linkToWorktree -Force
}

Remove-MergedWorktree -Repo $repo -Worktree $artifactWorktree -Label 'artifact fixture'
Assert-True (-not (Test-Path -LiteralPath $artifactWorktree)) 'Artifact-only worktree was not removed.'

Expand Down Expand Up @@ -156,6 +167,14 @@ try {
"Repository-generated path was not treated as disposable: $generatedPath"
}

# The fallback delete must work with git's forward-slash paths on every OS: the Windows
# long-path prefix it adds there makes a Linux/macOS path unresolvable.
$fallbackTarget = Join-Path $testRoot 'fallback-delete'
New-Item -ItemType Directory -Path (Join-Path $fallbackTarget 'nested/deeper') -Force | Out-Null
Set-Content -LiteralPath (Join-Path $fallbackTarget 'nested/deeper/file.txt') -Value 'x'
Remove-DirectoryTree -Path ($fallbackTarget -replace '\\', '/')
Assert-True (-not (Test-Path -LiteralPath $fallbackTarget)) 'Fallback delete did not remove the directory.'

Write-Host 'OK worktree cleanup safety tests passed.'
}
finally {
Expand Down
Loading
Loading