Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/official-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ jobs:
run: dotnet test --verbosity normal
working-directory: src/ReferenceCop.MSBuild.Tests

- name: Pack ReferenceCop for playground
run: dotnet pack -c Debug --nologo
working-directory: src/ReferenceCop.Package

- name: Run end-to-end tests
run: ./playground/Run-E2ETests.ps1
shell: pwsh

- name: Set NuGet Package Version Environment Variable from tag (only for tagged builds)
if: ${{ github.ref_type == 'tag' }}
run: echo "PKGVERSION=$('${{ github.ref_name }}'.replace('v',''))" >> $env:GITHUB_ENV
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/pr-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ jobs:

- name: Run Tests in ReferenceCop.MSBuild.Tests
run: dotnet test --verbosity normal
working-directory: src/ReferenceCop.MSBuild.Tests
working-directory: src/ReferenceCop.MSBuild.Tests

- name: Pack ReferenceCop for playground
run: dotnet pack -c Release --nologo -p:IsDevEnvironment=true
working-directory: src/ReferenceCop.Package

- name: Run end-to-end tests
run: ./playground/Run-E2ETests.ps1
shell: pwsh
222 changes: 222 additions & 0 deletions playground/Run-E2ETests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<#
.SYNOPSIS
Automated end-to-end test runner for ReferenceCop playground scenarios.

.DESCRIPTION
Builds various test configurations in the playground and validates that
ReferenceCop correctly detects violations and allows valid references.

.PARAMETER PackFirst
If set, builds and packs the ReferenceCop package before running tests.
#>
param(
[switch]$PackFirst
)

$ErrorActionPreference = "Stop"
$script:TestsPassed = 0
$script:TestsFailed = 0
$script:Failures = @()

$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path
$PlaygroundRoot = "$PSScriptRoot/TestProject"

function Write-TestHeader($name) {
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " TEST: $name" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
}

function Assert-BuildFails($project, $testName, $expectedDiagnostic) {
Write-TestHeader $testName
$output = dotnet build "$PlaygroundRoot/$project" --no-restore 2>&1 | Out-String
$exitCode = $LASTEXITCODE

if ($exitCode -eq 0) {
Write-Host " FAIL: Build succeeded but was expected to fail" -ForegroundColor Red
Write-Host " Output: $output" -ForegroundColor Gray
$script:TestsFailed++
$script:Failures += $testName
return
}

if ($expectedDiagnostic -and ($output -notmatch $expectedDiagnostic)) {
Write-Host " FAIL: Build failed but diagnostic '$expectedDiagnostic' not found in output" -ForegroundColor Red
Write-Host " Output: $output" -ForegroundColor Gray
$script:TestsFailed++
$script:Failures += $testName
return
}

Write-Host " PASS: Build failed as expected with diagnostic '$expectedDiagnostic'" -ForegroundColor Green
$script:TestsPassed++
}

function Assert-BuildSucceeds($project, $testName) {
Write-TestHeader $testName
$output = dotnet build "$PlaygroundRoot/$project" --no-restore 2>&1 | Out-String
$exitCode = $LASTEXITCODE

if ($exitCode -ne 0) {
Write-Host " FAIL: Build failed but was expected to succeed" -ForegroundColor Red
Write-Host " Output: $output" -ForegroundColor Gray
$script:TestsFailed++
$script:Failures += $testName
return
}

Write-Host " PASS: Build succeeded as expected" -ForegroundColor Green
$script:TestsPassed++
}

# --- Setup ---

if ($PackFirst) {
Write-Host "`nPacking ReferenceCop..." -ForegroundColor Yellow
Push-Location "$RepoRoot/src/ReferenceCop.Package"
dotnet pack -c Debug --nologo -v quiet
if ($LASTEXITCODE -ne 0) {
Write-Host "FATAL: Failed to pack ReferenceCop" -ForegroundColor Red
exit 1
}
Pop-Location
}

# Restore all playground projects
Write-Host "`nRestoring playground projects..." -ForegroundColor Yellow
dotnet restore "$PlaygroundRoot/SampleApp/SampleApp.csproj" --nologo -v quiet
dotnet restore "$PlaygroundRoot/ValidApp/ValidApp.csproj" --nologo -v quiet

# --- Test Scenarios ---

# Scenario 1: AssemblyName rule - SampleLibrary references Newtonsoft.Json which is blocked
Assert-BuildFails "SampleLibrary" `
"AssemblyName rule: Newtonsoft.Json blocked" `
"RC000[12]"

