From 2090f7865c29ea3b7edc735dc4b7a7764c348b6f Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 16 Jul 2026 18:45:32 +0200 Subject: [PATCH 1/3] ci(coverage): collect code coverage and publish a summary (refs #15) #15 scope item 4 ("collect with coverlet and publish a summary; decide on a floor later"). coverlet.collector was already referenced by four of the six test projects but the CI Test step never passed --collect and nothing summarized the result. - Add coverlet.collector to ErsatzTV.Core.Tests and ErsatzTV.Tests so coverage is uniform across all *.Tests projects. - CI Test step now runs --collect:"XPlat Code Coverage" --results-directory ./coverage, then a best-effort Coverage summary step merges the per-project Cobertura reports with ReportGenerator (TextSummary to the log, MarkdownSummaryGithub to the job step summary). No floor is enforced yet; continue-on-error keeps it purely informational. - gitignore /coverage/; document the step in docs/ci-cd.md (test job). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/docker-build.yml | 33 ++++++++++++++++++- .gitignore | 2 ++ .../ErsatzTV.Core.Tests.csproj | 4 +++ ErsatzTV.Tests/ErsatzTV.Tests.csproj | 4 +++ docs/ci-cd.md | 6 ++++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 427b1e401..e9dd1408b 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -101,7 +101,38 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Test - run: dotnet test --configuration Release --no-build --blame-hang-timeout "2m" --verbosity normal + run: >- + dotnet test --configuration Release --no-build --blame-hang-timeout "2m" --verbosity normal + --collect:"XPlat Code Coverage" --results-directory ./coverage + + # Coverage reporting (ersatztv#15 scope item 4): coverlet.collector emits a Cobertura report + # per test project (via --collect above); ReportGenerator merges them into a human-readable + # summary printed to the log and the job step summary. No floor is enforced yet ("decide on a + # floor later"), so this step is purely informational — continue-on-error keeps a missing + # report or a transient tool-install failure from ever blocking a build. + - name: Coverage summary + continue-on-error: true + run: | + set -euo pipefail + shopt -s globstar nullglob + reports=(coverage/**/coverage.cobertura.xml) + if [ ${#reports[@]} -eq 0 ]; then + echo "No coverage reports found under ./coverage -- skipping summary." + exit 0 + fi + echo "Found ${#reports[@]} coverage report(s)." + dotnet tool install --global dotnet-reportgenerator-globaltool >/dev/null + export PATH="$PATH:$HOME/.dotnet/tools" + reportgenerator \ + "-reports:coverage/**/coverage.cobertura.xml" \ + "-targetdir:coverage/report" \ + "-reporttypes:TextSummary;MarkdownSummaryGithub" + echo "::group::Coverage summary" + cat coverage/report/Summary.txt + echo "::endgroup::" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && [ -f coverage/report/SummaryGithub.md ]; then + cat coverage/report/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY" + fi migrations: name: EF migration integrity (SQLite + MySql) diff --git a/.gitignore b/.gitignore index f44e9cb5a..55bc3d56c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.*~ project.lock.json .DS_Store +# Code-coverage output (dotnet test --results-directory ./coverage, ersatztv#15) +/coverage/ *.pyc .worktrees/ diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index bdc224593..22ad55ccb 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -7,6 +7,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/ErsatzTV.Tests/ErsatzTV.Tests.csproj b/ErsatzTV.Tests/ErsatzTV.Tests.csproj index 76c49cf5a..8db215ef5 100644 --- a/ErsatzTV.Tests/ErsatzTV.Tests.csproj +++ b/ErsatzTV.Tests/ErsatzTV.Tests.csproj @@ -11,6 +11,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 2e0e233b0..5db0e3c3b 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -95,6 +95,12 @@ long builds (observed 31 min) stalling every PR run. Docker build) → `dotnet build -c Release` → `dotnet test -c Release --no-build`. Gates the image build. +- **Code coverage** (ersatztv#15): `dotnet test` runs with `--collect:"XPlat Code Coverage" + --results-directory ./coverage`, so `coverlet.collector` (referenced by every `*.Tests` + project) emits a Cobertura report per project. A follow-up **Coverage summary** step merges + them with ReportGenerator (`TextSummary` to the log, `MarkdownSummaryGithub` to the job step + summary). No floor is enforced yet ("decide on a floor later" — #15); the step is + `continue-on-error: true`, so a missing report or a transient tool install never blocks a build. - **Shallow checkout**: `fetch-depth: 1` (ersatztv#190) — this job never runs `git describe`/`git log`, only `build` needs full history/tags for version computation, so `test` and `migrations` both check out shallow. `build`'s checkout stays `fetch-depth: 0`. -- 2.47.3 From a886fd2824db97439a52d75440b1efd7c99423d9 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 16 Jul 2026 18:49:28 +0200 Subject: [PATCH 2/3] ci(coverage): make ReportGenerator install idempotent + pinned (review follow-up) Cold-review should-fix on PR #370: `dotnet tool install` errors under set -e if the tool is already present (retried step / cached runner image), which would silently degrade the continue-on-error summary step to a no-op. Use `dotnet tool update` (install-or-update) pinned to 5.5.10 for reproducible output. Also order coverlet.collector after CliWrap to keep the ItemGroup alphabetical (nit). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/docker-build.yml | 4 +++- ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index e9dd1408b..5e1fdaf22 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -121,7 +121,9 @@ jobs: exit 0 fi echo "Found ${#reports[@]} coverage report(s)." - dotnet tool install --global dotnet-reportgenerator-globaltool >/dev/null + # `update` is install-or-update (idempotent, unlike `install` which errors if the tool + # is already present under set -e); pinned for reproducible summary output. + dotnet tool update --global dotnet-reportgenerator-globaltool --version 5.5.10 >/dev/null export PATH="$PATH:$HOME/.dotnet/tools" reportgenerator \ "-reports:coverage/**/coverage.cobertura.xml" \ diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 22ad55ccb..e20aaa355 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -7,11 +7,11 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all - -- 2.47.3 From e78aa01c05552d2e403662e248674df00920862e Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 16 Jul 2026 19:03:54 +0200 Subject: [PATCH 3/3] ci(coverage): exclude generated EF migrations to stop coverage OOM (exit 137) The shared Build & test job OOM-killed (exit 137) with coverage enabled: the whole-solution Architecture.Tests process instruments both EF migration folders (~2.59M generated lines vs ~200k authored), ballooning coverlet's in-memory hit-count arrays past the runner's limit. Add coverlet.runsettings excluding **/Migrations/*.cs (+ generated/compiler attributes) and pass it via --settings on the Test step. This cuts the instrumented surface ~126x (2,510,185 -> 19,963 coverable lines in the Architecture.Tests process, verified locally) and makes the reported percentage reflect authored code. Documented in docs/ci-cd.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/docker-build.yml | 2 +- coverlet.runsettings | 24 ++++++++++++++++++++++++ docs/ci-cd.md | 16 +++++++++++----- 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 coverlet.runsettings diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 5e1fdaf22..fee965db9 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -103,7 +103,7 @@ jobs: - name: Test run: >- dotnet test --configuration Release --no-build --blame-hang-timeout "2m" --verbosity normal - --collect:"XPlat Code Coverage" --results-directory ./coverage + --collect:"XPlat Code Coverage" --settings coverlet.runsettings --results-directory ./coverage # Coverage reporting (ersatztv#15 scope item 4): coverlet.collector emits a Cobertura report # per test project (via --collect above); ReportGenerator merges them into a human-readable diff --git a/coverlet.runsettings b/coverlet.runsettings new file mode 100644 index 000000000..1c3d08a7c --- /dev/null +++ b/coverlet.runsettings @@ -0,0 +1,24 @@ + + + + + + + + **/Migrations/*.cs + GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute + [*]*.Migrations.* + true + + + + + diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 5db0e3c3b..d47d87b64 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -96,11 +96,17 @@ Docker build) → `dotnet build -c Release` → `dotnet test -c Release --no-bui the image build. - **Code coverage** (ersatztv#15): `dotnet test` runs with `--collect:"XPlat Code Coverage" - --results-directory ./coverage`, so `coverlet.collector` (referenced by every `*.Tests` - project) emits a Cobertura report per project. A follow-up **Coverage summary** step merges - them with ReportGenerator (`TextSummary` to the log, `MarkdownSummaryGithub` to the job step - summary). No floor is enforced yet ("decide on a floor later" — #15); the step is - `continue-on-error: true`, so a missing report or a transient tool install never blocks a build. + --settings coverlet.runsettings --results-directory ./coverage`, so `coverlet.collector` + (referenced by every `*.Tests` project) emits a Cobertura report per project. A follow-up + **Coverage summary** step merges them with ReportGenerator (`TextSummary` to the log, + `MarkdownSummaryGithub` to the job step summary). No floor is enforced yet ("decide on a + floor later" — #15); the step is `continue-on-error: true`, so a missing report or a + transient tool install never blocks a build. + - **`coverlet.runsettings` excludes generated EF migration code** (`**/Migrations/*.cs`, + ~2.59M generated lines vs ~200k authored). Instrumenting it OOM-killed the shared `test` + job (exit 137); excluding it cuts the instrumented surface ~126× (2.5M→20k coverable + lines in the whole-solution `Architecture.Tests` process) and makes the percentage reflect + authored code. - **Shallow checkout**: `fetch-depth: 1` (ersatztv#190) — this job never runs `git describe`/`git log`, only `build` needs full history/tags for version computation, so `test` and `migrations` both check out shallow. `build`'s checkout stays `fetch-depth: 0`. -- 2.47.3