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
50 changes: 50 additions & 0 deletions .agents/agents/test-author-writer.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: Test Author Writer
description: "Specialist for Purview.SourceGeneratorFramework test suites — writing, fixing, and modernising TUnit tests for generators, diagnostic analyzers, code fixes, and refactorings, and for adding stage-by-stage incremental cache tests."
tools:
[
"search/codebase",
"edit/editFiles",
"search",
"execute/getTerminalOutput",
"execute/runInTerminal",
"read/terminalLastCommand",
"read/terminalSelection",
"execute/createAndRunTask",
"execute/runTask",
"read/getTaskOutput",
"vscodeTasks/createAndRunTask",
"vscodeTasks/getTaskOutput",
"vscodeTasks/runTask",
]
---

You are a specialist for `Purview.SourceGeneratorFramework` test authoring.

## Primary objective

Produce correct, maintainable TUnit tests for source generators, diagnostic analyzers, code fix
providers, and refactoring providers, and prove incremental pipelines cache correctly.

## Background knowledge

Before writing or changing any test, load and apply the `source-generator-testing` skill (runner layer,
result types, `CodeQuery`, options, cache testing) and the `tunit-test-authoring` skill (base classes,
methods, assertion extensions, modernisation checklist). For source-generator emission work, also load the
`source-generator-codewriter-modernization` skill.

Key rules:

- Pick the base class by the Roslyn component type: generator → `TUnitSourceGeneratorTestBase` +
`GenerateAsync`; analyzer → `TUnitDiagnosticAnalyzerTestBase` + `AnalyzeAsync`; code fix →
`TUnitCodeFixTestBase` + `ApplyCodeFixAsync`/`ApplyFixAllAsync`; refactor →
`TUnitRefactoringTestBase` + `RefactorAsync`.
- Prefer `CodeQuery` (`result.Generated()` / `result.FixedCode()` with `Get/Has/TryGet`) over
raw-string assertions.
- Prefer the terminal assertion extensions (`HasGeneratedMethod`, `HasGeneratedClass`, …) that return
syntax nodes.
- Derive a `SourceGeneratorTestOptions` record that seeds namespaces and additional assemblies.
- For incremental pipelines, add a stage-by-stage cache test with `RunIncrementalAsync` /
`GenerateIncrementalAsync`, asserting `New` on first run and `Cached`/`Unchanged` on an identical rerun,
and `Modified` only on the stages whose inputs changed.
- Keep generated-output assertions deterministic (no timestamps); enable CodeWriter scope validation.
48 changes: 48 additions & 0 deletions .agents/prompts/modernize-test-to-codequery-tunit.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
agent: ask
description: "Modernise a Roslyn test suite to use CodeQuery + TUnit assertion extensions, and add a stage-by-stage incremental cache test."
---

You are modernising tests in this repository. Apply the guidance from the `source-generator-testing` and
`tunit-test-authoring` skills for picking the right base class, querying generated code with `CodeQuery`,
and asserting incremental caching.

## Inputs

- Target test file(s): `${input:targetFiles:Path(s) to test file(s)}`
- Roslyn component under test: `${input:componentType:generator|analyzer|codefix|refactor}` (inferred if blank)
- Generator/analyzer/code-fix/refactor type name: `${input:componentName:Component type name}`

## Task

Modernise each test so it uses the framework's `CodeQuery` syntax-lookup API and the TUnit assertion
extensions, and add a stage-by-stage cache test proving each incremental pipeline layer caches correctly.

### Requirements

1. Choose the correct base class and method for the component type:
- Generator → `TUnitSourceGeneratorTestBase<TGenerator>` → `GenerateAsync`.
- Analyzer → `TUnitDiagnosticAnalyzerTestBase<TAnalyzer>` → `AnalyzeAsync`.
- Code fix → `TUnitCodeFixTestBase<TAnalyzer, TCodeFix>` → `ApplyCodeFixAsync` / `ApplyFixAllAsync`.
- Refactor → `TUnitRefactoringTestBase<TRefactoring>` → `RefactorAsync`.
2. Replace `GetGeneratedTree(...)` + `string.Contains(...)` assertions with `CodeQuery`
(`result.Generated().Get/Has/TryGet…`) and the terminal assertion extensions
(`await Assert.That(result).HasGeneratedMethod/Class/Property/Field/SyntaxTree(…)`) that return the node.
3. Replace signature string checks with `TypeReference` parameter/return-type matching.
4. Ensure options come from a derived `SourceGeneratorTestOptions` record seeding the required namespaces
and additional assemblies; remove per-test duplication.
5. Add an incremental cache test using `RunIncrementalAsync` (or `GenerateIncrementalAsync` on the TUnit
base) with the four scenarios from the skills' "Incremental cache testing" sections
(`ServiceRegistrationCacheTests` / `IncrementalPipelineCacheTests` are the reference pattern):
- first run → every framework stage `New`;
- identical rerun (`RunIncrementalAsync(sources, …)` runs the same source twice) → framework stages
`Cached`/`Unchanged`;
- source-only change → `ForAttribute_*` `Modified`, property/config stages stay `Cached`;
- property-only change (`new IncrementalRunInput(sources, [("build_property.X", "value")])`) →
`GetMSBuildPropertyValue_*`/`GetGenerationConfiguration`/`GetGenerationContext_*` `Modified`,
`ForAttribute_*` stays `Cached`.
Use the `StepReasons(IncrementalCacheRun)` flattening helper; if the generator depends on its own
post-init output, assert on the framework-named stages rather than every tracked step.
6. Keep changes minimal and behavior equivalent; do not reformat unrelated tests.

