Skip to content

Commit 772c83f

Browse files
dmytroettDmytro.Ett
andauthored
Rework API surface (#15)
* WIP * Move options to separate file * First pass * Fixing AOT issues * Second pass * Slight tweak * Fixing compilation issues * No longer partial * All of the tests pass * Simplify snapshot management since verify does this for me. * Rework so that I no longer need registry * Adjusting the tests (I will adjust them later one more time). Verify is actually a bit annoying want to say. * We're done * finished unit tests * Adding a bit more of integration tests * Adding tests to cover global namespaces * Adding some namespace tests * We're inching forward * some more tests to cover interface generation * Added tiny readme * Added scripts to automatically accept pending Verify snapshots * Added unit testing skill * Adjust skills and agent * agents.md * this is good * Skill is done * nit * Format + couple of instructions * Updated scripts --------- Co-authored-by: Dmytro.Ett <dmytro.ett@outlook.com>
1 parent d6eea46 commit 772c83f

File tree

151 files changed

+2786
-2961
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

151 files changed

+2786
-2961
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
name: snapshot-unit-testing
3+
description: Write or update unit (not integration) tests in this repo, use Verify snapshot testing for source generator output, and run/accept snapshots via the provided scripts.
4+
---
5+
6+
# Scope
7+
8+
Locate unit tests under `test/*UnitTests`. Use Verify snapshots when validating generated output; prefer normal assertions for non-generator logic.
9+
10+
# Principles
11+
12+
Favor a small number of representative scenarios; combine related assertions to minimize snapshot files even if individual tests are larger.
13+
14+
Avoid opening or reading `.received`/`.verified` files; rely on `dotnet test` output and VerifyException details only.
15+
16+
Treat `scripts/accept-snapshot` and `scripts/accept-all-snapshots` as black boxes; do not open or inspect them.
17+
18+
# Workflow
19+
20+
Write or update the test based on a concrete scenario.
21+
22+
Run targeted tests with `dotnet test --filter` and add `--no-build`/`--no-restore` when appropriate.
23+
24+
Inspect `dotnet test` output to decide whether the test logic is wrong or snapshots need updating; do not open raw snapshot files.
25+
26+
Accept snapshots only when the behavior change is intended and clearly understood:
27+
- Use `scripts/accept-snapshot <TestClassName> <TestMethodName>` for a single test.
28+
- Use `scripts/accept-all-snapshots` for bulk updates.
29+
- Skip these scripts for non-snapshot tests.
30+
31+
Re-run tests and iterate until they pass.
32+
33+
# Commands
34+
35+
- Run a single test:
36+
- `dotnet test --filter "FullyQualifiedName~<TestClass>.<TestMethod>"`
37+
- Accept one snapshot:
38+
- `scripts/accept-snapshot <TestClassName> <TestMethodName>`
39+
- Accept all snapshots:
40+
- `scripts/accept-all-snapshots`
41+
42+
# Verify failure pattern
43+
44+
Use the failure output only; do not open the files listed.
45+
46+
```
47+
VerifyException : Directory: /home/me/projects/AttributedDI/test/AttributedDI.SourceGenerator.UnitTests/Snapshots
48+
NotEqual:
49+
- Received: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.DotNet9_0.received.txt
50+
Verified: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.verified.txt
51+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
cat <<'USAGE'
6+
Usage: accept-all-snapshots
7+
8+
Accepts Verify snapshots for all tests by promoting one TFM's received files
9+
per test to verified files and removing the other received files. When multiple
10+
TFMs exist for a test, the highest TFM version is accepted.
11+
USAGE
12+
}
13+
14+
if [[ $# -ne 0 ]]; then
15+
usage
16+
exit 1
17+
fi
18+
19+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
repo_root="$(cd "$script_dir/../../../.." && pwd)"
21+
snapshots_dir="$repo_root/test/AttributedDI.SourceGenerator.UnitTests/Snapshots"
22+
23+
if [[ ! -d "$snapshots_dir" ]]; then
24+
echo "Snapshots directory not found: $snapshots_dir" >&2
25+
exit 1
26+
fi
27+
28+
shopt -s nullglob
29+
30+
received_files=("$snapshots_dir"/*.received.*)
31+
32+
if [[ ${#received_files[@]} -eq 0 ]]; then
33+
echo "No received files found in $snapshots_dir" >&2
34+
exit 0
35+
fi
36+
37+
declare -A seen
38+
39+
for received_path in "${received_files[@]}"; do
40+
file_name="$(basename "$received_path")"
41+
base_name=""
42+
if [[ "$file_name" =~ ^(.+)\.DotNet[0-9]+_[0-9]+\.received\. ]]; then
43+
base_name="${BASH_REMATCH[1]}"
44+
elif [[ "$file_name" == *.received.* ]]; then
45+
base_name="${file_name%.received.*}"
46+
fi
47+
48+
if [[ -z "$base_name" ]]; then
49+
continue
50+
fi
51+
52+
if [[ -n "${seen[$base_name]+x}" ]]; then
53+
continue
54+
fi
55+
56+
seen[$base_name]=1
57+
test_class="${base_name%.*}"
58+
test_method="${base_name##*.}"
59+
"$script_dir/accept-snapshot" "$test_class" "$test_method"
60+
done
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Accepts Verify snapshots for all tests.
5+
6+
.DESCRIPTION
7+
Promotes one TFM's received files per test to verified files and removes the other
8+
received files. When multiple TFMs exist for a test, the highest TFM version is accepted.
9+
10+
.EXAMPLE
11+
./accept-all-snapshots.ps1
12+
#>
13+
[CmdletBinding()]
14+
param()
15+
16+
Set-StrictMode -Version Latest
17+
$ErrorActionPreference = 'Stop'
18+
19+
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
20+
$snapshotsDir = Join-Path $repoRoot 'test/AttributedDI.SourceGenerator.UnitTests/Snapshots'
21+
22+
if (-not (Test-Path -Path $snapshotsDir -PathType Container)) {
23+
throw "Snapshots directory not found: $snapshotsDir"
24+
}
25+
26+
$receivedFiles = @(Get-ChildItem -Path $snapshotsDir -Filter '*.received.*' -File -ErrorAction SilentlyContinue)
27+
28+
if ($receivedFiles.Count -eq 0) {
29+
Write-Output "No received files found in $snapshotsDir"
30+
exit 0
31+
}
32+
33+
$seen = @{}
34+
35+
foreach ($receivedFile in $receivedFiles) {
36+
$fileName = $receivedFile.Name
37+
$baseName = ''
38+
39+
if ($fileName -match '^(?<base>.+)\.DotNet\d+_\d+\.received\.') {
40+
$baseName = $Matches['base']
41+
} elseif ($fileName -like '*.received.*') {
42+
$baseName = $fileName -replace '\.received\..*$', ''
43+
}
44+
45+
if (-not $baseName) {
46+
continue
47+
}
48+
49+
if ($seen.ContainsKey($baseName)) {
50+
continue
51+
}
52+
53+
$seen[$baseName] = $true
54+
$lastDotIndex = $baseName.LastIndexOf('.')
55+
if ($lastDotIndex -lt 1) {
56+
continue
57+
}
58+
59+
$testClass = $baseName.Substring(0, $lastDotIndex)
60+
$testMethod = $baseName.Substring($lastDotIndex + 1)
61+
62+
& (Join-Path $PSScriptRoot 'accept-snapshot.ps1') $testClass $testMethod
63+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
cat <<'USAGE'
6+
Usage: accept-snapshot <TestClassName> <TestMethodName>
7+
8+
Accepts Verify snapshots for a single test by promoting one TFM's received files
9+
to verified files and removing the other received files. When multiple TFMs
10+
exist, the highest TFM version is accepted.
11+
USAGE
12+
}
13+
14+
if [[ $# -ne 2 ]]; then
15+
usage
16+
exit 1
17+
fi
18+
19+
test_class="$1"
20+
test_method="$2"
21+
22+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23+
repo_root="$(cd "$script_dir/../../../.." && pwd)"
24+
snapshots_dir="$repo_root/test/AttributedDI.SourceGenerator.UnitTests/Snapshots"
25+
26+
if [[ ! -d "$snapshots_dir" ]]; then
27+
echo "Snapshots directory not found: $snapshots_dir" >&2
28+
exit 1
29+
fi
30+
31+
base_name="${test_class}.${test_method}"
32+
33+
shopt -s nullglob
34+
35+
select_files_for_tfm() {
36+
local tfm="$1"
37+
local files=("$snapshots_dir/${base_name}.${tfm}.received."*)
38+
printf '%s\n' "${files[@]}"
39+
}
40+
41+
tfm_key() {
42+
local tfm="$1"
43+
if [[ "$tfm" =~ ^DotNet([0-9]+)_([0-9]+)$ ]]; then
44+
printf '%s\n' "$((10#${BASH_REMATCH[1]} * 1000 + 10#${BASH_REMATCH[2]}))"
45+
else
46+
printf '%s\n' "-1"
47+
fi
48+
}
49+
50+
selected_files=()
51+
selected_tfm=""
52+
53+
escaped_base_name="${base_name//./\\.}"
54+
tfms=()
55+
for received_path in "$snapshots_dir/${base_name}".*.received.*; do
56+
file_name="$(basename "$received_path")"
57+
if [[ "$file_name" =~ ^${escaped_base_name}\.DotNet([0-9]+)_([0-9]+)\.received\. ]]; then
58+
tfms+=("DotNet${BASH_REMATCH[1]}_${BASH_REMATCH[2]}")
59+
fi
60+
done
61+
62+
if [[ ${#tfms[@]} -gt 0 ]]; then
63+
best_tfm=""
64+
best_key="-1"
65+
for tfm in "${tfms[@]}"; do
66+
key="$(tfm_key "$tfm")"
67+
if [[ "$key" -gt "$best_key" ]]; then
68+
best_key="$key"
69+
best_tfm="$tfm"
70+
fi
71+
done
72+
if [[ -n "$best_tfm" ]]; then
73+
selected_tfm="$best_tfm"
74+
mapfile -t selected_files < <(select_files_for_tfm "$selected_tfm")
75+
fi
76+
fi
77+
78+
if [[ ${#selected_files[@]} -eq 0 ]]; then
79+
mapfile -t selected_files < <(printf '%s\n' "$snapshots_dir/${base_name}.received."*)
80+
selected_tfm=""
81+
fi
82+
83+
if [[ ${#selected_files[@]} -eq 0 ]]; then
84+
echo "No received files found for $base_name in $snapshots_dir" >&2
85+
exit 1
86+
fi
87+
88+
for received_path in "${selected_files[@]}"; do
89+
file_name="$(basename "$received_path")"
90+
if [[ -n "$selected_tfm" ]]; then
91+
verified_name="${file_name/.${selected_tfm}.received./.verified.}"
92+
else
93+
verified_name="${file_name/.received./.verified.}"
94+
fi
95+
verified_path="$snapshots_dir/$verified_name"
96+
mv -f "$received_path" "$verified_path"
97+
echo "Accepted: $verified_name"
98+
done
99+
100+
mapfile -t remaining_received < <(printf '%s\n' "$snapshots_dir/${base_name}".*.received.* "$snapshots_dir/${base_name}.received."*)
101+
for received_path in "${remaining_received[@]}"; do
102+
if [[ -f "$received_path" ]]; then
103+
rm -f "$received_path"
104+
echo "Removed: $(basename "$received_path")"
105+
fi
106+
done
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Accepts Verify snapshots for a single test.
5+
6+
.DESCRIPTION
7+
Promotes one TFM's received files to verified files and removes the other received
8+
files. When multiple TFMs exist, the highest TFM version is accepted.
9+
10+
.PARAMETER TestClassName
11+
Test class name used in the snapshot file name.
12+
13+
.PARAMETER TestMethodName
14+
Test method name used in the snapshot file name.
15+
16+
.EXAMPLE
17+
./accept-snapshot.ps1 AddAttributedDiTests GeneratesAddAttributedDiForEntryPoint
18+
#>
19+
[CmdletBinding()]
20+
param(
21+
[Parameter(Mandatory = $true, Position = 0)]
22+
[string]$TestClassName,
23+
24+
[Parameter(Mandatory = $true, Position = 1)]
25+
[string]$TestMethodName
26+
)
27+
28+
Set-StrictMode -Version Latest
29+
$ErrorActionPreference = 'Stop'
30+
31+
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
32+
$snapshotsDir = Join-Path $repoRoot 'test/AttributedDI.SourceGenerator.UnitTests/Snapshots'
33+
34+
if (-not (Test-Path -Path $snapshotsDir -PathType Container)) {
35+
throw "Snapshots directory not found: $snapshotsDir"
36+
}
37+
38+
$baseName = "$TestClassName.$TestMethodName"
39+
40+
function Get-ReceivedFilesForTfm([string]$tfm) {
41+
Get-ChildItem -Path $snapshotsDir -Filter "$baseName.$tfm.received.*" -File -ErrorAction SilentlyContinue
42+
}
43+
44+
$escapedBaseName = [Regex]::Escape($baseName)
45+
$tfmRegex = "^$escapedBaseName\.DotNet(?<major>\d+)_(?<minor>\d+)\.received\."
46+
47+
$receivedFiles = @(Get-ChildItem -Path $snapshotsDir -Filter "$baseName.*.received.*" -File -ErrorAction SilentlyContinue)
48+
$tfms = foreach ($file in $receivedFiles) {
49+
if ($file.Name -match $tfmRegex) {
50+
"DotNet$($Matches['major'])_$($Matches['minor'])"
51+
}
52+
}
53+
54+
$selectedFiles = @()
55+
$selectedTfm = ''
56+
57+
if ($tfms) {
58+
$selectedTfm = $tfms |
59+
Sort-Object {
60+
if ($_ -match '^DotNet(?<major>\d+)_(?<minor>\d+)$') {
61+
[int]$Matches['major'] * 1000 + [int]$Matches['minor']
62+
} else {
63+
-1
64+
}
65+
} -Descending |
66+
Select-Object -First 1
67+
68+
if ($selectedTfm) {
69+
$selectedFiles = @(Get-ReceivedFilesForTfm $selectedTfm)
70+
}
71+
}
72+
73+
if ($selectedFiles.Count -eq 0) {
74+
$selectedFiles = @(Get-ChildItem -Path $snapshotsDir -Filter "$baseName.received.*" -File -ErrorAction SilentlyContinue)
75+
$selectedTfm = ''
76+
}
77+
78+
if ($selectedFiles.Count -eq 0) {
79+
throw "No received files found for $baseName in $snapshotsDir"
80+
}
81+
82+
foreach ($receivedFile in $selectedFiles) {
83+
$fileName = $receivedFile.Name
84+
if ($selectedTfm) {
85+
$verifiedName = $fileName.Replace(".$selectedTfm.received.", '.verified.')
86+
} else {
87+
$verifiedName = $fileName.Replace('.received.', '.verified.')
88+
}
89+
90+
$verifiedPath = Join-Path $snapshotsDir $verifiedName
91+
Move-Item -Path $receivedFile.FullName -Destination $verifiedPath -Force
92+
Write-Output "Accepted: $verifiedName"
93+
}
94+
95+
$remainingReceived = @()
96+
$remainingReceived += Get-ChildItem -Path $snapshotsDir -Filter "$baseName.*.received.*" -File -ErrorAction SilentlyContinue
97+
$remainingReceived += Get-ChildItem -Path $snapshotsDir -Filter "$baseName.received.*" -File -ErrorAction SilentlyContinue
98+
99+
foreach ($receivedFile in $remainingReceived) {
100+
if (Test-Path -Path $receivedFile.FullName) {
101+
Remove-Item -Path $receivedFile.FullName -Force
102+
Write-Output "Removed: $($receivedFile.Name)"
103+
}
104+
}

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ csharp_style_conditional_delegate_call = true
107107

108108
# Modifier preferences
109109
csharp_prefer_static_local_function = true
110-
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
110+
csharp_preferred_modifier_order = public,private,protected,internal,static,partial,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
111111

112112
# Code-block preferences
113113
csharp_prefer_braces = true

0 commit comments

Comments
 (0)