# Scenario 2: ProjectPath rule - SampleApp referencing InternalLib should be blocked
# We need to temporarily add a project reference for this test
$sampleAppCsproj = "$PlaygroundRoot/SampleApp/SampleApp.csproj"
$originalContent = Get-Content $sampleAppCsproj -Raw

# Add InternalLib reference
$modifiedContent = $originalContent -replace '(</ItemGroup>\s*</Project>)', @"
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\InternalLib\InternalLib.csproj" />
</ItemGroup>

</Project>
"@
# Fix: replace last </Project> properly
$modifiedContent = $originalContent -replace '(<ItemGroup>\s*<ProjectReference Include="\.\.\\SampleLibrary)', @"
<ItemGroup>
<ProjectReference Include="..\InternalLib\InternalLib.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SampleLibrary"
"@

# Simpler approach: just write the test project with InternalLib reference
$testCsproj = @"
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProjectTag>App</ProjectTag>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="LaunchDebugger" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InternalLib\InternalLib.csproj" />
</ItemGroup>
</Project>
"@

# Save originals, write modified project and minimal program
$originalSampleApp = Get-Content $sampleAppCsproj -Raw
$sampleAppProgram = "$PlaygroundRoot/SampleApp/Program.cs"
$originalProgram = Get-Content $sampleAppProgram -Raw
$minimalProgram = "namespace SampleApp;`npublic class Program { public static void Main() { } }"

Set-Content $sampleAppCsproj -Value $testCsproj -NoNewline
Set-Content $sampleAppProgram -Value $minimalProgram -NoNewline
dotnet restore "$sampleAppCsproj" --nologo -v quiet 2>$null

Assert-BuildFails "SampleApp" `
"ProjectPath rule: App cannot reference Internal" `
"RC000[12]"

# Restore originals
Set-Content $sampleAppCsproj -Value $originalSampleApp -NoNewline
Set-Content $sampleAppProgram -Value $originalProgram -NoNewline
dotnet restore "$sampleAppCsproj" --nologo -v quiet 2>$null

# Scenario 3: ProjectTag rule - App referencing Tools project should warn
$toolsTestCsproj = @"
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProjectTag>App</ProjectTag>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="LaunchDebugger" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ToolsProject\ToolsProject.csproj" />
</ItemGroup>
</Project>
"@

Set-Content $sampleAppCsproj -Value $toolsTestCsproj -NoNewline
Set-Content $sampleAppProgram -Value $minimalProgram -NoNewline
dotnet restore "$sampleAppCsproj" --nologo -v quiet 2>$null

# ProjectTag rule has Severity=Warning, so build may succeed but with warnings
# We check for the diagnostic in output regardless of exit code
Write-TestHeader "ProjectTag rule: App cannot reference Tools (warning)"
$output = dotnet build "$PlaygroundRoot/SampleApp" --no-restore 2>&1 | Out-String
if ($output -match "RC000[12]") {
Write-Host " PASS: ProjectTag violation diagnostic detected" -ForegroundColor Green
$script:TestsPassed++
} else {
Write-Host " FAIL: Expected ProjectTag warning diagnostic not found" -ForegroundColor Red
Write-Host " Output: $output" -ForegroundColor Gray
$script:TestsFailed++
$script:Failures += "ProjectTag rule: App cannot reference Tools (warning)"
}

# Restore originals
Set-Content $sampleAppCsproj -Value $originalSampleApp -NoNewline
Set-Content $sampleAppProgram -Value $originalProgram -NoNewline
dotnet restore "$sampleAppCsproj" --nologo -v quiet 2>$null

# Scenario 4: Valid project builds successfully (no violations)
Assert-BuildSucceeds "ValidApp" `
"Valid project: No violations, build succeeds"

# --- Summary ---
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " RESULTS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Passed: $script:TestsPassed" -ForegroundColor Green
Write-Host " Failed: $script:TestsFailed" -ForegroundColor $(if ($script:TestsFailed -gt 0) { "Red" } else { "Green" })

if ($script:TestsFailed -gt 0) {
Write-Host "`n Failed tests:" -ForegroundColor Red
foreach ($f in $script:Failures) {
Write-Host " - $f" -ForegroundColor Red
}
exit 1
}

Write-Host "`n All e2e tests passed!" -ForegroundColor Green
exit 0
27 changes: 27 additions & 0 deletions playground/TEST-SCENARIOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# E2E Test Scenarios

This document describes the automated end-to-end test scenarios run by `Run-E2ETests.ps1`.

