Skip to content
Merged
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
36 changes: 30 additions & 6 deletions .github/scripts/render-homepage-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,29 @@
"MariaDB", "Firebird", "Db2", "Informix", "Sybase", "Hana"}


def render(results, run):
def render_coverage(report, run):
if report is None or not pathlib.Path(report).is_file():
return '<p>Code coverage unavailable for this run: no merged coverage artifact is available.</p>'
root = ET.parse(report).getroot()
if root.tag != 'coverage':
raise ValueError('Expected a Cobertura coverage report')
items = []
for metric, label in (("lines", "line coverage"), ("branches", "branch coverage")):
covered = int(root.attrib[f'{metric}-covered'])
total = int(root.attrib[f'{metric}-valid'])
if not 0 <= covered <= total or (metric == 'lines' and total == 0):
raise ValueError(f'Invalid {metric} coverage')
rate = f'{100 * covered / total:.2f}%' if total else 'N/A'
items.append(f'<li><strong>{rate}</strong>{label}<small>{covered:,} / {total:,} covered</small></li>')
url = html.escape(run.get('coverage_url', run['html_url']), quote=True)
return ('<h3>Code coverage</h3><ul class="test-counts coverage-counts">' + ''.join(items)
+ '</ul><p>Production assemblies, merged across unit and database suites; '
'shared code is counted once. '
f'<a href="{url}">Download the full HTML coverage report ↗</a> '
'(code-coverage artifact; open index.html).</p>')


def render(results, run, coverage=None):
suites = {}
for path in pathlib.Path(results).rglob("*.trx"):
name = path.stem
Expand All @@ -27,22 +49,24 @@ def render(results, run):
sha = html.escape(run["head_sha"][:7])
date = html.escape(run["updated_at"])
conclusion = html.escape(run["conclusion"] or "unknown")
source = f'<p><a href="{url}">CI run · {sha}</a> · {date} · workflow: {conclusion}. Latest completed master run at deployment time.</p>'
source = f'<p><a href="{url}">CI run · {sha}</a> · {date} · workflow: {conclusion}. Latest completed master run at deployment time.</p>'
coverage_html = render_coverage(coverage, run)
if set(suites) != EXPECTED:
return source + f'<p>Incomplete test results: {len(suites)} of {len(EXPECTED)} suites available. Full totals unavailable; inspect the run for failures or missing artifacts.</p>'
return source + f'<p>Incomplete test results: {len(suites)} of {len(EXPECTED)} suites available. Full totals unavailable; inspect the run for failures or missing artifacts.</p>' + coverage_html
totals = {key: sum(s[key] for s in suites.values()) for key in ("total", "executed", "passed", "failed")}
totals["skipped"] = totals["total"] - totals["executed"]
totals["other"] = totals["executed"] - totals["passed"] - totals["failed"]
counts = '<ul class="test-counts">' + ''.join(f'<li><strong>{totals[key]:,}</strong>{label}</li>' for key, label in (("executed", "executed"), ("passed", "passed"), ("failed", "failed"), ("skipped", "skipped / not executed"), ("other", "other outcomes"))) + '</ul>'
return counts + source
return counts + coverage_html + source


if __name__ == "__main__":
results, metadata, page = sys.argv[1:]
results, metadata, page = sys.argv[1:4]
coverage = sys.argv[4] if len(sys.argv) > 4 else None
target = pathlib.Path(page)
content = target.read_text(encoding="utf-8")
start = "<!-- TEST_RESULTS_START -->"
end = "<!-- TEST_RESULTS_END -->"
before, remainder = content.split(start, 1)
_, after = remainder.split(end, 1)
target.write_text(before + start + "\n" + render(results, json.loads(pathlib.Path(metadata).read_text(encoding="utf-8"))) + "\n" + end + after, encoding="utf-8")
target.write_text(before + start + "\n" + render(results, json.loads(pathlib.Path(metadata).read_text(encoding="utf-8")), coverage) + "\n" + end + after, encoding="utf-8")
47 changes: 45 additions & 2 deletions .github/scripts/test-homepage-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,53 @@ def test_duplicate_suite_rejected(self):
with self.assertRaises(ValueError):
renderer.render(self.root, self.run)

def test_invalid_counts_rejected(self):
def test_invalid_counts_rejected(self):
self.populate(passed=8)
with self.assertRaises(ValueError):
renderer.render(self.root, self.run)
renderer.render(self.root, self.run)

def coverage(self, lines=75, total=100, branches=2, branch_total=3):
path = self.root / 'Cobertura.xml'
path.write_text(f'<coverage lines-covered="{lines}" lines-valid="{total}" '
f'branches-covered="{branches}" branches-valid="{branch_total}"/>')
return path

