codecharter coverage [root] [optionen]
codecharter coverage sucht die Testprojekte unterhalb von root
(standardmäßig das aktuelle Verzeichnis), führt sie mit Coverage-Messung aus,
führt die Ergebnisse zusammen und lässt den Build fehlschlagen, sobald die
Line-Coverage unter den geforderten Mindestwert fällt. Liegt genau eine
Solution-Datei im Root, wird sie einmal vorab gebaut; die Testprojekte laufen
danach ohne erneuten Build und mehrere gleichzeitig. Derselbe Lauf meldet
zusätzlich jede ungedeckte Region mit Quelltext-Ausschnitt, sodass du genau
siehst, welchen Zeilen ein Test fehlt.
Damit ist die zweite Hälfte des Quality Gates abgedeckt:
analyze beantwortet "ist der Code sauber
geschrieben", coverage beantwortet "ist er tatsächlich getestet".
Voraussetzung
Die Messung läuft über den Collector XPlat Code Coverage, der das Paket
coverlet.collector in jedem Testprojekt benötigt:
<PackageReference Include="coverlet.collector" Version="6.*" PrivateAssets="all" />
Ein Testprojekt ohne dieses Paket führt seine Tests zwar aus, liefert aber keine Coverage-Daten. Der Lauf benennt jedes betroffene Projekt und gibt die Zeile aus, die du ergänzen musst.
Schnellstart
# Repository gegen den konfigurierten Mindestwert prüfen
codecharter coverage
# Einen Ordner gegen 90 % prüfen
codecharter coverage src --min-coverage 90
# Ergebnisse eines bereits gelaufenen Testlaufs auswerten
codecharter coverage --skip-tests
# Report in eine Datei statt nach stdout schreiben
codecharter coverage --output-file coverage-report.json
Während der Lauf arbeitet, schreibt er seinen Fortschritt nach stderr, sodass
auch ein langer Lauf nie stumm ist: die Anzahl gefundener Testprojekte, den
gemeinsamen Build mit Dauer und jedes Testprojekt beim Start und beim Ende mit
Dauer und Ergebnis. Der JSON-Report geht nach stdout, Diagnosen und eine
einzeilige Zusammenfassung nach stderr. Durch diese Trennung bleibt codecharter coverage > report.json
in einer Pipeline direkt verwendbar, ohne dass du etwas herausfiltern musst.
Optionen
| Option | Standard | Beschreibung |
|---|---|---|
[root] |
aktuelles Verzeichnis | Verzeichnisbaum, der nach Testprojekten durchsucht wird. |
--min-coverage <prozent> |
aus der Konfiguration | Geforderte Mindest-Line-Coverage (0 bis 100). Überschreibt den konfigurierten Wert für diesen Lauf. Die Zahl wird gebietsschema-unabhängig gelesen, 99.5 ist also überall korrekt, 99,5 nicht. |
--git-ref <bereich> |
aus | Statt des gesamten Repositories die Zeilen prüfen, die dieser Git-Bereich geändert hat. Siehe Diff-Coverage. |
--affected-by <bereich> |
aus | Statt aller gefundenen Testprojekte nur die Testprojekte ausführen, die dieser Git-Bereich betrifft. Setzt denselben Bereich wie --git-ref voraus. Siehe Testauswahl eingrenzen mit --affected-by. |
--min-diff-coverage <prozent> |
der wirksame --min-coverage-Wert |
Geforderte Mindest-Coverage der geänderten Zeilen (0 bis 100). Setzt --git-ref voraus. Wird gebietsschema-unabhängig gelesen wie --min-coverage. |
--results-root <verzeichnis> |
<root>/TestResults/coverage |
Ablage für Test- und Coverage-Artefakte. Ein relativer Pfad wird gegen das aktuelle Arbeitsverzeichnis aufgelöst, nicht gegen root. |
--skip-tests |
aus | Tests nicht ausführen, sondern die bereits vorhandenen Coverage-Dateien unterhalb des Results-Root auswerten. |
--max-parallel <n> |
Prozessoranzahl, gedeckelt bei 4 | Wie viele Testprojekte nach dem gemeinsamen Build gleichzeitig laufen (1 oder mehr). Überschreibt den konfigurierten Wert für diesen Lauf. Mit 1 laufen sie nacheinander, was einen echten Fehlschlag von zwei Projekten unterscheidbar macht, die sich um denselben Port, dieselbe Pipe oder dieselbe Datei streiten. |
--output-file <pfad> |
stdout | JSON-Report in diese Datei schreiben. |
--test-timeout <sekunden> |
1800 (30 Minuten) |
Wie lange ein einzelnes Testprojekt laufen darf. Läuft die Zeit ab, werden der Testprozess und alles, was er gestartet hat, beendet. Das Projekt wird mit genau diesem Grund als fehlgeschlagen berichtet, der Lauf macht mit den übrigen Projekten weiter. Die Grenze gilt je Projekt, nicht für den ganzen Lauf. |
--findings |
aus | Jede ungedeckte Region zusätzlich als Finding ausgeben, gerendert über dieselben Formatter wie bei analyze. |
--findings-format <name> |
console |
Format für --findings: console, json, sarif oder github-annotations. |
--findings-file <pfad> |
stderr | Ausgabe von --findings in diese Datei schreiben. |
--verbose |
aus | Zusätzlich informative Fortschrittszeilen nach stderr ausgeben. |
--no-color |
aus | ANSI-Farbcodes abschalten. |
--test-timeout gibt es nur auf der Kommandozeile und nicht als
Konfigurationsschlüssel: Der passende Wert richtet sich nach der Maschine und
nach dem Zeitlimit des CI-Jobs, nicht nach dem Repository. Ein abgelaufenes
Projekt wird nie wiederholt, denn ein Hänger wiederholt sich und ein zweiter
Versuch würde nur dieselbe Zeit noch einmal verbrauchen.
Solange ein Lauf arbeitet, gehört ihm sein Results-Root. Ein zweiter Lauf auf
demselben Results-Root wartet auf den ersten und sagt das auch auf stderr, denn
sonst würden beide dieselben Verzeichnisse je Projekt leeren und neu schreiben
und am Ende beide eine Mischung auswerten. Das Warten ist auf zehn Minuten
begrenzt. Danach bricht der Lauf mit Exit-Code 64 ab, statt eine Zahl zu
liefern, der niemand trauen kann. Eine Sperre, die ein abgebrochener Lauf
zurückgelassen hat, wird erkannt und entfernt, ein abgebrochener CI-Job
blockiert den nächsten also nicht.
--skip-tests prüft vor der Auswertung, ob zu jedem gefundenen Testprojekt
eine Coverage-Datei vorliegt. Ein veralteter oder abgebrochener Lauf kann das
Gate so nicht unbemerkt passieren. Stammt das Ergebnisverzeichnis von einem
anderen Werkzeug und trägt nicht die Ablage je Projekt, fällt die Prüfung auf
einen Abgleich der Anzahlen zurück und weist darauf hin.
Konfiguration
Die Policy steht in .codecharter/config.yml, wird also einmal eingecheckt und
gilt für alle:
coverage:
minimum-percent: 99.5 # Schwelle des Gates (0 bis 100), Standard 100
snippet-context-lines: 3 # Kontextzeilen um jede ungedeckte Region
max-parallel-test-projects: 4 # gleichzeitig laufende Testprojekte, Standard: Kerne, gedeckelt bei 4
exclude: # von der Messung ausgenommene Dateien (Globs)
- "**/*.Designer.cs"
- "src/Generated/**"
excluded-directories: # bei der Suche übersprungene Verzeichnisbäume (Globs)
- "vendor/**"
Alle fünf Schlüssel gelten repositoryweit. Anders als die Analyse-Sektionen
lässt sich coverage nicht pro Pfad-Scope variieren. Globs werden
groß-/kleinschreibungsgenau gegen Pfade relativ zum Lauf-Root abgeglichen,
geschrieben mit Schrägstrichen (**/ überspannt Verzeichnisse, * bleibt
innerhalb eines Pfadsegments).
exclude und excluded-directories
Die beiden Listen sehen sich ähnlich, tun aber sehr Verschiedenes:
excludewirkt zum Zeitpunkt der Messung. Die Tests laufen weiterhin, die passenden Quelldateien zählen nur nicht in die Prozentzahl und erzeugen keine ungedeckten Regionen. Gedacht für generierten Code, der sich nicht sinnvoll testen lässt.excluded-directorieswirkt zum Zeitpunkt der Suche. Ein passender Verzeichnisbaum wird gar nicht erst betreten, ein Testprojekt darin also nie gefunden und nie ausgeführt. Gedacht für einen mitgelieferten Fremdbaum, dessen Tests dein Repository nichts angehen.
Weil die Suche den ganzen Baum abschneidet, sobald das Verzeichnis selbst
passt, bedeuten vendor, vendor/ und vendor/** dasselbe. Mit
**/third-party triffst du ein so benanntes Verzeichnis in jeder Tiefe. Die
Verzeichnisnamen bin, obj, node_modules und .git werden immer
übersprungen und brauchen keinen Eintrag.
Beachte, dass coverage.exclude unabhängig vom übergeordneten exclude
der Analyse ist. Ein von der Regelanalyse ausgenommener Ordner verschwindet
also nicht stillschweigend aus deiner Coverage-Zahl, und umgekehrt.
Die wirksame Schwelle wird in dieser Reihenfolge bestimmt:
--min-coverageauf der Kommandozeile.codecharter/config.local.yml(dein persönliches, maschinenlokales Overlay).codecharter/config.yml(die eingecheckte Team-Einstellung)- der eingebaute Standardwert 100
Der Report nennt, welche der vier Quellen gewonnen hat, sodass sich ein
überraschendes Gate-Ergebnis immer zurückverfolgen lässt.
max-parallel-test-projects folgt derselben Reihenfolge, mit --max-parallel
an erster Stelle, führt aber keine Herkunft mit: nur die Schwelle nennt ihre
Quelle. Eine coverage.exclude- oder coverage.excluded-directories-Liste im
lokalen Overlay ersetzt dabei die eingecheckte Liste, sie ergänzt sie
nicht.
All das lässt sich auch von der Kommandozeile aus pflegen statt von Hand, siehe codecharter config.
Was in die Zahl einfließt
Positional Records, die nur Daten tragen, enthalten kein Verhalten, das sich zu
testen lohnt, und bleiben deshalb außerhalb der Messung. Ein Record gilt so
lange als reiner Datenträger, wie kein Mitglied einen selbst geschriebenen
Rumpf hat: Auto-Properties (auch mit init) und der generierte
Primärkonstruktor erhalten diesen Status, während eine berechnete Property, ein
prüfender Konstruktor, ein Methodenrumpf, ein Operator, ein Indexer oder ein
Event den gesamten Record wieder in das Gate holen.
Klassen und Structs werden auf diesem Weg nie ausgenommen. Die Ausnahme ist bewusst auf Records beschränkt, weil nur ein Record den Vertrag "Daten, keine Logik" ausdrücklich erklärt.
Alles Weitere nimmst du selbst aus, über coverage.exclude.
Diff-Coverage
--git-ref <bereich> schaltet das Gate vom gesamten Repository auf die Zeilen
um, die dieser Bereich geändert hat:
codecharter coverage --git-ref origin/main..HEAD
Der Bereich verwendet dieselbe Syntax wie
analyze --git-ref und wird an
git diff <bereich> --unified=0 übergeben. Jede hinzugefügte oder geänderte
Zeile der neuen Seite des Diffs wird mit der zusammengeführten Zeilen-Coverage
des Laufs geschnitten. Die Diff-Coverage ist dann der Anteil der gedeckten
messbaren geänderten Zeilen an allen messbaren geänderten Zeilen. Messbar ist
eine geänderte Zeile, wenn die Coverage-Daten etwas über sie aussagen. Ein
Kommentar, eine Leerzeile, eine Datei ohne Coverage-Daten und eine über
coverage.exclude ausgenommene Datei ändern daran nichts und zählen nirgends
mit. Umbenannte Dateien werden unter ihrem neuen Pfad geführt.
Das ist das Gate, das ein Pull-Request braucht. Ein Repository bei 62 Prozent muss nicht erst auf 100 klettern, um zu verlangen, dass neuer Code getestet ankommt. Jede Änderung prüft sich selbst, und die Gesamtzahl steigt von allein.
Die Gesamt-Coverage wird weiterhin gemessen und genau wie bisher berichtet, sie
entscheidet nur nicht mehr über das Ergebnis. Die Schwelle des Diff-Gates ist
--min-diff-coverage, wenn du es angibst, und andernfalls der wirksame
--min-coverage-Wert, so wie ihn die Konfigurationsreihenfolge aufgelöst hat.
Der Report nennt, welcher der beiden Fälle gegolten hat.
Ein Diff, der überhaupt keine messbare Zeile geändert hat, etwa ein reiner
Doku-Commit oder eine Umbenennung, passiert das Gate und sagt das ausdrücklich:
der berichtete Prozentwert ist null, nie eine erfundene 100.
Für die Diff-Schwelle gibt es bewusst noch keinen Konfigurationsschlüssel. Ein
Git-Bereich ist nur je Aufruf sinnvoll, und --git-ref gehört in die
Pipeline-Definition, nicht in eine eingecheckte Datei.
In einer Pull-Request-Pipeline
- uses: actions/checkout@v4
with:
fetch-depth: 0 # der Basis-Commit muss im Clone liegen
- name: Changed-lines coverage gate
run: codecharter coverage --git-ref origin/main..HEAD --min-diff-coverage 90
fetch-depth: 0 ist der eine Punkt, den du richtig setzen musst. Ein
CI-Checkout ist standardmäßig flach, dann fehlt der Basis-Commit und der Bereich
lässt sich nicht auflösen. Das ist ein Aufruffehler (Exit-Code 64) und nie ein
stiller Erfolg; die Meldung sagt das auch so.
Testauswahl eingrenzen mit --affected-by
--git-ref grenzt ein, welche Zeilen das Gate betreffen; --affected-by
grenzt ein, welche Testprojekte überhaupt laufen:
codecharter coverage --git-ref origin/main..HEAD --affected-by origin/main..HEAD
--affected-by muss denselben Bereich wie --git-ref benennen: Beide Optionen
beschreiben dieselbe Änderung aus zwei Blickwinkeln, und eine Abweichung
zwischen beiden wird abgewiesen, bevor überhaupt etwas läuft. Ausgehend vom
Bereich verfolgt der Befehl den Projektverweisgraphen rückwärts von jedem
Projekt, zu dem eine geänderte Datei gehört, und führt nur die Testprojekte
aus, die dieser Graph erreicht, statt aller von der Suche gefundenen. Auf einer
großen Solution, bei der ein Pull-Request typischerweise nur eine Handvoll
Projekte berührt, wird aus einem Coverage-Lauf, der sonst alles baut und
testet, einer, der nur ausführt, was die Änderung überhaupt betreffen kann.
Eine geänderte Datei, die sich keinem Projekt zuordnen lässt, oder die zu einer
Datei gehört, von der die gesamte Solution abhängt, etwa
Directory.Build.props, eine .sln, .codecharter/config.yml oder alles
unter build/, .github/workflows/ oder tools/, weitet den Lauf automatisch
wieder auf alle gefundenen Testprojekte aus. Eine Änderung, die alles betreffen
könnte, wird also so behandelt, als beträfe sie alles, statt sie stillschweigend
zu unterprüfen.
Drei Aufruffehler, alle mit Exit-Code 64:
--affected-byohne--git-ref.--affected-bymit einem anderen Bereich als--git-ref.--affected-byzusammen mit--skip-tests: Beide Optionen beantworten dieselbe Frage ("welche Projekte zählen") aus unvereinbaren Quellen, einer bereits vorhandenen Coverage-Datei oder einem frisch berechneten Projektabschluss.
Das Gate bleibt auch bei einer engeren Testauswahl fail-closed. Nach dem
Lauf wird jede vom Bereich geänderte Datei gegen das tatsächlich Gemessene
geprüft. Hat eine geänderte Datei überhaupt keine Coverage-Daten, weil die
Tests des zugehörigen Projekts nicht ausgewählt wurden oder weil die Datei
tatsächlich ungetestet ist, schlägt der Lauf mit Exit-Code 3 fehl und nennt
die Datei, genau wie bei einem Lauf ohne jede Messung. --affected-by kann
einen Lauf nur schneller machen, niemals eine Lücke in der Coverage wie einen
Erfolg aussehen lassen.
Schlägt die Prüfung fehl, benennt die Meldung auf stderr die Datei direkt (die Meldung bleibt auf Englisch):
--affected-by completeness check failed: changed file '...' has no coverage data at all. Its owning project's tests were not selected, or the file is genuinely untested; either way this suite selection cannot gate it. Run without --affected-by, or widen the selected suites.
Dabei gilt derselbe Exit-Code 3, den ein Lauf ohne jede Coverage-Daten ergäbe.
Stammt das Ergebnisverzeichnis von einem einfachen
dotnet test --results-directory statt von diesem Befehl (derselbe flache
Aufbau, bei dem --skip-tests auf einen Abgleich der Anzahlen zurückfällt),
braucht die Vollständigkeitsprüfung für --affected-by einen
projektweisen Aufbau: In einem flachen Aufbau sieht die Coverage eines
ausgewählten Projekts genauso aus wie eine veraltete Datei, die ein
übersprungenes Projekt aus einem früheren Lauf hinterlassen hat. Statt zu raten, wird die Prüfung mit
einer Warnung auf stderr übersprungen, und das Gate verlässt sich auf die
Testauswahl.
Exit-Codes
| Code | Bedeutung |
|---|---|
0 |
Die Coverage hat den geforderten Mindestwert erreicht. |
1 |
Die Coverage liegt unter dem geforderten Mindestwert. |
2 |
Tests sind fehlgeschlagen oder die Coverage-Daten waren unvollständig. |
3 |
Überhaupt keine Coverage-Daten. |
64 |
Fehler in Aufruf, Konfiguration oder Umgebung (etwa ein fehlendes .NET SDK oder ein nicht beschreibbares --output-file). |
Mit --affected-by ist eine widersprüchliche oder unvereinbare Kombination
von Optionen (siehe Testauswahl eingrenzen mit --affected-by)
ein Aufruffehler (64), und eine fehlgeschlagene Vollständigkeitsprüfung nach
dem Lauf wird als 3 gemeldet, genau wie jeder andere Lauf ohne Messung für
eine Datei, auf die es ankommt.
Mit --git-ref behalten die Codes ihre Bedeutung und gelten für das Diff-Gate:
1 heißt, die geänderten Zeilen haben ihren Mindestwert verfehlt, und 64 deckt
neben den übrigen Aufruffehlern auch einen nicht auflösbaren Git-Bereich ab.
Das Gate ist fail-closed: fehlende, leere oder unlesbare Coverage-Daten führen
nie zu einem Erfolg. Exit-Code 3 bedeutet "es wurde nichts gemessen" und
erscheint niemals als grüne 100 %.
Die angezeigte Prozentzahl wird abgerundet. 100.00 % heißt also, dass
jede messbare Zeile gedeckt ist, und sonst nichts. Eine einzige ungedeckte
Zeile in einer großen Solution erscheint als 99.99 % und lässt ein Gate mit
100 % fehlschlagen, statt sich zu einem falschen Erfolg aufzurunden.
Der Report
Der JSON-Report besteht aus vier Teilen:
summary: messbare Zeilen, gedeckte Zeilen, der Prozentwert, der geforderte Mindestwert, dessen Herkunft, das Gate-Ergebnis und das Alter der ausgewerteten Coverage-Dateien.testResults: ein Eintrag je Testprojekt mit dessen Exit-Code und der Anzahl der ausgeführten Tests:total,passed,failedundskipped. Diese vier Werte sindnull, wenn das Projekt keine auswertbaren Testergebnisse hinterlassen hat, etwa nach einem mitten im Lauf abgestürzten Test-Host. So sieht "nicht gemessen" nie wie ein Lauf ohne Tests aus. Die Liste insgesamt ist leer, wenn du--skip-testsverwendest.uncoveredRegions: jeder zusammenhängende Block nie ausgeführter Zeilen, mit Datei (als absoluter Pfad und relativ zum Lauf-Root), umgebender Methode, Zeilennummern und Quelltext-Ausschnitt. Eine Region endet außerdem an der Methodengrenze: Ein Block, der vom Ende einer Methode in die nächste läuft, erscheint als zwei Regionen, jede der Methode zugeordnet, in der sie tatsächlich liegt.diffCoverage:null, solange der Lauf nicht mit--git-refeingegrenzt wurde. Andernfalls der Git-Bereich,measurableChangedLines,coveredChangedLines, derpercent-Wert (null, wenn nichts Messbares geändert wurde), derrequiredPercentmit seiner HerkunftthresholdProvenance(--min-diff-coverageoderinherited), das Gate-ErgebnismetunduncoveredChangedRegions, also die ungedeckten Regionen eingeschränkt auf die geänderten Zeilen, in derselben Form wieuncoveredRegions.
Der relative Pfad ist der stabile Schlüssel für CI-Annotationen, denn er hängt nicht davon ab, wohin der Build-Agent das Repository ausgecheckt hat.
Ungedeckte Regionen als Findings
Mit --findings werden dieselben ungedeckten Regionen zusätzlich als Findings
gemeldet, in genau der Form, die analyze erzeugt, und
über dieselben Formatter gerendert. Jedes Finding trägt die Regel-ID
coverage/uncovered-region, eine Meldung mit Methode und Zeilenbereich sowie
als Anker den repo-relativen Dateipfad und die erste Zeile der Region. Die
Schwere folgt dem Gate: informativ, solange der Lauf seine Schwelle noch
erreicht, und ein Fehler, sobald er sie verfehlt. Mit --git-ref folgt die Schwere
dem Diff-Gate, und nur eine Region, die tatsächlich eine geänderte Zeile enthält,
kann den Fehler tragen. Jede andere Region bleibt informativ, damit ein kleiner
Pull-Request nie eine ganze gewachsene Codebasis rot färbt.
# Ungedeckte Zeilen an einem GitHub-Pull-Request annotieren
codecharter coverage --output-file coverage-report.json \
--findings --findings-format github-annotations
Der Schalter ist rein additiv. Der JSON-Report bleibt die Standardausgabe auf
stdout und ändert sich nicht, und der Exit-Code ist exakt derselbe wie ohne den
Schalter. Die Findings gehen nach stderr, solange du mit --findings-file
kein Ziel angibst, damit stdout weiterhin den Report trägt. In der Ausgabe von
codecharter analyze tauchen Coverage-Findings nie auf, die beiden Gates
bleiben getrennt.
In der CI
- name: Coverage gate
run: codecharter coverage --output-file coverage-report.json
# Exit-Code 1 färbt den Schritt rot, sobald die Coverage unter den Mindestwert fällt.
Da die Schwelle in der Konfigurationsdatei steht, muss die Pipeline-Definition nicht angefasst werden, wenn das Team die Latte höher legt.
Mit einem KI-Assistenten
Dasselbe Gate steht KI-Coding-Assistenten als Tool run_coverage des
MCP-Servers zur Verfügung. Der Assistent erhält die
ungedeckten Regionen samt Datei, Methode, Zeilennummern und Ausschnitt, also
alles, was er braucht, um die fehlenden Tests zu schreiben, ohne vorher danach
zu suchen. Zusätzlich enthält das Tool-Ergebnis ein Array findings mit
denselben Regionen in der Form, die auch die analyze_*-Tools liefern. Ein
Assistent kann eine Coverage-Lücke damit genauso behandeln wie einen
Regelverstoß. Das Tool nimmt zusätzlich den optionalen Parameter git_ref mit
derselben Bedeutung wie --git-ref und liefert dann die Sektion diffCoverage
mit, sodass ein Assistent genau die Zeilen abarbeiten kann, die seine eigene
Änderung ungetestet gelassen hat. Es nimmt außerdem den Parameter affected_by
mit derselben Bedeutung und denselben Regeln wie --affected-by entgegen, und
sein Ergebnis trägt, anders als der Report auf stdout der Kommandozeile, ein
Objekt coverageScope, das genau benennt, welche Testsuiten ausgewählt und
welche übersprungen wurden, und bei einer fehlgeschlagenen
Vollständigkeitsprüfung, welche geänderte Datei die Ursache war.
Weiterführend
- codecharter analyze für die Code-Qualitätshälfte des Gates.
- Konfigurationsdatei für die
coverage-Sektion im Zusammenhang. - Exit-Codes für die Exit-Codes der übrigen Befehle.