diff --git a/.github/scripts/gen_landing_page.py b/.github/scripts/gen_landing_page.py new file mode 100644 index 00000000..94d6c218 --- /dev/null +++ b/.github/scripts/gen_landing_page.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Generate the root index.html landing page for the gh-pages site. + +Scans results////summary.json +and produces a table linking to each run's individual report. + +Usage: + python3 gen_landing_page.py + + is the root of the gh-pages checkout (i.e. the directory that +contains the 'results/' sub-tree and will receive the generated index.html). +""" + +import json +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def pct_str(num: int, den: int) -> str: + if den == 0: + return "—" + return f"{100 * num / den:.1f} %" + + +def format_duration(seconds: float) -> str: + t = int(seconds) + if t < 60: + return f"{t} s" + m, s = divmod(t, 60) + if m < 60: + return f"{m} min {s} s" + h, m = divmod(m, 60) + return f"{h} h {m} min" + + +def pct_class(num: int, den: int) -> str: + """CSS class for a pass-rate cell.""" + if den == 0: + return "na" + r = num / den + if r >= 0.90: + return "ok" + if r >= 0.70: + return "warn" + return "fail" + + +def git_date(summary_path: Path, site_root: Path) -> str: + """Return the YYYY-MM-DD of the git commit that last touched summary_path.""" + try: + rel = summary_path.relative_to(site_root) + result = subprocess.run( + ["git", "log", "-1", "--format=%as", "--", str(rel)], + capture_output=True, text=True, cwd=site_root, + ) + return result.stdout.strip() + except Exception: + return "" + + +# ── Data loading ────────────────────────────────────────────────────────────── + +def load_runs(site_root: Path) -> list[dict]: + runs = [] + results_dir = site_root / "results" + if not results_dir.exists(): + return runs + + for summary_path in sorted(results_dir.glob("*/*/*/summary.json"), reverse=True): + try: + with open(summary_path, encoding="utf-8") as f: + data = json.load(f) + except Exception: + continue + + models = data.get("models", []) + n = len(models) + n_exp = sum(1 for m in models if m.get("export", False)) + n_par = sum(1 for m in models if m.get("parse", False)) + n_sim = sum(1 for m in models if m.get("sim", False)) + + cmp_models = [m for m in models if m.get("cmp_total", 0) > 0] + n_cmp = len(cmp_models) + n_cmp_pass = sum(1 for m in cmp_models if m["cmp_pass"] == m["cmp_total"]) + + run_dir = summary_path.parent + index_url = str((run_dir / "index.html").relative_to(site_root)).replace("\\", "/") + + runs.append({ + "bm_version": data.get("bm_version", "?"), + "library": data.get("library", "?"), + "lib_version": data.get("lib_version", "?"), + "omc_version": data.get("omc_version", "?"), + "total": n, + "n_exp": n_exp, + "n_par": n_par, + "n_sim": n_sim, + "n_cmp": n_cmp, + "n_cmp_pass": n_cmp_pass, + "duration": format_duration(data.get("total_time_s", 0)), + "date": git_date(summary_path, site_root), + "index_url": index_url, + }) + + return runs + + +# ── HTML rendering ──────────────────────────────────────────────────────────── + +def _pct_cell(num: int, den: int) -> str: + css = pct_class(num, den) + label = f"{num}/{den} ({pct_str(num, den)})" if den > 0 else "—" + return f'{label}' + + +def render(runs: list[dict]) -> str: + now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + + if runs: + rows = [] + for r in runs: + cmp_cell = ( + _pct_cell(r["n_cmp_pass"], r["n_cmp"]) + if r["n_cmp"] > 0 + else '—' + ) + rows.append(f"""\ + + {r['bm_version']} + {r['library']} + {r['lib_version']} + {r['omc_version']} + {r['date']} + {r['duration']} + {_pct_cell(r['n_exp'], r['total'])} + {_pct_cell(r['n_par'], r['n_exp'])} + {_pct_cell(r['n_sim'], r['n_par'])} + {cmp_cell} + """) + rows_html = "\n".join(rows) + else: + rows_html = ' No results yet.' + + return f"""\ + + + + + BaseModelicaLibraryTesting — Test Results + + + +

BaseModelicaLibraryTesting — Test Results

+

Generated: {now}