def test_merged_coverage_and_report_link(self):
self.populate()
self.run['coverage_url'] = self.run['html_url'] + '/artifacts/123'
result = renderer.render(self.root, self.run, self.coverage())
for text in ('75.00%</strong>line coverage', '66.67%</strong>branch coverage',
'75 / 100 covered', '2 / 3 covered', '/artifacts/123', '48</strong>executed'):
self.assertIn(text, result)

def test_missing_coverage_preserves_test_results(self):
self.populate()
result = renderer.render(self.root, self.run, self.root / 'missing.xml')
self.assertIn('Code coverage unavailable for this run', result)
self.assertIn('48</strong>executed', result)
self.assertNotIn('0.00%', result)

def test_zero_branches_is_not_zero_percent(self):
result = renderer.render(self.root, self.run, self.coverage(branches=0, branch_total=0))
self.assertIn('N/A</strong>branch coverage', result)

def test_invalid_coverage_rejected(self):
for values in ({'lines': 101}, {'lines': -1}, {'total': 0}, {'branches': 4}):
with self.subTest(values=values), self.assertRaises(ValueError):
renderer.render(self.root, self.run, self.coverage(**values))

def test_incomplete_tests_still_identify_coverage_source(self):
result = renderer.render(self.root, self.run, self.coverage())
self.assertIn('Incomplete test results: 0 of 12', result)
self.assertIn('75.00%', result)
self.assertIn('abc1234', result)

def test_suite_coverage_is_not_used_as_merged_coverage(self):
self.populate()
self.coverage()
result = renderer.render(self.root, self.run)
self.assertIn('Code coverage unavailable', result)
self.assertNotIn('75.00%', result)