## Scenarios

| # | Rule Type | Scenario | Expected Result |
|---|-----------|----------|-----------------|
| 1 | AssemblyName | SampleLibrary references Newtonsoft.Json (blocked by `NoNewtonsoftJson` rule) | Build fails with RC0001/RC0002 |
| 2 | ProjectPath | SampleApp references InternalLib (blocked by `AppCannotReferenceInternal` rule) | Build fails with RC0001/RC0002 |
| 3 | ProjectTag | SampleApp (tag=App) references ToolsProject (tag=Tools), blocked by `AppCannotReferenceTools` rule | Warning diagnostic RC0001/RC0002 emitted |
| 4 | Valid | ValidApp with no violations | Build succeeds cleanly |

## Running Locally

```powershell
# Pack first, then run tests
./playground/Run-E2ETests.ps1 -PackFirst

# Or if already packed:
./playground/Run-E2ETests.ps1
```

## CI Integration

The tests run automatically in both PR and official build workflows after the unit tests pass.
The workflow packs the package in Debug mode, then invokes the test runner.
6 changes: 6 additions & 0 deletions playground/TestProject/InternalLib/InternalHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace InternalLib;

public class InternalHelper
{
public static string GetSecret() => "internal";
}
10 changes: 10 additions & 0 deletions playground/TestProject/InternalLib/InternalLib.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProjectTag>Library</ProjectTag>
</PropertyGroup>

</Project>
14 changes: 3 additions & 11 deletions playground/TestProject/ReferenceCop.config
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
<UseExperimentalDetectors>true</UseExperimentalDetectors>
<EnableDebugMessages>true</EnableDebugMessages>
<Rules>
<!-- Example 1: Block references to assemblies matching a pattern -->
<AssemblyName>
<Name>NoSystemWebReferences</Name>
<Description>System.Web assemblies should not be referenced</Description>
<Severity>Error</Severity>
<Pattern>System.Web*</Pattern>
</AssemblyName>

<!-- Example 2: Block references between projects with specific tags -->
<!-- Example 1: Block references between projects with specific tags -->
<ProjectTag>
<Name>AppCannotReferenceTools</Name>
<Description>Application projects should not reference tool projects</Description>
Expand All @@ -26,14 +18,14 @@
<Description>Applications should not reference internal implementation libraries</Description>
<Severity>Error</Severity>
<FromPath>**/SampleApp/**</FromPath>
<ToPath>**/Internal/**</ToPath>
<ToPath>**/InternalLib/**</ToPath>
</ProjectPath>

<!-- Example 4: Another AssemblyName rule for testing -->
<AssemblyName>
<Name>NoNewtonsoftJson</Name>
<Description>Use System.Text.Json instead of Newtonsoft.Json</Description>
<Severity>Warning</Severity>
<Severity>Error</Severity>
<Pattern>Newtonsoft.Json*</Pattern>
</AssemblyName>
</Rules>
Expand Down
6 changes: 6 additions & 0 deletions playground/TestProject/ToolsProject/Tool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ToolsProject;

public class Tool
{
public static void Run() { }
}
10 changes: 10 additions & 0 deletions playground/TestProject/ToolsProject/ToolsProject.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProjectTag>Tools</ProjectTag>
</PropertyGroup>

</Project>
1 change: 1 addition & 0 deletions playground/TestProject/ValidApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Console.WriteLine("Hello from ValidApp!");
15 changes: 15 additions & 0 deletions playground/TestProject/ValidApp/ValidApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ProjectTag>App</ProjectTag>
</PropertyGroup>

<ItemGroup>
<CompilerVisibleProperty Include="LaunchDebugger" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion playground/nuget.config
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<packageSources>
<clear />
<!-- Direct reference to ReferenceCop package build output -->
<add key="local-build" value="../src/ReferenceCop.Package/bin/Debug" />
<add key="local-build" value="../src/ReferenceCop.Package/bin/Release" />
<!-- NuGet.org for other dependencies -->
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
Expand Down
3 changes: 3 additions & 0 deletions playground/playground.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
</Folder>
<Project Path="TestProject/SampleApp/SampleApp.csproj" />
<Project Path="TestProject/SampleLibrary/SampleLibrary.csproj" />
<Project Path="TestProject/InternalLib/InternalLib.csproj" />
<Project Path="TestProject/ToolsProject/ToolsProject.csproj" />
<Project Path="TestProject/ValidApp/ValidApp.csproj" />
</Solution>
Loading