Verify by building the test project and running its suite before finishing.
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Refactor the selected legacy emitter implementation from manual `string` / `Stri
### Requirements

1. Use structured declaration APIs where applicable:
- `WriteClass/WriteStruct/WriteRecordClass/WriteInterface/WriteEnum`
- `WriteMethod`, `WriteProperty`, `WriteField`, `WriteConstructor`
- `Class/Struct/RecordClass/Interface/Enum`
- `Method`, `Property`, `Field`, `Constructor`
2. Use XML helper extensions instead of raw `///` composition:
- `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` or `XmlCodeBlock`
3. Use `TypeReferenceOptions` when type text becomes complex (nullability, generics, arrays).
3. Use `TypeReference` when type text becomes complex (nullability, generics, arrays).
4. Ensure writer lifetime is output-scoped (`generationContext.CreateCodeWriter()` inside callback).
5. Preserve behavior, diagnostics, and generated names.
6. Keep changes minimal and focused; do not reformat unrelated logic.
Expand Down
8 changes: 8 additions & 0 deletions .agents/skills/source-generator-testing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Ignore all files
*

# Don't ignore directories, so Git can traverse them
!*/

# Keep this file
!.gitignore
8 changes: 8 additions & 0 deletions .agents/skills/tunit-test-authoring/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Ignore all files
*

# Don't ignore directories, so Git can traverse them
!*/

# Keep this file
!.gitignore
19 changes: 19 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# GitHub Copilot Instructions

## Primary instruction source

Use the repository root [`AGENTS.md`](../AGENTS.md) as the **primary** source of truth for behavior, architecture context, testing standards, and completion criteria.

If this file and `AGENTS.md` appear to conflict, prefer `AGENTS.md` unless this file explicitly states a GitHub Copilot-only exception.

## Copilot-specific guidance

This file should only contain **GitHub Copilot-specific** instruction details.
Keep product, architecture, and general engineering standards centralized in `AGENTS.md`.

## Operating expectations for Copilot

- Apply the `AGENTS.md` testing bar strictly (TUnit/TUnit.Mocks, AAA comments, naming, cancellation token rule).
- Treat work as incomplete until relevant tests pass.
- Consult the repository `.agents/` folder for additional skills/workflows that may improve execution quality.
- Keep edits minimal, focused, and aligned with existing SDK and repository conventions.
35 changes: 0 additions & 35 deletions .github/workflows/ci.yml

This file was deleted.

18 changes: 18 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: PR

on:
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build and test
uses: purview-dev/build/.github/workflows/purview-build.yml@main
with:
run-pack: true
validate-pack: true
secrets: inherit
45 changes: 0 additions & 45 deletions .github/workflows/publish.yml

This file was deleted.

18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Release

on:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Release packages
uses: purview-dev/build/.github/workflows/purview-release.yml@main
with:
release-mode: NuGet
release-branch: main
secrets: inherit
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,5 @@ sketch
!scripts/*

BenchmarkDotNet.Artifacts/

.tools/purview-build/**
9 changes: 3 additions & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<Project>
<PropertyGroup>
<!--
Set explicitly rather than relying on auto-detection otherwise
operations with RestorePackagesWithLockFile fail.
-->
<!-- Set explicitly rather than relying on auto-detection. -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<!--
Expand All @@ -14,12 +11,12 @@
<RoslynCompilerVersion>4.13.0</RoslynCompilerVersion>
<RoslynAnalyzersVersion>5.9.0</RoslynAnalyzersVersion>
<TUnitVersion>1.65.68</TUnitVersion>
<PurviewSourceGenFramework>1.0.0-prerelease.26</PurviewSourceGenFramework>
<PurviewSourceGenFramework>1.0.0-prerelease.35</PurviewSourceGenFramework>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Bogus" Version="35.6.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.400" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynCompilerVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="$(RoslynAnalyzersVersion)" />
<PackageVersion Include="TUnit" Version="$(TUnitVersion)" />
Expand Down
48 changes: 48 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,58 @@ build_configuration := "Release"
artifacts_folder := "./artifacts"
default_test_filter := "/*/*/*/*/"