if __name__ == '__main__':
Expand Down
22 changes: 19 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
run: |
python3 -B .github/scripts/build-docs.py --check
python3 -B .github/scripts/verify-docs.py
- name: Verify test-count renderer
- name: Verify test-count and coverage renderer
run: python3 .github/scripts/test-homepage-tests.py
- name: Select latest completed master test run
id: tests
Expand All @@ -58,6 +58,14 @@ jobs:
});
const run = data.workflow_runs[0];
if (run) {
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
...context.repo, run_id: run.id, per_page: 100
});
const coverage = artifacts.find(a => a.name === 'code-coverage' && !a.expired);
if (coverage) {
run.coverage_url = `${run.html_url}/artifacts/${coverage.id}`;
core.setOutput('has-coverage', 'true');
}
fs.writeFileSync('ci-run.json', JSON.stringify(run));
core.setOutput('run-id', String(run.id));
}
Expand All @@ -69,9 +77,17 @@ jobs:
run-id: ${{ steps.tests.outputs.run-id }}
pattern: test-results-*
path: ci-results
- name: Render test counts and provenance
- name: Download merged coverage from the same run
if: steps.tests.outputs.has-coverage == 'true'
uses: actions/download-artifact@v4
with:
github-token: ${{ github.token }}
run-id: ${{ steps.tests.outputs.run-id }}
name: code-coverage
path: ci-coverage
- name: Render test counts, coverage and provenance
if: steps.tests.outputs.run-id != ''
run: python3 .github/scripts/render-homepage-tests.py ci-results ci-run.json docs/index.html
run: python3 .github/scripts/render-homepage-tests.py ci-results ci-run.json docs/index.html ci-coverage/Cobertura.xml
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj)
[![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/)

[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Documentation](https://dotnetprojects.github.io/Migrator.NET/guide/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) · [CI test counts](https://dotnetprojects.github.io/Migrator.NET/#test-results)
[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Documentation](https://dotnetprojects.github.io/Migrator.NET/guide/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) · [CI test results & coverage](https://dotnetprojects.github.io/Migrator.NET/#test-results)

DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied.

Expand Down
9 changes: 7 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,18 @@ The comparison distinguishes source capabilities from guarantees about released

The quick start targets .NET 9 and installs the core library and SQLite driver through NuGet. Sample validation uses local project references to catch API drift. The SQLite driver version matches the repository test dependency.

## Status badges and test counts
## Status badges, test counts and code coverage

The homepage links to master CI results and NuGet, and identifies the MPL-1.1 license.
Pages also redeploys when master CI completes. During deployment it selects the latest
completed master push run (including failures), downloads its TRX artifacts, and renders
executed/passed/failed/skipped/other counts with the run URL, commit and timestamp.
Counts are a deployment snapshot, not code coverage or a guarantee about every provider.
The same run supplies the merged `code-coverage` artifact. Pages displays line and branch
coverage percentages and covered/total counts for production assemblies, with a link to
the downloadable HTML report (open `index.html`). Shared code is counted once in the
merged report; suite percentages are never added or averaged. A missing or expired
coverage artifact is shown as unavailable, never as 0% or replaced by another run.
Counts and coverage are a deployment snapshot, not a guarantee about every provider.
Missing suites produce an incomplete-results message, never a partial success total.
The committed/local page shows a fallback link until deployment supplies results.
The renderer only changes the uploaded Pages artifact; it does not commit generated counts. Preserve `TEST_RESULTS_START` and `TEST_RESULTS_END` in `_src/home.html`.
Expand Down
4 changes: 2 additions & 2 deletions docs/_src/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ <h1 id="hero-title">Database changes,<br><em>written in C#.</em></h1>
<section class="container section start-grid" id="quick-start" aria-labelledby="start-title"><div><p class="eyebrow">FROM EMPTY FOLDER TO FIRST TABLE</p><h2 id="start-title">Start with<br><em>one change.</em></h2><p>Install the library and your database driver. Add the migration above, wire up the runner, and apply it. SQLite makes a convenient first database.</p><a class="button primary" href="guide/quick-start.html">Follow the complete quick start ↗</a></div>{{install}}</section>
<section class="manual-preview container section" aria-labelledby="manual-title"><div class="section-heading"><div><p class="eyebrow">THE MIGRATION MANUAL</p><h2 id="manual-title">Past “hello, table.”</h2></div><p>Detailed guides, paired examples, and provider notes.<br><a href="guide/index.html">Browse every chapter ↗</a></p></div><div class="chapter-preview"><a href="guide/creating-tables.html"><span>01 / AUTHORING</span><h3>Shape the schema ↗</h3><p>Tables, columns, data, indexes and constraints.</p></a><a href="guide/configuration.html"><span>02 / EXECUTION</span><h3>Control the run ↗</h3><p>Configuration, transactions, DI, CLI and SQL preview.</p></a><a href="guide/providers.html"><span>03 / DATABASES</span><h3>Know your provider ↗</h3><p>Storage behavior, SQLite rebuilds and engine differences.</p></a><a href="guide/testing.html"><span>04 / PRACTICE</span><h3>Ship with evidence ↗</h3><p>Reversals, conditional changes, testing and upgrades.</p></a></div></section>
<section class="provider-section container section" id="providers" aria-labelledby="provider-title"><p class="eyebrow">BRING YOUR DATABASE</p><h2 id="provider-title">One migration system.<br><em>Many dialects.</em></h2><ul class="provider-list"><li><a href="guide/sqlite.html">SQLite</a></li><li><a href="guide/sql-server.html">SQL Server</a></li><li><a href="guide/postgresql.html">PostgreSQL</a></li><li><a href="guide/mysql.html">MySQL</a></li><li><a href="guide/mysql.html">MariaDB</a></li><li><a href="guide/oracle.html">Oracle</a></li><li><a href="guide/other-providers.html">Firebird</a></li><li><a href="guide/other-providers.html">Db2</a></li><li><a href="guide/other-providers.html">Informix</a></li><li><a href="guide/other-providers.html">Sybase</a></li><li><a href="guide/other-providers.html">SAP HANA</a></li><li><a href="guide/other-providers.html">Ingres</a></li></ul><p>Capabilities follow the database engine. The <a href="guide/providers.html">provider guide</a> explains aliases, drivers and operation-specific behavior.</p></section>
<section class="container section ci-section" id="test-results" aria-labelledby="tests-title"><p class="eyebrow">BUILD AND DATABASE TESTS</p><h2 id="tests-title">Evidence from CI.</h2><p>The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately.</p><!-- TEST_RESULTS_START -->
<p>Test totals are populated during deployment from the latest completed master CI run. <a href="https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml?query=branch%3Amaster">View CI runs and test-result artifacts ↗</a>.</p>
<section class="container section ci-section" id="test-results" aria-labelledby="tests-title"><p class="eyebrow">TEST RESULTS AND CODE COVERAGE</p><h2 id="tests-title">Evidence from CI.</h2><p>The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately. Line and branch coverage measure production code across the combined suites.</p><!-- TEST_RESULTS_START -->
<p>Test totals and code coverage are populated during deployment from the latest completed master CI run. <a href="https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml?query=branch%3Amaster">View CI runs and test-result artifacts ↗</a>.</p>
<!-- TEST_RESULTS_END --></section>
{{comparison}}
<section class="closing-section container"><p class="eyebrow">EXPLICIT CHANGES. A LASTING RECORD.</p><h2>The next version<br><em>starts with a change.</em></h2><a class="button primary" href="guide/quick-start.html">Open the manual ↗</a><a class="text-link" href="https://github.com/dotnetprojects/Migrator.NET">Contribute on GitHub →</a></section>
Expand Down
8 changes: 7 additions & 1 deletion docs/assets/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,13 @@ img {
font-size:14px;
color:var(--muted);
}
.test-counts {
.coverage-counts small {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.test-counts {
display:flex;
list-style:none;
gap:35px;
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/altering-tables.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/guide/api-map.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading