|
28 | 28 |
|
29 | 29 |
|
30 | 30 | REPO_ROOT = Path(__file__).resolve().parents[3] |
| 31 | +README = REPO_ROOT / "README.md" |
| 32 | +DOCS_MODELS = REPO_ROOT / "docs" / "models" |
| 33 | +SUPPORTED_MODELS_HEADING = "## Supported Models" |
31 | 34 | RECIPES_DIR = REPO_ROOT / "src" / "megatron" / "bridge" / "recipes" |
32 | 35 | PERF_RECIPES_DIR = REPO_ROOT / "src" / "megatron" / "bridge" / "perf_recipes" |
33 | 36 | TRAINING_README = REPO_ROOT / "scripts" / "training" / "README.md" |
@@ -605,6 +608,81 @@ def test_qwen3_model_guides_match_fern_and_reference_exported_recipes(): |
605 | 608 | assert not missing, f"{sphinx_path.relative_to(REPO_ROOT)} references unknown recipes: {missing}" |
606 | 609 |
|
607 | 610 |
|
| 611 | +def _normalize(text: str) -> str: |
| 612 | + """Lowercase and drop every non-alphanumeric character, so separators and case do not matter.""" |
| 613 | + return re.sub(r"[^a-z0-9]", "", text.lower()) |
| 614 | + |
| 615 | + |
| 616 | +def _supported_models_rows() -> list[tuple[str, str]]: |
| 617 | + """`(family_cell, variants_cell)` for every data row of the README Supported Models table.""" |
| 618 | + text = _read(README) |
| 619 | + assert SUPPORTED_MODELS_HEADING in text, f"README.md has no {SUPPORTED_MODELS_HEADING!r} section" |
| 620 | + body = text.split(SUPPORTED_MODELS_HEADING, 1)[1] |
| 621 | + following = re.search(r"^## ", body, flags=re.MULTILINE) |
| 622 | + table = body[: following.start()] if following else body |
| 623 | + |
| 624 | + rows: list[tuple[str, str]] = [] |
| 625 | + for line in table.splitlines(): |
| 626 | + line = line.strip() |
| 627 | + if not line.startswith("|") or "---" in line or line.count("|") < 3: |
| 628 | + continue |
| 629 | + cells = [cell.strip() for cell in line.strip("|").split("|")] |
| 630 | + if not cells[0].lower().startswith("family"): |
| 631 | + rows.append((cells[0], cells[1] if len(cells) > 1 else "")) |
| 632 | + return rows |
| 633 | + |
| 634 | + |
| 635 | +def _row_for_family(rows: list[tuple[str, str]], family: str) -> tuple[str, str] | None: |
| 636 | + """The row claiming `family`, matched by its docs link or by name, or None.""" |
| 637 | + for cell, variants in rows: |
| 638 | + if f"docs/models/{family}/" in cell: |
| 639 | + return cell, variants |
| 640 | + for cell, variants in rows: |
| 641 | + if _normalize(family) in _normalize(cell): |
| 642 | + return cell, variants |
| 643 | + return None |
| 644 | + |
| 645 | + |
| 646 | +def test_readme_supported_models_table_lists_every_shipped_model_family(): |
| 647 | + """Every documented model family with code in the tree has a Supported Models row.""" |
| 648 | + rows = _supported_models_rows() |
| 649 | + assert len(rows) >= 15, f"only {len(rows)} Supported Models rows parsed — the table layout has changed" |
| 650 | + |
| 651 | + missing = [] |
| 652 | + for family_dir in sorted(path for path in DOCS_MODELS.iterdir() if path.is_dir()): |
| 653 | + name = family_dir.name |
| 654 | + shipped = (REPO_ROOT / "examples" / "models" / name).is_dir() or ( |
| 655 | + REPO_ROOT / "src" / "megatron" / "bridge" / "models" / name |
| 656 | + ).is_dir() |
| 657 | + if shipped and _row_for_family(rows, name) is None: |
| 658 | + missing.append(f"docs/models/{name}/") |
| 659 | + assert not missing, f"documented, shipped model families have no Supported Models row: {missing}" |
| 660 | + |
| 661 | + |
| 662 | +def test_readme_supported_models_rows_name_every_shipped_model_variant(): |
| 663 | + """Every documented variant page with runnable examples is named in its family's row.""" |
| 664 | + rows = _supported_models_rows() |
| 665 | + checked, missing = [], [] |
| 666 | + for page in sorted(DOCS_MODELS.glob("*/*.md")): |
| 667 | + if page.name == "index.md": |
| 668 | + continue |
| 669 | + examples = re.findall(r"(examples/[A-Za-z0-9_./-]+)", _read(page)) |
| 670 | + if not any((REPO_ROOT / path.rstrip("/.")).exists() for path in examples): |
| 671 | + continue # nothing runnable behind this page yet, so the table cannot be expected to name it |
| 672 | + row = _row_for_family(rows, page.parent.name) |
| 673 | + if row is None: |
| 674 | + continue # already reported at family granularity |
| 675 | + checked.append(page.name) |
| 676 | + named = _normalize(" ".join(row)) |
| 677 | + tokens = [token for token in re.split(r"[-_]", page.stem) if token] |
| 678 | + if _normalize(page.stem) in named or all(_normalize(token) in named for token in tokens): |
| 679 | + continue |
| 680 | + missing.append(f"docs/models/{page.parent.name}/{page.name}") |
| 681 | + |
| 682 | + assert checked, "no documented variant page cleared the shipped gate — the docs layout has changed" |
| 683 | + assert not missing, f"documented, shipped model variants are not named in their README row: {missing}" |
| 684 | + |
| 685 | + |
608 | 686 | def test_sphinx_docs_link_out_of_tree_tutorials_as_urls(): |
609 | 687 | """Sphinx must not treat repository-root tutorials as source documents.""" |
610 | 688 | for path in SPHINX_TUTORIAL_LINK_DOCS: |
|
0 commit comments