> crap-analysis

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.

fetch
$curl "https://skillshub.wtf/Aaronontheweb/dotnet-skills/crap-analysis?format=md"
SKILL.mdcrap-analysis

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 ScoreRisk LevelAction Required
< 5LowWell-tested, maintainable code
5-30MediumAcceptable but watch complexity
> 30HighNeeds 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

MethodComplexityCoverageCalculationCRAP
GetUserId()10%1 x (1 - 0)^21
ParseToken()5452%54 x (1 - 0.52)^212.4
ValidateForm()200%20 x (1 - 0)^220
ProcessOrder()4520%45 x (1 - 0.20)^228.8
ImportData()8010%80 x (1 - 0.10)^264.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 version="1.0" encoding="utf-8" ?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat code coverage">
        <Configuration>
          <!-- OpenCover format includes cyclomatic complexity for CRAP scores -->
          <Format>cobertura,opencover</Format>

          <!-- Exclude test and benchmark assemblies -->
          <Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>

          <!-- Exclude generated code, obsolete members, and explicit exclusions -->
          <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>

          <!-- Exclude source-generated files, Blazor generated code, and migrations -->
          <ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>

          <!-- Exclude test projects -->
          <IncludeTestAssembly>false</IncludeTestAssembly>

          <!-- Optimization flags -->
          <SingleHit>false</SingleHit>
          <UseSourceLink>true</UseSourceLink>
          <SkipAutoProps>true</SkipAutoProps>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>

Key Configuration Options

OptionPurpose
FormatMust include opencover for complexity metrics
ExcludeExclude test/benchmark assemblies by pattern
ExcludeByAttributeSkip generated, obsolete, and explicitly excluded code (includes ExcludeFromCodeCoverageAttribute)
ExcludeByFileSkip source-generated files, Blazor components, and migrations
SkipAutoPropsDon'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

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-reportgenerator-globaltool": {
      "version": "5.4.5",
      "commands": ["reportgenerator"],
      "rollForward": false
    }
  }
}

Then restore:

dotnet tool restore

Or Install Globally

dotnet tool install --global dotnet-reportgenerator-globaltool

Collecting Coverage

Run Tests with Coverage Collection

# 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

dotnet reportgenerator \
  -reports:"TestResults/**/coverage.opencover.xml" \
  -targetdir:"coverage" \
  -reporttypes:"Html;TextSummary;MarkdownSummaryGithub"

Report Types

TypeDescriptionOutput
HtmlFull interactive reportcoverage/index.html
TextSummaryPlain text summarycoverage/Summary.txt
MarkdownSummaryGithubGitHub-compatible markdowncoverage/SummaryGithub.md
BadgesSVG badges for READMEcoverage/badge_*.svg
CoberturaMerged Cobertura XMLcoverage/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 TypeTargetAction
Line Coverage> 80%Good for most projects
Branch Coverage> 60%Catches conditional logic
CRAP Score< 30Maximum for new code

Configuring Thresholds

Create coverage.props in your repository:

<Project>
  <PropertyGroup>
    <!-- Coverage thresholds for CI enforcement -->
    <CoverageThresholdLine>80</CoverageThresholdLine>
    <CoverageThresholdBranch>60</CoverageThresholdBranch>
  </PropertyGroup>
</Project>

CI/CD Integration

GitHub Actions

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

- 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

# 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

MetricNew CodeLegacy Code
Line Coverage80%+60%+ (improve gradually)
Branch Coverage60%+40%+ (improve gradually)
Maximum CRAP30Document exceptions
High-risk methodsMust have testsAdd tests before modifying

What Gets Excluded

The recommended coverage.runsettings excludes:

PatternReason
[*.Tests]*Test assemblies aren't production code
[*.Benchmark]*Benchmark projects
[*.Migrations]*Database migrations (generated)
GeneratedCodeAttributeSource generators
CompilerGeneratedAttributeCompiler-generated code
ExcludeFromCodeCoverageAttributeExplicit developer opt-out
*.g.cs, *.designer.csGenerated files
*.razor.g.csBlazor component generated code
*.razor.css.g.csBlazor CSS isolation generated code
**/Migrations/**/*EF Core migrations (auto-generated)
SkipAutoPropsAuto-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

┌ stats

installs/wk0
░░░░░░░░░░
github stars628
██████████
first seenMar 17, 2026
└────────────

┌ repo

Aaronontheweb/dotnet-skills
by Aaronontheweb
└────────────

┌ tags

└────────────