--- name: crap-analysis description: Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths. invocable: true --- # CRAP Score Analysis ## When to Use This Skill Use this skill when: - Evaluating code quality and test coverage before changes - Identifying high-risk code that needs refactoring or testing - Setting up coverage collection for a .NET project - Prioritizing which code to test based on risk - Establishing coverage thresholds for CI/CD pipelines --- ## What is CRAP? **CRAP Score = Complexity x (1 - Coverage)^2** The CRAP (Change Risk Anti-Patterns) score combines cyclomatic complexity with test coverage to identify risky code. | CRAP Score | Risk Level | Action Required | |------------|------------|-----------------| | **< 5** | Low | Well-tested, maintainable code | | **5-30** | Medium | Acceptable but watch complexity | | **> 30** | High | Needs tests or refactoring | ### Why CRAP Matters - **High complexity + low coverage = danger**: Code that's hard to understand AND untested is risky to modify - **Complexity alone isn't enough**: A complex method with 100% coverage is safer than a simple method with 0% - **Focuses effort**: Prioritize testing on complex code, not simple getters/setters ### CRAP Score Examples | Method | Complexity | Coverage | Calculation | CRAP | |--------|------------|----------|-------------|------| | `GetUserId()` | 1 | 0% | 1 x (1 - 0)^2 | **1** | | `ParseToken()` | 54 | 52% | 54 x (1 - 0.52)^2 | **12.4** | | `ValidateForm()` | 20 | 0% | 20 x (1 - 0)^2 | **20** | | `ProcessOrder()` | 45 | 20% | 45 x (1 - 0.20)^2 | **28.8** | | `ImportData()` | 80 | 10% | 80 x (1 - 0.10)^2 | **64.8** | --- ## Coverage Collection Setup ### coverage.runsettings Create a `coverage.runsettings` file in your repository root. The **OpenCover format is required** for CRAP score calculation because it includes cyclomatic complexity metrics. ```xml cobertura,opencover [*.Tests]*,[*.Benchmark]*,[*.Migrations]* Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute **/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/* false false true true ``` ### Key Configuration Options | Option | Purpose | |--------|---------| | `Format` | Must include `opencover` for complexity metrics | | `Exclude` | Exclude test/benchmark assemblies by pattern | | `ExcludeByAttribute` | Skip generated, obsolete, and explicitly excluded code (includes `ExcludeFromCodeCoverageAttribute`) | | `ExcludeByFile` | Skip source-generated files, Blazor components, and migrations | | `SkipAutoProps` | Don't count auto-properties as branches | --- ## ReportGenerator Installation Install ReportGenerator as a local tool for generating HTML reports with Risk Hotspots. ### Add to .config/dotnet-tools.json ```json { "version": 1, "isRoot": true, "tools": { "dotnet-reportgenerator-globaltool": { "version": "5.4.5", "commands": ["reportgenerator"], "rollForward": false } } } ``` Then restore: ```bash dotnet tool restore ``` ### Or Install Globally ```bash dotnet tool install --global dotnet-reportgenerator-globaltool ``` --- ## Collecting Coverage ### Run Tests with Coverage Collection ```bash # Clean previous results rm -rf coverage/ TestResults/ # Run unit tests with coverage dotnet test tests/MyApp.Tests.Unit \ --settings coverage.runsettings \ --collect:"XPlat Code Coverage" \ --results-directory ./TestResults # Run integration tests (optional, adds to coverage) dotnet test tests/MyApp.Tests.Integration \ --settings coverage.runsettings \ --collect:"XPlat Code Coverage" \ --results-directory ./TestResults ``` ### Generate HTML Report ```bash dotnet reportgenerator \ -reports:"TestResults/**/coverage.opencover.xml" \ -targetdir:"coverage" \ -reporttypes:"Html;TextSummary;MarkdownSummaryGithub" ``` ### Report Types | Type | Description | Output | |------|-------------|--------| | `Html` | Full interactive report | `coverage/index.html` | | `TextSummary` | Plain text summary | `coverage/Summary.txt` | | `MarkdownSummaryGithub` | GitHub-compatible markdown | `coverage/SummaryGithub.md` | | `Badges` | SVG badges for README | `coverage/badge_*.svg` | | `Cobertura` | Merged Cobertura XML | `coverage/Cobertura.xml` | --- ## Reading the Report ### Risk Hotspots Section The HTML report includes a **Risk Hotspots** section showing methods sorted by complexity: - **Cyclomatic Complexity**: Number of independent paths through code (if/else, switch cases, loops) - **NPath Complexity**: Number of acyclic execution paths (exponential growth with nesting) - **Crap Score**: Calculated from complexity and coverage ### Interpreting Results ``` Risk Hotspots ───────────── Method Complexity Coverage Crap Score ────────────────────────────────────────────────────────────────── DataImporter.ParseRecord() 54 52% 12.4 AuthService.ValidateToken() 32 0% 32.0 ← HIGH RISK OrderProcessor.Calculate() 28 85% 1.3 UserService.CreateUser() 15 100% 0.0 ``` **Action items:** - `ValidateToken()` has CRAP > 30 with 0% coverage - **test immediately or refactor** - `ParseRecord()` is complex but has decent coverage - acceptable - `CreateUser()` and `Calculate()` are well-tested - safe to modify --- ## Coverage Thresholds ### Recommended Standards | Coverage Type | Target | Action | |---------------|--------|--------| | Line Coverage | > 80% | Good for most projects | | Branch Coverage | > 60% | Catches conditional logic | | CRAP Score | < 30 | Maximum for new code | ### Configuring Thresholds Create `coverage.props` in your repository: ```xml 80 60 ``` --- ## CI/CD Integration ### GitHub Actions ```yaml name: Coverage on: pull_request: branches: [main, dev] jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - name: Restore tools run: dotnet tool restore - name: Run tests with coverage run: | dotnet test \ --settings coverage.runsettings \ --collect:"XPlat Code Coverage" \ --results-directory ./TestResults - name: Generate report run: | dotnet reportgenerator \ -reports:"TestResults/**/coverage.opencover.xml" \ -targetdir:"coverage" \ -reporttypes:"Html;MarkdownSummaryGithub;Cobertura" - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/ - name: Add coverage to PR uses: marocchino/sticky-pull-request-comment@v2 with: path: coverage/SummaryGithub.md ``` ### Azure Pipelines ```yaml - task: DotNetCoreCLI@2 displayName: 'Run tests with coverage' inputs: command: 'test' arguments: '--settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory $(Build.SourcesDirectory)/TestResults' - task: DotNetCoreCLI@2 displayName: 'Generate coverage report' inputs: command: 'custom' custom: 'reportgenerator' arguments: '-reports:"$(Build.SourcesDirectory)/TestResults/**/coverage.opencover.xml" -targetdir:"$(Build.SourcesDirectory)/coverage" -reporttypes:"HtmlInline_AzurePipelines;Cobertura"' - task: PublishCodeCoverageResults@2 displayName: 'Publish coverage' inputs: codeCoverageTool: 'Cobertura' summaryFileLocation: '$(Build.SourcesDirectory)/coverage/Cobertura.xml' ``` --- ## Quick Reference ### One-Liner Commands ```bash # Full analysis workflow rm -rf coverage/ TestResults/ && \ dotnet test --settings coverage.runsettings \ --collect:"XPlat Code Coverage" \ --results-directory ./TestResults && \ dotnet reportgenerator \ -reports:"TestResults/**/coverage.opencover.xml" \ -targetdir:"coverage" \ -reporttypes:"Html;TextSummary" # View summary cat coverage/Summary.txt # Open HTML report (Linux) xdg-open coverage/index.html # Open HTML report (macOS) open coverage/index.html # Open HTML report (Windows) start coverage/index.html ``` ### Project Standards | Metric | New Code | Legacy Code | |--------|----------|-------------| | Line Coverage | 80%+ | 60%+ (improve gradually) | | Branch Coverage | 60%+ | 40%+ (improve gradually) | | Maximum CRAP | 30 | Document exceptions | | High-risk methods | Must have tests | Add tests before modifying | --- ## What Gets Excluded The recommended `coverage.runsettings` excludes: | Pattern | Reason | |---------|--------| | `[*.Tests]*` | Test assemblies aren't production code | | `[*.Benchmark]*` | Benchmark projects | | `[*.Migrations]*` | Database migrations (generated) | | `GeneratedCodeAttribute` | Source generators | | `CompilerGeneratedAttribute` | Compiler-generated code | | `ExcludeFromCodeCoverageAttribute` | Explicit developer opt-out | | `*.g.cs`, `*.designer.cs` | Generated files | | `*.razor.g.cs` | Blazor component generated code | | `*.razor.css.g.cs` | Blazor CSS isolation generated code | | `**/Migrations/**/*` | EF Core migrations (auto-generated) | | `SkipAutoProps` | Auto-properties (trivial branches) | --- ## When to Update Thresholds **Lower thresholds temporarily for:** - Legacy codebases being modernized (document in README) - Generated code that can't be modified - Third-party wrapper code **Never lower thresholds for:** - "It's too hard to test" - refactor instead - "We'll add tests later" - add them now - New features - should meet standards from the start --- ## Additional Resources - **Coverlet Documentation**: https://github.com/coverlet-coverage/coverlet - **ReportGenerator**: https://github.com/danielpalme/ReportGenerator - **CRAP Score Original Paper**: http://www.artima.com/weblogs/viewpost.jsp?thread=215899