pipeline_feed := "https://api.nuget.org/v3/index.json"
pipeline_tool := ".tools/purview-build/purview-build"

[private]
default:
just --list

# Install the shared Purview.Build tool (authenticated to the Purview-Dev feed) if not present
[private]
ensure-pipeline-tool:
if [ ! -x "{{ pipeline_tool }}" ]; then \
dotnet tool install Purview.Build --tool-path .tools/purview-build --add-source "{{ pipeline_feed }}"; \
fi

# Run the PR pipeline (restore, build, lint, tests)
[group('Pipeline')]
pipeline-pr *args:
just ensure-pipeline-tool
echo "Running PR pipeline..."
"{{ pipeline_tool }}" {{ args }}

# Run the build pipeline (restore, build, lint)
[group('Pipeline')]
pipeline-build *args:
just ensure-pipeline-tool
echo "Running build pipeline..."
"{{ pipeline_tool }}" --Build:RunTests=false --Release:Mode=None {{ args }}

# Run the release pipeline (restore, build, lint, tests, pack, publish, GitHub release)
[group('Pipeline')]
pipeline-release *args:
just ensure-pipeline-tool
echo "Running release pipeline..."
"{{ pipeline_tool }}" --Release:Mode=NuGet {{ args }}

# Run the release pipeline (restore, build, lint, tests, pack, local nuget publish)
# Note: `just` runs recipes through the shell, which strips backslashes from unquoted arguments.
# Use the LOCAL_NUGET_FEED_PATH environment variable or forward slashes, e.g.
# just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/
[group('Pipeline')]
pipeline-local-release *args:
just ensure-pipeline-tool
echo "Running local release pipeline..."
"{{ pipeline_tool }}" --Release:Mode=LocalNuGet {{ args }}

# Run the pipeline with tests enabled
[group('Pipeline')]
pipeline-tests *args:
just ensure-pipeline-tool
echo "Running tests pipeline..."
"{{ pipeline_tool }}" --Build:RunTests=true --Release:Mode=None {{ args }}

# Build and test with the specified configuration, defaulting to "Release"
build solutionOrProject=solution configuration=build_configuration:
echo "Building {{ BLUE }}{{ solutionOrProject }}{{ NORMAL }} with configuration {{ YELLOW }}{{ configuration }}{{ NORMAL }}"
Expand Down
19 changes: 3 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,24 +593,11 @@ var span = "user@example.com".AsSpan();
var result = schema.ValidateSpan(span);
```

## Dependency Reproducibility
## Dependency Management

This repository commits a `packages.lock.json` file for every project, so the full resolved dependency graph (direct and transitive) is reproducible. CI restores with `dotnet restore --locked-mode`, which fails the build if the committed lock files drift from the declared dependencies.
Package versions are declared centrally in `Directory.Packages.props`. No `packages.lock.json` files are committed; package resolution is left to NuGet at restore time.

### How it works

- Package versions are declared centrally in `Directory.Packages.props`. Note that a version like `13.0.4` is a *minimum* version requirement, not an exact pin, so the restore graph alone is not reproducible.
- The lock files pin the actually *resolved* version and content hash of every package for each target framework, which is what makes builds reproducible without forcing exact versions onto consumers.
- When package versions change (for example via Dependabot or manual edits to `Directory.Packages.props` / `Directory.Build.targets`), regenerate the lock files and commit them **in the same change**:
```bash
dotnet restore src/ZodSharp.slnx --force-evaluate
```
Committing lock files without the version change, or vice versa, will fail CI.

### Known risks

- **Cross-platform lock files** - Lock files are generated on the developer's OS, but CI runs on Linux (atm). The current package set is OS-agnostic (no runtime identifiers involved), so resolution should be identical. If locked-mode restore ever fails on Linux, regenerate the lock files from a Linux/WSL environment and recommit them.
- **Lock file drift** - Any dependency change requires the lock files to be regenerated and committed together. Dependabot and package-update PRs must therefore include the lock-file updates; CI's locked-mode restore enforces this.
> Note: a version like `13.0.4` in `Directory.Packages.props` is a *minimum* version requirement, not an exact pin, so the resolved graph can drift as newer packages are published.

## License

Expand Down
Loading