+ + + + + + + + + + + + + +{rows_html} +
BaseModelica.jlLibraryVersionOpenModelicaDateDurationBM ExportBM ParseMTK SimRef Cmp
+ + +""" + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +def main() -> None: + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + site_root = Path(sys.argv[1]).resolve() + runs = load_runs(site_root) + html = render(runs) + + # Disable Jekyll so GitHub Pages serves files as-is + (site_root / ".nojekyll").touch() + + out = site_root / "index.html" + out.write_text(html, encoding="utf-8") + print(f"Landing page written to {out} ({len(runs)} run(s) listed)") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bb45eafb..89fab857 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -92,7 +92,7 @@ jobs: library = "Modelica", version = "4.1.0", filter = "Modelica.Electrical.Analog.Examples.ChuaCircuit", - results_root = "main/Modelica/4.1.0/" + results_root = "results/main/Modelica/4.1.0/" ) ' @@ -100,5 +100,5 @@ jobs: uses: actions/upload-artifact@v6 with: name: sanity-results-${{ matrix.os }} - path: main/ + path: results/ if-no-files-found: error diff --git a/.github/workflows/msl-test.yml b/.github/workflows/msl-test.yml new file mode 100644 index 00000000..7e659e5d --- /dev/null +++ b/.github/workflows/msl-test.yml @@ -0,0 +1,129 @@ +name: MSL Test & GitHub Pages + +# Run the full Modelica Standard Library pipeline and publish results to +# GitHub Pages at results//Modelica// +# +# Prerequisites (one-time repo setup): +# Settings → Pages → Source: Deploy from a branch → Branch: gh-pages / (root) + +on: + schedule: + - cron: '0 3 * * *' # every day 03:00 UTC + workflow_dispatch: + inputs: + lib_version: + description: 'Modelica Standard Library version' + required: false + default: '4.1.0' + type: string + +concurrency: + group: pages + cancel-in-progress: false # never abort a Pages deployment mid-flight + +permissions: + contents: write # needed to push to gh-pages + +jobs: + test-and-deploy: + name: Test MSL & Deploy Pages + runs-on: ubuntu-latest + timeout-minutes: 480 + + env: + LIB_VERSION: ${{ inputs.lib_version || '4.1.0' }} + + steps: + - name: Checkout source + uses: actions/checkout@v6 + + - name: Set up OpenModelica (nightly) + uses: OpenModelica/setup-openmodelica@v1.0 + with: + version: nightly + packages: | + omc + libraries: | + 'Modelica ${{ env.LIB_VERSION }}' + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: '1' + arch: x64 + + - name: Restore Julia package cache + uses: julia-actions/cache@v2 + + - name: Build package + uses: julia-actions/julia-buildpkg@v1 + + # ── Resolve versions ────────────────────────────────────────────────────── + - name: Resolve BaseModelica.jl version + id: versions + run: | + julia --project=. -e ' + import BaseModelica + println("bm_version=" * string(pkgversion(BaseModelica))) + ' >> "$GITHUB_OUTPUT" + + # ── Reference results ───────────────────────────────────────────────────── + - name: Checkout MAP-LIB reference results + uses: actions/checkout@v6 + with: + repository: modelica/MAP-LIB_ReferenceResults + ref: v${{ env.LIB_VERSION }} + path: MAP-LIB_ReferenceResults + fetch-depth: 1 + + # ── Run the pipeline ────────────────────────────────────────────────────── + - name: Run MSL pipeline + env: + BM_VERSION: ${{ steps.versions.outputs.bm_version }} + run: | + julia --project=. -e ' + using BaseModelicaLibraryTesting + main( + library = "Modelica", + version = ENV["LIB_VERSION"], + results_root = "results/$(ENV["BM_VERSION"])/Modelica/$(ENV["LIB_VERSION"])", + ref_root = "MAP-LIB_ReferenceResults", + ) + ' + + - name: Upload test results + uses: actions/upload-artifact@v6 + with: + name: sanity-results-${{ matrix.os }} + path: results/ + if-no-files-found: error + + + # ── Deploy to gh-pages ──────────────────────────────────────────────────── + - name: Prepare gh-pages worktree + run: | + git fetch origin gh-pages 2>/dev/null || true + if git show-ref --verify refs/remotes/origin/gh-pages 2>/dev/null; then + git worktree add site origin/gh-pages + else + # First ever run: create an orphan branch + git worktree add --orphan -b gh-pages site + fi + + - name: Copy new results into gh-pages tree + run: rsync -a results/ site/results/ + + - name: Generate landing page + run: python3 .github/scripts/gen_landing_page.py site + + - name: Commit and push to gh-pages + env: + BM_VERSION: ${{ steps.versions.outputs.bm_version }} + run: | + cd site + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --quiet && { echo "Nothing to commit."; exit 0; } + git commit -m "results: ${BM_VERSION}/Modelica/${LIB_VERSION} [$(date -u '+%Y-%m-%d')]" + git push origin HEAD:gh-pages diff --git a/README.md b/README.md index c9a513a8..ad751368 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # BaseModelicaLibraryTesting.jl [![Build Status][build-badge-svg]][build-action-url] +[![MSL Test Reports][msl-badge-svg]][msl-pages-url] Experimental Base Modelica library testing based on Julia. @@ -68,6 +69,8 @@ file for details. [build-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml/badge.svg?branch=main [build-action-url]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/CI.yml?query=branch%3Amain +[msl-badge-svg]: https://github.com/OpenModelica/BaseModelicaLibraryTesting.jl/actions/workflows/msl-test.yml/badge.svg?branch=main +[msl-pages-url]: https://openmodelica.github.io/BaseModelicaLibraryTesting.jl/ [openmodelica-url]: https://openmodelica.org/ [basemodelicajl-url]: https://github.com/SciML/BaseModelica.jl [modelingtoolkitjl-url]: https://github.com/SciML/ModelingToolkit.jl