From 17e82958cbed69682748c008bb9e67d13f1e2f0f Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Wed, 22 Jul 2026 15:42:46 +0200 Subject: [PATCH 01/13] feat: allow non root docs() --- .gitignore | 4 +- docs.bzl | 17 +- docs/how-to/faq.md | 106 ------------ docs/how-to/index.rst | 1 - .../score_metamodel/external_needs.py | 52 ++++-- .../tests/test_external_needs.py | 156 +++++++++++++++++- .../score_source_code_linker/__init__.py | 11 ++ .../tests/test_xml_parser.py | 90 ++++++++++ .../score_source_code_linker/xml_parser.py | 70 +++++++- src/incremental.py | 33 ++-- 10 files changed, 384 insertions(+), 156 deletions(-) delete mode 100644 docs/how-to/faq.md diff --git a/.gitignore b/.gitignore index 9aca50cb9..e5f5d2329 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ user.bazelrc .ruff_cache # docs build artifacts -/_build* -docs/ubproject.toml +_build/ +ubproject.toml # Vale - editorial style guide .vale.ini diff --git a/docs.bzl b/docs.bzl index d89a1ac4c..a03d54e67 100644 --- a/docs.bzl +++ b/docs.bzl @@ -127,7 +127,7 @@ def _missing_requirements(deps): fail(msg) fail("This case should be unreachable?!") -def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = None, metamodel = None): +def docs(source_dir = "docs", data = [], deps = [], scan_code = [], test_sources = [], known_good = None, metamodel = None): """Creates all targets related to documentation. By using this function, you'll get any and all updates for documentation targets in one place. @@ -137,16 +137,13 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = data: Additional data files to include in the documentation build. deps: Additional dependencies for the documentation build. scan_code: List of code targets to scan for source code links. + test_sources: Optional list of repo-relative directory paths which will be used to filter testcases for documentation generation. + When empty (default), all testcases found in `bazel-testlogs` will be used. known_good: Optional label to a "known good" JSON file for source links. metamodel: Optional label to a metamodel.yaml file. When set, the extension loads this file instead of the default metamodel shipped with score_metamodel. """ - call_path = native.package_name() - - if call_path != "": - fail("docs() must be called from the root package. Current package: " + call_path) - metamodel_data = [] metamodel_env = {} metamodel_opts = [] @@ -207,12 +204,16 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = docs_env = { "SOURCE_DIRECTORY": source_dir, + "PACKAGE_DIR": native.package_name(), "DATA": str(data), + "TEST_SOURCES": str(test_sources), "SCORE_SOURCELINKS": "$(location :sourcelinks_json)", } | metamodel_env docs_sources_env = { "SOURCE_DIRECTORY": source_dir, + "PACKAGE_DIR": native.package_name(), "DATA": str(data_with_docs_sources), + "TEST_SOURCES": str(test_sources), "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", } | metamodel_env if known_good: @@ -342,8 +343,8 @@ def _sourcelinks_json(name, srcs): See https://eclipse-score.github.io/docs-as-code/main/how-to/source_to_doc_links.html Args: - name: Name of the target - srcs: Source files to scan for traceability tags + name: Name of the target. + srcs: Source files to scan for traceability tags. """ output_file = name + ".json" diff --git a/docs/how-to/faq.md b/docs/how-to/faq.md deleted file mode 100644 index dbfc5bdb0..000000000 --- a/docs/how-to/faq.md +++ /dev/null @@ -1,106 +0,0 @@ - - -# FAQ - -*docs-as-code is the S-CORE tool for building documentation, defining requirements and -verifying compliance.* - -In this document you will find answers to frequently asked questions regarding -docs-as-code and its usage. - - -## Why is docs-as-code so slow? - -The performance of docs-as-code has significantly improved in version 1.0.0 and later. - -## IDE support (auto completion, metamodel checks, preview, LSP capabilities) - -Since 1.0.0 IDE support works via esbonio. You'll need to install the esbonio extension -for your IDE (e.g., VSCode, PyCharm, etc.). Your repository must also be configured for -esbonio (settings.json). - -If this does not work, please use the live preview feature: `bazel run //:live_preview`. -This provides immediate metamodel feedback (although only on the console) and -IDE-agnostic preview capabilities. - -Another option is to use the [ubCode](https://ubcode.useblocks.com) extension for -VS Code. It provides lightning-fast linting, formatting, previewing, navigation and -analytical features for large Sphinx and Sphinx-Needs projects. -The `ubproject.toml` configuration file gets generated automatically when a Sphinx -build is running once. The file can safely be committed to the repository to enable -IDE support without requiring a Sphinx build first. - -### Esbonio - -Known issues: -* Python is required at startup, which is a problem for any Python-based LSP. We are - working to improve this by providing a devcontainer with Python preinstalled. - Additionally, we have submitted a feature request for Esbonio to handle Python - installation. - - For now please run `bazel run //:ide_support` and restart VS Code. - - -### ubc - -Currently, ubc is not aware of our metamodel. As a result, checks and auto-completion -features are not available. - -We plan to explore improvements in this area in the future together with useblocks. -These features are on the immediate roadmap. - - -## Do we need to write custom Python code for every Metamodel check? -With our current approach, allowed attributes and links for Needs are defined within the -`metamodel.yml` file. If the check can be fully described there (e.g., process -requirements are only allowed to link to stakeholder requirements), no custom code is -needed. It is also not necessary to write individual tests for every single check -performed by the metamodel. - -Only a few very specific checks require custom Python code beyond the generic metamodel -capabilities. These are cases that cannot be addressed by generic metamodel approaches -in any tool. For example: "the middle part of certain IDs must match the directory name -of the file." - - -## How can I be sure that the Metamodel does what I want it to do? -We use *examples* written in reStructuredText (rst) to verify that the metamodel has -been configured as intended. - -Metamodel checks are verified through standard testing practices, like any other code. -The examples mentioned above are helpful, but they are only examples. They are not -mandatory for verification of the metamodel checks. - - - -## Sphinx and safety -It is important to distinguish between metamodel checks and HTML rendering. - -Metamodel checks can be verified / qualified without Sphinx. - -If the renderer is safety-relevant, then qualification of Sphinx (and Sphinx Needs) is -required. This is currently under evaluation by the process team (@aschemmel-tech). - - -## What about versioning of requirements? -We are currently discussing possible implementations to enable linking to specific -versions of requirements (e.g. `implements: req-5@v3.0.0`). - - -## Sphinx traceability -It is possible to link requirements from other requirements, from source code, or from -tests (tests within the next days). - -### What about bazel targets? -Bazel targets are not involved in traceability (currently not required by process). diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 7cae52036..cb2d38a29 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -25,7 +25,6 @@ Here you find practical guides on how to use docs-as-code. get_started setup write_docs - faq other_modules dashboards_and_quality_gates source_to_doc_links diff --git a/src/extensions/score_metamodel/external_needs.py b/src/extensions/score_metamodel/external_needs.py index a39736661..980dfe1c2 100644 --- a/src/extensions/score_metamodel/external_needs.py +++ b/src/extensions/score_metamodel/external_needs.py @@ -31,10 +31,16 @@ class ExternalNeedsSource: bazel_module: str path_to_target: str target: str + # True for a same-repo mount (`//pkg:needs_json`), whose runfiles live under + # `_main/…`. False for a cross-module mount (`@repo//…:needs_json`), whose + # runfiles live under `{bazel_module}+/…`. + is_local: bool = False def _parse_bazel_external_need(s: str) -> ExternalNeedsSource | None: - if not s.startswith("@"): + is_cross_module = s.startswith("@") + is_local = s.startswith("//") + if not is_cross_module and not is_local: # Local need, not external needs return None @@ -46,16 +52,28 @@ def _parse_bazel_external_need(s: str) -> ExternalNeedsSource | None: ":", 1 ) # @score_process//:needs_json => [@score_process//, needs_json] repo, path_to_target = repo_and_path.split("//", 1) - repo = repo.lstrip("@") + repo = repo.lstrip("@") # empty for same-repo `//pkg:needs_json` - if path_to_target == "" and target in ("needs_json", "docs_sources"): + if target in ("needs_json", "docs_sources"): return ExternalNeedsSource( - bazel_module=repo, path_to_target=path_to_target, target=target + bazel_module=repo, + path_to_target=path_to_target, + target=target, + is_local=is_local, ) # Unknown data target. Probably not a needs.json file. return None +def _runfiles_module_dir(e: ExternalNeedsSource) -> str: + """Runfiles top-level directory holding this source's package tree. + + Same-repo mounts are staged under `_main/…`; cross-module mounts under the + module's bzlmod canonical name `{bazel_module}+/…`. + """ + return "_main" if e.is_local else f"{e.bazel_module}+" + + def parse_external_needs_sources_from_DATA(v: str) -> list[ExternalNeedsSource]: if v in ["[]", ""]: return [] @@ -150,7 +168,13 @@ def get_external_needs_source(external_needs_source: str) -> list[ExternalNeedsS def add_external_needs_json(e: ExternalNeedsSource, config: Config): - json_file_raw = f"{e.bazel_module}+/{e.target}/_build/needs/needs.json" + json_file_raw = ( + Path(_runfiles_module_dir(e)) + / e.path_to_target + / e.target + / "_build/needs/needs.json" + ) + r = get_runfiles_dir() json_file = r / json_file_raw logger.debug(f"External needs.json: {json_file}") @@ -176,22 +200,28 @@ def add_external_needs_json(e: ExternalNeedsSource, config: Config): def add_external_docs_sources(e: ExternalNeedsSource, config: Config): # Note that bazel does NOT write the files under e.target! - # {e.bazel_module}+ matches the original git layout! + # The runfiles layout mirrors the original git layout: same-repo mounts live + # under `_main/…`, cross-module mounts under `{e.bazel_module}+/…` + # (see _runfiles_module_dir). r = get_runfiles_dir() if "ide_support.runfiles" in str(r): logger.error("Combo builds are currently only supported with Bazel.") return - docs_source_path = Path(r) / f"{e.bazel_module}+" + docs_source_path = Path(r) / _runfiles_module_dir(e) / e.path_to_target + + # A cross-module root mount keeps its module name as the collection key + # (unchanged). Sub-package / same-repo mounts disambiguate via the path. + key = "/".join(c for c in (e.bazel_module, e.path_to_target) if c) or "_main" if "collections" not in config: config.collections = {} - config.collections[e.bazel_module] = { + config.collections[key] = { "driver": "symlink", "source": str(docs_source_path), - "target": e.bazel_module, + "target": key, } - logger.info(f"Added external docs source: {docs_source_path} -> {e.bazel_module}") + logger.info(f"Added external docs source: {docs_source_path} -> {key}") def connect_external_needs(app: Sphinx, config: Config): @@ -204,8 +234,6 @@ def connect_external_needs(app: Sphinx, config: Config): config.needs_external_needs = [] for e in external_needs: - assert not e.path_to_target # path_to_target is always empty - if e.target == "needs_json": add_external_needs_json(e, app.config) elif e.target == "docs_sources": diff --git a/src/extensions/score_metamodel/tests/test_external_needs.py b/src/extensions/score_metamodel/tests/test_external_needs.py index a9db5052e..a73ff601f 100644 --- a/src/extensions/score_metamodel/tests/test_external_needs.py +++ b/src/extensions/score_metamodel/tests/test_external_needs.py @@ -36,13 +36,54 @@ def test_empty_list(): assert parse_external_needs_sources_from_DATA("[]") == [] -def test_external_str_does_not_start_with_at(): - assert get_external_needs_source('["noatrepo//foo/bar:baz"]') == [] +def test_external_str_is_neither_at_nor_slash(): + # Labels that start with neither "@" nor "//" are not bazel needs sources. + assert get_external_needs_source('["noatrepo/foo/bar:baz"]') == [] -def test_single_entry_with_path(): +def test_same_repo_entry_with_path(): + # A same-repo `//pkg:needs_json` mount now parses as a local source that + # carries its sub-package path. + result = parse_external_needs_sources_from_DATA('["//foo/bar:needs_json"]') + assert result == [ + ExternalNeedsSource( + bazel_module="", + path_to_target="foo/bar", + target="needs_json", + is_local=True, + ) + ] + + +def test_same_repo_root_entry(): + result = parse_external_needs_sources_from_DATA('["//:needs_json"]') + assert result == [ + ExternalNeedsSource( + bazel_module="", + path_to_target="", + target="needs_json", + is_local=True, + ) + ] + + +def test_cross_module_sub_package_entry(): + # A cross-module sub-package target now parses (previously rejected) and + # keeps its path so the runfiles path can be built correctly. + result = parse_external_needs_sources_from_DATA('["@repo//foo/bar:needs_json"]') + assert result == [ + ExternalNeedsSource( + bazel_module="repo", + path_to_target="foo/bar", + target="needs_json", + is_local=False, + ) + ] + + +def test_single_entry_with_path_non_target(): + # A target that is not needs_json / docs_sources is not reported as an external needs source. result = parse_external_needs_sources_from_DATA('["@repo//foo/bar:baz"]') - # IF a target has a path, it will not be reported as external needs assert result == [] @@ -74,6 +115,7 @@ def test_multiple_entries(): def test_multiple_entries_2(): + # Both targets are named "needs_json" but one is a sub-package target, so the path is preserved. result = parse_external_needs_sources_from_DATA( '["@repo1//:needs_json", "@repo2//path:needs_json"]' ) @@ -81,7 +123,13 @@ def test_multiple_entries_2(): assert result == [ ExternalNeedsSource( bazel_module="repo1", path_to_target="", target="needs_json" - ) + ), + ExternalNeedsSource( + bazel_module="repo2", + path_to_target="path", + target="needs_json", + is_local=False, + ), ] @@ -120,6 +168,40 @@ def test_add_external_needs_json_appends_entry( assert Path(entry["json_path"]) == json_path +def test_add_external_needs_json_appends_entry_local( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A same-repo mount resolves under `_main///…`.""" + e = ExternalNeedsSource( + bazel_module="", + target="needs_json", + path_to_target="src/tests/e2e/external_needs/producer", + is_local=True, + ) + config = Config() + config.needs_external_needs = [] + + runfiles_dir = tmp_path + rel_json = Path( + "_main/src/tests/e2e/external_needs/producer/needs_json/_build/needs/needs.json" + ) + json_path = runfiles_dir / rel_json + json_path.parent.mkdir(parents=True, exist_ok=True) + json_path.write_text( + json.dumps({"project_url": "https://example.test/local"}), encoding="utf-8" + ) + + monkeypatch.setattr(ext_needs, "get_runfiles_dir", lambda: runfiles_dir) + + add_external_needs_json(e, config) + + assert config.needs_external_needs is not None + assert len(config.needs_external_needs) == 1 + entry = config.needs_external_needs[0] + assert entry["base_url"] == "https://example.test/local/main" + assert Path(entry["json_path"]) == json_path + + def test_add_external_needs_json_missing_file_keeps_list_empty( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -161,6 +243,70 @@ def test_add_external_docs_sources_adds_collection( assert entry["target"] == "third_party_docs" +def test_add_external_docs_sources_local_sub_package( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A same-repo sub-package `docs_sources` mount resolves under `_main/`. + + Mirrors test_add_external_needs_json_appends_entry_local but for the + `docs_sources` path: the local branch stages under `_main/…`, appends + `path_to_target`, and the collection key falls through the + `bazel_module + path_to_target` join (bazel_module empty for a local mount). + """ + e = ExternalNeedsSource( + bazel_module="", + target="docs_sources", + path_to_target="src/tests/e2e/external_needs/producer", + is_local=True, + ) + config = Config() + config.collections = {} + + monkeypatch.setattr(ext_needs, "get_runfiles_dir", lambda: tmp_path) + + add_external_docs_sources(e, config) + + assert config.collections is not None + key = "src/tests/e2e/external_needs/producer" + assert key in config.collections + entry = config.collections[key] + assert entry["driver"] == "symlink" + assert entry["source"] == str( + tmp_path / "_main" / "src/tests/e2e/external_needs/producer" + ) + assert entry["target"] == key + + +def test_add_external_docs_sources_local_root_key_fallback( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A same-repo root `docs_sources` mount falls back to the `_main` key. + + With both bazel_module and path_to_target empty, the key join yields "" and + the `or "_main"` fallback names the collection, while the source stays at the + `_main` runfiles root. + """ + e = ExternalNeedsSource( + bazel_module="", + target="docs_sources", + path_to_target="", + is_local=True, + ) + config = Config() + config.collections = {} + + monkeypatch.setattr(ext_needs, "get_runfiles_dir", lambda: tmp_path) + + add_external_docs_sources(e, config) + + assert config.collections is not None + assert "_main" in config.collections + entry = config.collections["_main"] + assert entry["driver"] == "symlink" + assert entry["source"] == str(tmp_path / "_main") + assert entry["target"] == "_main" + + def test_add_external_docs_sources_ide_support_returns_without_changes( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 644a41324..e67fc4544 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -350,6 +350,17 @@ def setup(app: Sphinx) -> dict[str, str | bool]: types=bool, description="If True, render links as plain text without GitHub URLs (useful for Bazel sandbox builds)", ) + app.add_config_value( + "testcase_source_dirs", + default="", + rebuild="env", + types=str, + description=( + "str(list) of repo-relative directory paths. When set, the test-code-linker " + "only builds testcase needs for testcases whose source file lives under one of " + "these directories. Empty means no filtering (scan the whole workspace)." + ), + ) setup_once(app) return { diff --git a/src/extensions/score_source_code_linker/tests/test_xml_parser.py b/src/extensions/score_source_code_linker/tests/test_xml_parser.py index 83163a557..00c802e50 100644 --- a/src/extensions/score_source_code_linker/tests/test_xml_parser.py +++ b/src/extensions/score_source_code_linker/tests/test_xml_parser.py @@ -377,6 +377,96 @@ def _fake_add_external_need(**kwargs: object) -> object: ) +# ╭───────────────────────────────────────────────────────────────╮ +# │ Tests for testcase_source_dirs scoping (docs(test_sources=)) │ +# ╰───────────────────────────────────────────────────────────────╯ + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_is_testcase_in_scope_empty_allow_list_is_unfiltered(): + """An empty allow-list disables filtering: everything is in scope.""" + assert xml_parser.is_testcase_in_scope("src/tests/e2e/basic/x.py", []) is True + # Even a testcase without a `file` attribute is in scope when unfiltered. + assert xml_parser.is_testcase_in_scope(None, []) is True + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_is_testcase_in_scope_positive_match(): + """A file located under an allowed dir is in scope (positive match).""" + assert ( + xml_parser.is_testcase_in_scope("src/tests/e2e/basic/x.py", ["src/tests/e2e"]) + is True + ) + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_is_testcase_in_scope_not_under_any_allowed_dir(): + """A file outside every allowed dir is out of scope.""" + assert xml_parser.is_testcase_in_scope("src/other/x.py", ["src/tests/e2e"]) is False + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_is_testcase_in_scope_none_file_with_allow_list(): + """With a non-empty allow-list a testcase without a `file` is out of scope.""" + assert xml_parser.is_testcase_in_scope(None, ["src/tests/e2e"]) is False + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_is_testcase_in_scope_no_string_prefix_match(): + """Matching is component-wise, not a raw string-prefix match.""" + # "basic" starts with the allowed "ba" as a string, but not as a path + # component, so this must NOT be considered in scope. + assert ( + xml_parser.is_testcase_in_scope( + "src/tests/e2e/basic/x.py", ["src/tests/e2e/ba"] + ) + is False + ) + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_parse_testcase_source_dirs(): + """`str(list)` config values are parsed into a list of paths.""" + assert xml_parser.parse_testcase_source_dirs("[]") == [] + assert xml_parser.parse_testcase_source_dirs("") == [] + assert xml_parser.parse_testcase_source_dirs('["a", "b/c"]') == ["a", "b/c"] + + +@add_test_properties( + partially_verifies=["tool_req__docs_test_link_testcase"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_parse_testcase_source_dirs_non_list_raises(): + """A malformed (non-list) config value fails loudly instead of downstream.""" + with pytest.raises(ValueError, match="must be a list"): + xml_parser.parse_testcase_source_dirs('"not a list"') + + # ─────────────[ Boilerplate generated by CoPilot ]───────────── diff --git a/src/extensions/score_source_code_linker/xml_parser.py b/src/extensions/score_source_code_linker/xml_parser.py index 9483b6474..c40443ce6 100644 --- a/src/extensions/score_source_code_linker/xml_parser.py +++ b/src/extensions/score_source_code_linker/xml_parser.py @@ -22,6 +22,7 @@ import contextlib import hashlib import itertools +import json import os import xml.etree.ElementTree as ET from pathlib import Path @@ -54,6 +55,38 @@ logger.setLevel("DEBUG") +def parse_testcase_source_dirs(v: str) -> list[str]: + """Parse the `testcase_source_dirs` config value into a list of paths. + + The value arrives as a `str(list)` produced by Starlark (double-quoted, i.e. + valid JSON), mirroring how `external_needs_source` is parsed in + `score_metamodel.external_needs.parse_external_needs_sources_from_DATA`. + """ + if v in ("[]", ""): + return [] + parsed = json.loads(v) + if not isinstance(parsed, list): + raise ValueError( + f"testcase_source_dirs must be a list, got {type(parsed).__name__}: {v!r}" + ) + return parsed + + +def is_testcase_in_scope(test_file: str | None, allowed_dirs: list[str]) -> bool: + """Return True if the testcase should be turned into a need. + + An empty `allowed_dirs` disables filtering (everything is in scope). Otherwise + a testcase is in scope only if it has a `file` attribute that is located under + one of the allowed (repo-relative) directories. + """ + if not allowed_dirs: + return True + if not test_file: + return False + file_path = Path(test_file) + return any(file_path.is_relative_to(allowed) for allowed in allowed_dirs) + + def clean_test_file_name(raw_filepath: Path) -> Path: """ incoming path: @@ -163,7 +196,9 @@ def parse_properties(case_properties: dict[str, Any], properties: Element): return case_properties -def read_test_xml_file(file: Path) -> tuple[list[DataOfTestCase], list[str], list[str]]: +def read_test_xml_file( + file: Path, allowed_dirs: list[str] | None = None +) -> tuple[list[DataOfTestCase], list[str], list[str]]: """ Reading & parsing the test.xml files into TestCaseNeeds @@ -172,6 +207,7 @@ def read_test_xml_file(file: Path) -> tuple[list[DataOfTestCase], list[str], lis - list[TestCaseNeed] - list[str] => Testcase Names that did not have the required properties. """ + allowed_dirs = allowed_dirs or [] test_case_needs: list[DataOfTestCase] = [] non_prop_tests: list[str] = [] missing_prop_tests: list[str] = [] @@ -180,6 +216,15 @@ def read_test_xml_file(file: Path) -> tuple[list[DataOfTestCase], list[str], lis md = get_metadata_from_test_path(file) for testsuite in root.findall("testsuite"): for testcase in testsuite.findall("testcase"): + test_file = testcase.get("file") + # When testcase_source_dirs is configured, only testcases whose source + # file lives under one of the allowed directories are turned into needs. + # Out-of-scope testcases are skipped here (before the mandatory + # name/classname assertion below) so they are neither added as needs nor + # cached. Skipping early also keeps a scoped build robust against + # malformed test.xml files emitted by unrelated, out-of-scope tests. + if not is_testcase_in_scope(test_file, allowed_dirs): + continue case_properties = {} testcasename = testcase.get("name", "") testclassname = testcase.get("classname", "") @@ -192,7 +237,6 @@ def read_test_xml_file(file: Path) -> tuple[list[DataOfTestCase], list[str], lis testname = "__".join([testcn, testcasename]) else: testname = testcasename - test_file = testcase.get("file") line = testcase.get("line") # ╭──────────────────────────────────────╮ @@ -303,8 +347,18 @@ def run_xml_parser(app: Sphinx, env: BuildEnvironment): testlogs_dir = find_test_folder() if testlogs_dir is None: return + allowed_dirs = parse_testcase_source_dirs( + getattr(app.config, "testcase_source_dirs", "") + ) + if allowed_dirs: + logger.info( + f"Scoping testcase needs to source dirs: {allowed_dirs}", + type="score_source_code_linker", + ) xml_file_paths = find_xml_files(testlogs_dir) - test_case_needs = build_test_needs_from_files(app, env, xml_file_paths) + test_case_needs = build_test_needs_from_files( + app, env, xml_file_paths, allowed_dirs + ) # Saving the test case needs for cache logger.info( f"Saving {len(test_case_needs)} test case needs to the cache `score_testcaseneeds_cache.json` in _build/." @@ -324,12 +378,18 @@ def run_xml_parser(app: Sphinx, env: BuildEnvironment): def build_test_needs_from_files( - app: Sphinx, enw_: BuildEnvironment, xml_paths: list[Path] + app: Sphinx, + enw_: BuildEnvironment, + xml_paths: list[Path], + allowed_dirs: list[str] | None = None, ) -> list[DataOfTestCase]: """ Reading in all test.xml files, and building 'testcase' external need objects out of them. + When `allowed_dirs` is non-empty, only testcases whose source file lives under one + of those repo-relative directories are turned into needs (see read_test_xml_file). + Returns: - list[TestCaseNeed] """ @@ -337,7 +397,7 @@ def build_test_needs_from_files( for file in xml_paths: # Last value can be ignored. The 'is_valid' function already prints infos test_cases, tests_missing_all_props, tests_missing_some_props = ( - read_test_xml_file(file) + read_test_xml_file(file, allowed_dirs) ) non_prop_tests = ", ".join(n for n in tests_missing_all_props) if non_prop_tests: diff --git a/src/incremental.py b/src/incremental.py index c6f4cc8e2..0519dff63 100644 --- a/src/incremental.py +++ b/src/incremental.py @@ -96,25 +96,25 @@ def update_module_hash(build_dir: Path, sentinel_files: list[Path]) -> None: logger.info("Waiting for client to connect on port: " + str(args.debug_port)) debugpy.wait_for_client() - workspace = os.getenv("BUILD_WORKSPACE_DIRECTORY") - if workspace: - workspace += "/" - else: - workspace = "" + ws_root = Path(os.getenv("BUILD_WORKSPACE_DIRECTORY", "")) + # Docs source and output are resolved relative to the package where docs() + # was called. For the root BUILD, PACKAGE_DIR == "" so this is unchanged. + package_dir = ws_root / os.environ.get("PACKAGE_DIR", "") - build_dir = Path(workspace + "_build") + build_dir = package_dir / "_build" sentinel_files = [ - Path(workspace + "MODULE.bazel"), - Path(workspace + "MODULE.bazel.lock"), - Path(workspace + "BUILD"), + ws_root / "MODULE.bazel", + ws_root / "MODULE.bazel.lock", + package_dir / "BUILD", ] clean_builddir_if_stale(build_dir, sentinel_files) - warning_file = Path(workspace + "_build/warnings.txt") + warning_file = build_dir / "warnings.txt" + source_directory = get_env("SOURCE_DIRECTORY") base_arguments = [ - workspace + get_env("SOURCE_DIRECTORY"), - workspace + "_build", + str(package_dir / source_directory), + str(build_dir), "--warning-file", str(warning_file), "-W", # treat warning as errors @@ -123,13 +123,14 @@ def update_module_hash(build_dir: Path, sentinel_files: list[Path]) -> None: "--jobs", "auto", f"--define=external_needs_source={get_env('DATA')}", + f"--define=testcase_source_dirs={os.environ.get('TEST_SOURCES', '[]')}", ] metamodel_yaml = os.environ.get("SCORE_METAMODEL_YAML", "") if metamodel_yaml: # Normalize to absolute path so it resolves correctly after Sphinx changes cwd if not os.path.isabs(metamodel_yaml): - metamodel_yaml = workspace + metamodel_yaml + metamodel_yaml = str(ws_root / metamodel_yaml) metamodel_yaml = os.path.abspath(metamodel_yaml) base_arguments.append(f"--define=score_metamodel_yaml={metamodel_yaml}") @@ -141,16 +142,14 @@ def update_module_hash(build_dir: Path, sentinel_files: list[Path]) -> None: base_arguments.append(f"-A=github_user={github_user}") base_arguments.append(f"-A=github_repo={github_repo}") base_arguments.append("-A=github_version=main") - base_arguments.append(f"-A=doc_path={get_env('SOURCE_DIRECTORY')}") + base_arguments.append(f"-A=doc_path={package_dir / source_directory}") if os.getenv("KNOWN_GOOD_JSON"): base_arguments.append(f"--define=KNOWN_GOOD_JSON={get_env('KNOWN_GOOD_JSON')}") action = get_env("ACTION") if action == "live_preview": - Path(workspace + "/_build/score_source_code_linker_cache.json").unlink( - missing_ok=True - ) + (build_dir / "score_source_code_linker_cache.json").unlink(missing_ok=True) sphinx_autobuild_main( base_arguments + [ From 55f43953341478343afccb55eba04f4d187d0d9c Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Wed, 22 Jul 2026 16:32:32 +0200 Subject: [PATCH 02/13] fix formatting --- .../tests/rst/options/test_options_failure_effect.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/tests/rst/options/test_options_failure_effect.rst b/src/extensions/score_metamodel/tests/rst/options/test_options_failure_effect.rst index 9d0b9a1fc..6ef600c20 100644 --- a/src/extensions/score_metamodel/tests/rst/options/test_options_failure_effect.rst +++ b/src/extensions/score_metamodel/tests/rst/options/test_options_failure_effect.rst @@ -158,7 +158,7 @@ .. feat_saf_fmea:: Empty failure_effect :id: feat_saf_fmea__test__empty_failure_effect :fault_id: FMEA_05 - :failure_effect: + :failure_effect: :sufficient: yes :status: valid :expect: feat_saf_fmea__test__empty_failure_effect: is missing required attribute: `failure_effect`. From 2e977f4bd07ffba197d205acc1d54b9198b463eb Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 13:31:07 +0200 Subject: [PATCH 03/13] test: add tests for docs-bzl level --- .github/workflows/test.yml | 13 +- src/tests/README.md | 101 +----- src/tests/docs_bzl/README.md | 22 ++ src/tests/docs_bzl/fixtures/basic/BUILD | 19 + .../docs_bzl/fixtures/basic/docs/conf.py | 16 + .../docs_bzl/fixtures/basic/docs/index.rst | 16 + .../fixtures/external_needs/consumer/BUILD | 23 ++ .../external_needs/consumer/docs/conf.py | 28 ++ .../external_needs/consumer/docs/index.rst | 19 + .../consumer/docs/metamodel.yaml | 29 ++ .../fixtures/external_needs/producer/BUILD | 22 ++ .../external_needs/producer/docs/conf.py | 38 ++ .../external_needs/producer/docs/index.rst | 20 ++ .../producer/docs/metamodel.yaml | 29 ++ .../fixtures/metamodel_violation/BUILD | 22 ++ .../fixtures/metamodel_violation/docs/conf.py | 27 ++ .../metamodel_violation/docs/index.rst | 19 + .../metamodel_violation/docs/metamodel.yaml | 30 ++ src/tests/docs_bzl/helpers.py | 142 ++++++++ src/tests/docs_bzl/test_docs_bzl.py | 74 ++++ src/tests/downstream_compatibility/BUILD | 26 ++ src/tests/downstream_compatibility/README.md | 117 ++++++ .../conftest.py | 0 .../test_consumer.py | 12 +- src/tests/test-levels.drawio.svg | 332 ++++++++++++++++++ 25 files changed, 1096 insertions(+), 100 deletions(-) create mode 100644 src/tests/docs_bzl/README.md create mode 100644 src/tests/docs_bzl/fixtures/basic/BUILD create mode 100644 src/tests/docs_bzl/fixtures/basic/docs/conf.py create mode 100644 src/tests/docs_bzl/fixtures/basic/docs/index.rst create mode 100644 src/tests/docs_bzl/fixtures/external_needs/consumer/BUILD create mode 100644 src/tests/docs_bzl/fixtures/external_needs/consumer/docs/conf.py create mode 100644 src/tests/docs_bzl/fixtures/external_needs/consumer/docs/index.rst create mode 100644 src/tests/docs_bzl/fixtures/external_needs/consumer/docs/metamodel.yaml create mode 100644 src/tests/docs_bzl/fixtures/external_needs/producer/BUILD create mode 100644 src/tests/docs_bzl/fixtures/external_needs/producer/docs/conf.py create mode 100644 src/tests/docs_bzl/fixtures/external_needs/producer/docs/index.rst create mode 100644 src/tests/docs_bzl/fixtures/external_needs/producer/docs/metamodel.yaml create mode 100644 src/tests/docs_bzl/fixtures/metamodel_violation/BUILD create mode 100644 src/tests/docs_bzl/fixtures/metamodel_violation/docs/conf.py create mode 100644 src/tests/docs_bzl/fixtures/metamodel_violation/docs/index.rst create mode 100644 src/tests/docs_bzl/fixtures/metamodel_violation/docs/metamodel.yaml create mode 100644 src/tests/docs_bzl/helpers.py create mode 100644 src/tests/docs_bzl/test_docs_bzl.py create mode 100644 src/tests/downstream_compatibility/BUILD create mode 100644 src/tests/downstream_compatibility/README.md rename src/tests/{ => downstream_compatibility}/conftest.py (100%) rename src/tests/{ => downstream_compatibility}/test_consumer.py (95%) create mode 100644 src/tests/test-levels.drawio.svg diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7fab5c3d..61ed36d10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,7 @@ jobs: uses: step-security/harden-runner@v2.18.0 with: egress-policy: audit + - name: Checkout repository (Handle all events) uses: actions/checkout@v4.2.2 with: @@ -38,12 +39,16 @@ jobs: repository-cache: true bazelisk-cache: true cache-save: ${{ github.event_name == 'push' }} - - name: Run test targets + + - name: Run docs_bzl tests + run: pytest -vv src/tests/docs_bzl + + - name: Run bazel test targets run: | bazel run --lockfile_mode=error //:ide_support bazel test --lockfile_mode=error //... - - name: Prepare bundled consumer report + - name: Prepare bundled test report if: always() # Creating tests-report directory # Follow Symlinks via '-L' to copy correctly @@ -52,9 +57,9 @@ jobs: mkdir -p tests-report rsync -amL --include='*/' --include='test.xml' --include='test.log' --exclude='*' bazel-testlogs/ tests-report/ - - name: Upload bundled consumer report + - name: Upload bundled test report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: tests-report path: tests-report diff --git a/src/tests/README.md b/src/tests/README.md index 05e0d85c1..060487730 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -13,105 +13,24 @@ # Docs-As-Code Consumer Tests -This test validates that changes to the docs-as-code system don't break downstream consumers. -It tests both local changes and git-based overrides against real consumer repositories. +# Testing -## Use in CI +![test levels in S-CORE docs-as-code](test-levels.drawio.svg) -If you want to start the consumer tests on a PR inside `docs-as-code`, then all you have to do is comment -`/consumer-test` on the PR and this should trigger them. +## end-to-end tests -## Quick Start +*Also known as system, product, or black-box tests.* -```bash -# Create the virtual environment -bazel run //:ide_support - -# Run with std. configuration -.venv_docs/bin/python -m pytest -s src/tests - -# Run with more verbose output (up to -vvv) -.venv_docs/bin/python -m pytest -s -v src/tests - -# Run specific repositories only -.venv_docs/bin/python -m pytest -s src/tests --repo=score - -# Disable the persistent cache -.venv_docs/bin/python -m pytest -s src/tests --disable-cache - -# Or combine both options -.venv_docs/bin/python -m pytest -s src/tests --disable-cache --repo=score -``` +This directory contains end-to-end tests, namely: -## Verbosity Levels +- `docs_bzl`: tests based on the public `docs()` macro. Must be executed via plain pytest (run via `.venv_docs`) as they execute real `bazel run`/`bazel build` internally. See [docs_bzl/README.md](docs_bzl/README.md) for details. -The test suite supports different levels of output detail: +- `downstream_compatibility`: *(formerly consumer tests)* Tests local changes and Git-based overrides against real consumer repositories. This provides broad, intentionally less-controlled coverage and helps detect breaking changes in the docs-as-code system before they affect downstream consumers. -- **No flags**: Basic test results and summary table -- **`-v`**: Shows detailed warnings breakdown by logger type -- **`-vv`**: Adds full stdout/stderr output from build commands -- **`-vvv`**: Streams build output in real-time (useful for debugging hanging builds) +### Targets -## Command Line Options +You can query targets in this directory with: -### `--disable-cache` -Disabled persistent caching for clean testing cycle. - -**What the test normaly do:** -- Uses `~/.cache/docs_as_code_consumer_tests` instead of temporary directories -- Reuses cloned repositories between runs (with git updates) -- Significantly speeds up subsequent test runs - -**This option disables the above mentioned behaviour and clones the repositories fresh** - -**When to use:** During development when you need to ensure testing is done on a fresh env. - -### `--repo` -Filters which repositories to test. - -**Usage:** ```bash -# Test only the 'score' repository -.venv_docs/bin/python -m pytest -s src/tests --repo=score - -# Test multiple repositories -.venv_docs/bin/python -m pytest -s src/tests --repo=score,module_template - -# Invalid repo names fall back to testing all repositories -.venv_docs/bin/python -m pytest -s src/tests --repo=nonexistent -``` - -**Available repositories:** Check `REPOS_TO_TEST` in the test file for current list. - -## Currently tested repositories - -- [score](https://github.com/eclipse-score/score) -- [process_description](https://github.com/eclipse-score/process_description) -- [module_template](https://github.com/eclipse-score/module_template) - -## What Gets Tested - -For each repository, the test: -1. Clones the consumer repository -2. Tests with **local override** (your current changes) -3. Tests with **git override** (current commit from remote) -4. Runs build commands and test commands -5. Analyzes warnings and build success - -## Example Development Workflow - -```bash -# Create the virtual environment -bazel run //:ide_support - -# First run - clones everything fresh -.venv_docs/bin/python -m pytest -s -v src/tests --repo=score - -# Make changes to docs-as-code... - -# Subsequent runs - much faster due to caching -.venv_docs/bin/python -m pytest -s -v src/tests --repo=score - -# Final validation - test all repos without cache -.venv_docs/bin/python -m pytest -s -v src/tests --disable-cache +bazel query 'kind(".*_test", //src/tests/...)' ``` diff --git a/src/tests/docs_bzl/README.md b/src/tests/docs_bzl/README.md new file mode 100644 index 000000000..b483b4764 --- /dev/null +++ b/src/tests/docs_bzl/README.md @@ -0,0 +1,22 @@ + + +# `docs()` end-to-end tests + +These tests run **outside** Bazel with pytest and drive the public `docs()` +macro through real `bazel run` / `bazel build` commands against small fixture +packages in this directory. They test exactly what a user runs. + +Note that these tests run `bazel` commands, so they are slow. They need to be executed +sequentially. Use sparingly. + +Run via: + + pytest src/tests/docs_bzl -vv + +*(no venv is required)* diff --git a/src/tests/docs_bzl/fixtures/basic/BUILD b/src/tests/docs_bzl/fixtures/basic/BUILD new file mode 100644 index 000000000..a67698dba --- /dev/null +++ b/src/tests/docs_bzl/fixtures/basic/BUILD @@ -0,0 +1,19 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +docs( + source_dir = "docs", + test_sources = ["src/tests/docs_bzl/fixtures/basic"], +) diff --git a/src/tests/docs_bzl/fixtures/basic/docs/conf.py b/src/tests/docs_bzl/fixtures/basic/docs/conf.py new file mode 100644 index 000000000..9b0fd6e88 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/basic/docs/conf.py @@ -0,0 +1,16 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +project = "Basic Test" +project_url = "https://github.com/eclipse-score/docs-as-code" +extensions = ["score_sphinx_bundle"] diff --git a/src/tests/docs_bzl/fixtures/basic/docs/index.rst b/src/tests/docs_bzl/fixtures/basic/docs/index.rst new file mode 100644 index 000000000..cc8afd54b --- /dev/null +++ b/src/tests/docs_bzl/fixtures/basic/docs/index.rst @@ -0,0 +1,16 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +Basic Test +========== diff --git a/src/tests/docs_bzl/fixtures/external_needs/consumer/BUILD b/src/tests/docs_bzl/fixtures/external_needs/consumer/BUILD new file mode 100644 index 000000000..977e96715 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/consumer/BUILD @@ -0,0 +1,23 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +# Mounts the same-repo producer's needs.json so the :need: link in index.rst can +# resolve. test_sources scopes the test-code-linker to this (test-less) package, +# avoiding the workspace-wide bazel-testlogs scan during the html build. +docs( + source_dir = "docs", + data = ["//src/tests/docs_bzl/fixtures/external_needs/producer:needs_json"], + test_sources = ["src/tests/docs_bzl/fixtures/external_needs/consumer"], +) diff --git a/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/conf.py b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/conf.py new file mode 100644 index 000000000..9e5c5d250 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/conf.py @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import os + +project = "External Needs Consumer" +project_url = "https://example.invalid/external-needs-consumer" +extensions = ["score_sphinx_bundle"] + +# FIXME: +# sphinx-needs refuses to add an external need whose type is not registered in +# this project's needs_types, so the consumer must declare the producer's +# `test_req` type. score_metamodel's own checks skip external needs, so the +# id/parts rules never run here. Loaded via the score_metamodel_yaml config +# value because docs(metamodel=...) does not reach the build target. +score_metamodel_yaml = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "metamodel.yaml" +) diff --git a/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/index.rst b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/index.rst new file mode 100644 index 000000000..17f7631a6 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/index.rst @@ -0,0 +1,19 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +External Needs Consumer +======================= + +This document links to a need defined in the producer package: +:need:`test_req__producer__demo`. diff --git a/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/metamodel.yaml b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/metamodel.yaml new file mode 100644 index 000000000..dc4e1651a --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/consumer/docs/metamodel.yaml @@ -0,0 +1,29 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +needs_types: + test_req: + title: Test Requirement + prefix: test_req__ + parts: 3 + mandatory_options: + id: ^test_req__[0-9a-zA-Z_]*$ + status: ^(draft|valid)$ + optional_options: + # These are all sphinx-needs default fields. Listing them keeps the + # metamodel/default option overlap exactly at the set score_metamodel + # treats as intentional ({id, tags, status, content, template}), which + # avoids a setup-time warning that -W would turn into a build error. + tags: .* + content: .* + template: .* +links: {} diff --git a/src/tests/docs_bzl/fixtures/external_needs/producer/BUILD b/src/tests/docs_bzl/fixtures/external_needs/producer/BUILD new file mode 100644 index 000000000..352903a86 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/producer/BUILD @@ -0,0 +1,22 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +# FIXME: +# The custom metamodel is supplied via score_metamodel_yaml in docs/conf.py +# rather than docs(metamodel=...), because the metamodel= wiring does not reach +# the :needs_json target in the current docs.bzl (regression from #484). +docs( + source_dir = "docs", +) diff --git a/src/tests/docs_bzl/fixtures/external_needs/producer/docs/conf.py b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/conf.py new file mode 100644 index 000000000..5bdbc3015 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/conf.py @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import os + +project = "External Needs Producer" +project_url = "https://example.invalid/external-needs-producer" +extensions = ["score_sphinx_bundle"] + +# sphinx-needs writes this as the needs.json `current_version`. A consumer that +# mounts this needs.json resolves needs under this version key, so it must be +# non-empty (an unset version yields an empty current_version that sphinx-needs +# rejects with "No version defined"). +version = "main" + +# docs(metamodel=...) does not reach the :needs_json (sphinx_docs) target in the +# current docs.bzl, so we load the custom metamodel via the officially-supported +# score_metamodel_yaml config value. The YAML lives next to this conf.py so it is +# globbed into :docs_sources and materialized in the sandbox. +score_metamodel_yaml = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "metamodel.yaml" +) + +# The producer need id `test_req__producer__demo` has three "__"-parts. The +# id_contains_feature check requires the feature part ("producer") to appear in +# the docname or in required_in_id; the fixture lives in index.rst, so we declare +# it here. +required_in_id = ["producer"] diff --git a/src/tests/docs_bzl/fixtures/external_needs/producer/docs/index.rst b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/index.rst new file mode 100644 index 000000000..8e5a06b4f --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/index.rst @@ -0,0 +1,20 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +External Needs Producer +======================= + +.. test_req:: Producer demo requirement + :id: test_req__producer__demo + :status: valid diff --git a/src/tests/docs_bzl/fixtures/external_needs/producer/docs/metamodel.yaml b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/metamodel.yaml new file mode 100644 index 000000000..dc4e1651a --- /dev/null +++ b/src/tests/docs_bzl/fixtures/external_needs/producer/docs/metamodel.yaml @@ -0,0 +1,29 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +needs_types: + test_req: + title: Test Requirement + prefix: test_req__ + parts: 3 + mandatory_options: + id: ^test_req__[0-9a-zA-Z_]*$ + status: ^(draft|valid)$ + optional_options: + # These are all sphinx-needs default fields. Listing them keeps the + # metamodel/default option overlap exactly at the set score_metamodel + # treats as intentional ({id, tags, status, content, template}), which + # avoids a setup-time warning that -W would turn into a build error. + tags: .* + content: .* + template: .* +links: {} diff --git a/src/tests/docs_bzl/fixtures/metamodel_violation/BUILD b/src/tests/docs_bzl/fixtures/metamodel_violation/BUILD new file mode 100644 index 000000000..352903a86 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/metamodel_violation/BUILD @@ -0,0 +1,22 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +# FIXME: +# The custom metamodel is supplied via score_metamodel_yaml in docs/conf.py +# rather than docs(metamodel=...), because the metamodel= wiring does not reach +# the :needs_json target in the current docs.bzl (regression from #484). +docs( + source_dir = "docs", +) diff --git a/src/tests/docs_bzl/fixtures/metamodel_violation/docs/conf.py b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/conf.py new file mode 100644 index 000000000..557f0d7fe --- /dev/null +++ b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/conf.py @@ -0,0 +1,27 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import os + +project = "Metamodel Check" +project_url = "https://example.invalid/metamodel-check" +extensions = ["score_sphinx_bundle"] + +# NOTE: docs(metamodel=...) does not reach the :needs_json (sphinx_docs) target +# in the current docs.bzl (the metamodel_opts/metamodel_data wiring was dropped +# in commit 970f2604 / #484). We therefore load the custom metamodel via the +# officially-supported score_metamodel_yaml config value. The YAML lives next to +# this conf.py so it is globbed into :docs_sources and materialized in the sandbox. +score_metamodel_yaml = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "metamodel.yaml" +) diff --git a/src/tests/docs_bzl/fixtures/metamodel_violation/docs/index.rst b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/index.rst new file mode 100644 index 000000000..fa0a6d725 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/index.rst @@ -0,0 +1,19 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +Metamodel Check +=============== + +.. test_req:: A requirement missing its status + :id: test_req__demo_missing diff --git a/src/tests/docs_bzl/fixtures/metamodel_violation/docs/metamodel.yaml b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/metamodel.yaml new file mode 100644 index 000000000..386f815e4 --- /dev/null +++ b/src/tests/docs_bzl/fixtures/metamodel_violation/docs/metamodel.yaml @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +needs_types: + test_req: + title: Test Requirement + prefix: test_req__ + parts: 2 + mandatory_options: + id: ^test_req__[0-9a-zA-Z_]*$ + status: ^(draft|valid)$ + optional_options: + # FIXME: + # These are all sphinx-needs default fields. Listing them keeps the + # metamodel/default option overlap exactly at the set score_metamodel + # treats as intentional ({id, tags, status, content, template}), which + # avoids a setup-time warning that -W would turn into a build error. + tags: .* + content: .* + template: .* +links: {} diff --git a/src/tests/docs_bzl/helpers.py b/src/tests/docs_bzl/helpers.py new file mode 100644 index 000000000..13611ed6f --- /dev/null +++ b/src/tests/docs_bzl/helpers.py @@ -0,0 +1,142 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Helpers for Category-2 e2e tests: run real bazel against in-repo fixtures.""" + +import json +import shutil +import subprocess +import time +from dataclasses import dataclass +from pathlib import Path + +from src.helper_lib import find_git_root + +FIXTURES = Path(__file__).parent / "fixtures" +GIT_ROOT = find_git_root() +assert GIT_ROOT +FIXTURES_BAZEL = "//" + str(FIXTURES.relative_to(GIT_ROOT)) + + +def repo_root() -> Path: + root = find_git_root() + assert root is not None, "git root not found" + return root + + +def run_bazel(args: list[str], expect_error: bool = False): + start_time = time.time() + cmd = ["bazel", *args] + cmd_str = " ".join(cmd) + + p = subprocess.run( + cmd, + cwd=repo_root(), + capture_output=True, + text=True, + ) + end_time = time.time() + print(f"Running 'bazel {' '.join(args)}' took {end_time - start_time:.4f} seconds") + + if expect_error and p.returncode == 0: + raise RuntimeError( + f"{cmd_str} was expected to fail but succeeded\n" + f"stdout:\n{p.stdout}\n" + f"stderr:\n{p.stderr}" + ) + + if not expect_error and p.returncode != 0: + raise RuntimeError( + f"bazel {args} failed with exit code {p.returncode}\n" + f"stdout:\n{p.stdout}\n" + f"stderr:\n{p.stderr}" + ) + + return p + + +@dataclass +class RunResult: + stdout: str + stderr: str + build_dir: Path + artifacts: dict[str, Path] | None + + +def run_fixture( + bazel_cmd: str, + fixture: str, + target: str, + expect_error: bool = False, +) -> RunResult: + """Run bazel in a fixture subdirectory.""" + assert bazel_cmd in ("build", "run"), "only build and run are supported" + assert target.startswith(":"), "target must be relative to fixture" + + if not (FIXTURES / fixture).is_dir(): + raise ValueError(f"fixture {fixture} does not exist in {FIXTURES}") + + # Split logs by bazel commands visually without adding any fancy formatting dependency + print("") + + full_target = FIXTURES_BAZEL + "/" + fixture + target + + clean_build(FIXTURES / fixture) + + p1 = run_bazel([bazel_cmd, full_target], expect_error=expect_error) + + if bazel_cmd == "build": + # Generic solution needs to use cquery. + # That extra cquery is about 0,5 seconds, which sounds horrible for testing, + # but considering how slow bazel is anyway... + RUN_CQUERY = False + if RUN_CQUERY: + p2 = run_bazel(["cquery", "--output=files", full_target]) + artifacts_dir = Path(p2.stdout.strip().splitlines()[0]) + else: + assert GIT_ROOT + # e.g. bazel-out/k8-fastbuild/bin/src/tests/docs_bzl/fixtures/external_needs/producer/needs_json/_build/needs + artifacts_dir = ( + GIT_ROOT + / "bazel-out/k8-fastbuild/bin/" + / FIXTURES.relative_to(GIT_ROOT) + / fixture + / target[1:] + / "_build/needs" + ) + + assert artifacts_dir.is_dir(), ( + f"expected output artifacts to be a directory, got {artifacts_dir}" + ) + artifacts = {f.name: f for f in artifacts_dir.iterdir()} + else: + artifacts = None + + return RunResult( + stdout=p1.stdout, + stderr=p1.stderr, + build_dir=FIXTURES / fixture / "_build", + artifacts=artifacts, + ) + + +def clean_build(path: Path) -> None: + """Delete the _build directory in a path, if it exists.""" + shutil.rmtree(path / "_build", ignore_errors=True) + + +def load_needs(needs_json: Path): + data = json.loads(needs_json.read_text(encoding="utf-8")) + needs: dict[str, object] = {} + for version in data.get("versions", {}).values(): + needs.update(version.get("needs", {})) + return needs diff --git a/src/tests/docs_bzl/test_docs_bzl.py b/src/tests/docs_bzl/test_docs_bzl.py new file mode 100644 index 000000000..701536777 --- /dev/null +++ b/src/tests/docs_bzl/test_docs_bzl.py @@ -0,0 +1,74 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +end-to-end tests for the public docs() macro. +See README.md in this directory for usage instructions. +""" + +from pathlib import Path + +from src.tests.docs_bzl.helpers import ( + load_needs, + run_fixture, +) + +FIXTURES = Path(__file__).parent / "fixtures" + + +def test_basic_docs_builds_html(): + result = run_fixture("run", "basic", ":docs") + + index_html = result.build_dir / "index.html" + + assert "Basic Test" in index_html.read_text(encoding="utf-8") + + +def test_metamodel_violation_fails_build(): + result = run_fixture( + "build", "metamodel_violation", ":needs_json", expect_error=True + ) + assert "is missing required attribute" in result.stderr + + +def test_producer_needs_json_contains_local_need(): + """ + The producer must export its own need + """ + result = run_fixture("build", "external_needs/producer", ":needs_json") + assert result.artifacts + + needs_json = result.artifacts["needs.json"] + needs = load_needs(needs_json) + + assert "test_req__producer__demo" in needs, sorted(needs) + + +def test_consumer_link_resolves(): + """ + The consumer must resolve the producer's need as an external link in its HTML output. + + Exit 0 under -W already proves no "not found" warning fired; + the html-content assert is the stronger guard: the producer id must + appear as a resolved external link (href into the producer's base_url), not + just as literal text. + """ + result = run_fixture("run", "external_needs/consumer", ":docs") + + index = result.build_dir / "index.html" + html = index.read_text(encoding="utf-8") + + # A resolved external link points at the producer's base_url + fragment. + assert "external-needs-producer/main/index.html#test_req__producer__demo" in html, ( + html + ) diff --git a/src/tests/downstream_compatibility/BUILD b/src/tests/downstream_compatibility/BUILD new file mode 100644 index 000000000..7c906e511 --- /dev/null +++ b/src/tests/downstream_compatibility/BUILD @@ -0,0 +1,26 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:score_pytest.bzl", "score_pytest") + +score_pytest( + name = "test", + size = "large", + srcs = [ + "conftest.py", + "test_consumer.py", + ], + data = [], + deps = [], + env = {}, +) diff --git a/src/tests/downstream_compatibility/README.md b/src/tests/downstream_compatibility/README.md new file mode 100644 index 000000000..05e0d85c1 --- /dev/null +++ b/src/tests/downstream_compatibility/README.md @@ -0,0 +1,117 @@ + + +# Docs-As-Code Consumer Tests + +This test validates that changes to the docs-as-code system don't break downstream consumers. +It tests both local changes and git-based overrides against real consumer repositories. + +## Use in CI + +If you want to start the consumer tests on a PR inside `docs-as-code`, then all you have to do is comment +`/consumer-test` on the PR and this should trigger them. + +## Quick Start + +```bash +# Create the virtual environment +bazel run //:ide_support + +# Run with std. configuration +.venv_docs/bin/python -m pytest -s src/tests + +# Run with more verbose output (up to -vvv) +.venv_docs/bin/python -m pytest -s -v src/tests + +# Run specific repositories only +.venv_docs/bin/python -m pytest -s src/tests --repo=score + +# Disable the persistent cache +.venv_docs/bin/python -m pytest -s src/tests --disable-cache + +# Or combine both options +.venv_docs/bin/python -m pytest -s src/tests --disable-cache --repo=score +``` + +## Verbosity Levels + +The test suite supports different levels of output detail: + +- **No flags**: Basic test results and summary table +- **`-v`**: Shows detailed warnings breakdown by logger type +- **`-vv`**: Adds full stdout/stderr output from build commands +- **`-vvv`**: Streams build output in real-time (useful for debugging hanging builds) + +## Command Line Options + +### `--disable-cache` +Disabled persistent caching for clean testing cycle. + +**What the test normaly do:** +- Uses `~/.cache/docs_as_code_consumer_tests` instead of temporary directories +- Reuses cloned repositories between runs (with git updates) +- Significantly speeds up subsequent test runs + +**This option disables the above mentioned behaviour and clones the repositories fresh** + +**When to use:** During development when you need to ensure testing is done on a fresh env. + +### `--repo` +Filters which repositories to test. + +**Usage:** +```bash +# Test only the 'score' repository +.venv_docs/bin/python -m pytest -s src/tests --repo=score + +# Test multiple repositories +.venv_docs/bin/python -m pytest -s src/tests --repo=score,module_template + +# Invalid repo names fall back to testing all repositories +.venv_docs/bin/python -m pytest -s src/tests --repo=nonexistent +``` + +**Available repositories:** Check `REPOS_TO_TEST` in the test file for current list. + +## Currently tested repositories + +- [score](https://github.com/eclipse-score/score) +- [process_description](https://github.com/eclipse-score/process_description) +- [module_template](https://github.com/eclipse-score/module_template) + +## What Gets Tested + +For each repository, the test: +1. Clones the consumer repository +2. Tests with **local override** (your current changes) +3. Tests with **git override** (current commit from remote) +4. Runs build commands and test commands +5. Analyzes warnings and build success + +## Example Development Workflow + +```bash +# Create the virtual environment +bazel run //:ide_support + +# First run - clones everything fresh +.venv_docs/bin/python -m pytest -s -v src/tests --repo=score + +# Make changes to docs-as-code... + +# Subsequent runs - much faster due to caching +.venv_docs/bin/python -m pytest -s -v src/tests --repo=score + +# Final validation - test all repos without cache +.venv_docs/bin/python -m pytest -s -v src/tests --disable-cache +``` diff --git a/src/tests/conftest.py b/src/tests/downstream_compatibility/conftest.py similarity index 100% rename from src/tests/conftest.py rename to src/tests/downstream_compatibility/conftest.py diff --git a/src/tests/test_consumer.py b/src/tests/downstream_compatibility/test_consumer.py similarity index 95% rename from src/tests/test_consumer.py rename to src/tests/downstream_compatibility/test_consumer.py index 046c35c67..32c132fdf 100644 --- a/src/tests/test_consumer.py +++ b/src/tests/downstream_compatibility/test_consumer.py @@ -15,10 +15,10 @@ Via Python (requires ide_support to have been run first): bazel run //:ide_support # once, to set up .venv_docs - python -m pytest src/tests/test_consumer.py -s - python -m pytest src/tests/test_consumer.py -k "score and local" - python -m pytest src/tests/test_consumer.py -k "process_description" - python -m pytest src/tests/test_consumer.py --disable-cache + python -m pytest src/tests/downstream_compatibility/test_consumer.py -s + python -m pytest src/tests/downstream_compatibility/test_consumer.py -k "score and local" + python -m pytest src/tests/downstream_compatibility/test_consumer.py -k "process_description" + python -m pytest src/tests/downstream_compatibility/test_consumer.py --disable-cache Known non-passing tests are sometimes marked xfail and do not count as failures. """ @@ -66,7 +66,7 @@ def __init__( self.commands = commands else: # The minimum is to ensure that the repo can build against the current score_docs_as_code branch. - # If docs works, then usually docs_checl and needs_json will work as well. + # If docs works, then usually docs_check and needs_json will work as well. self.commands = ["bazel run //:docs"] @@ -222,6 +222,8 @@ def _ensure_repo(repo_path: Path, git_url: str, use_cache: bool) -> None: def _cleanup_before_cmd(cwd: Path, cmd: str) -> None: # ubproject.toml is created by :docs + # Delete in cwd (repo root) and any subdir to avoid stale config from previous runs + (cwd / "ubproject.toml").unlink(missing_ok=True) for p in cwd.glob("*/ubproject.toml"): p.unlink() diff --git a/src/tests/test-levels.drawio.svg b/src/tests/test-levels.drawio.svg new file mode 100644 index 000000000..e7c6d43dc --- /dev/null +++ b/src/tests/test-levels.drawio.svg @@ -0,0 +1,332 @@ + + + + + + + + + + +
+
+
+ Extension Test Scope +
+
+
+
+ + Extension Test Scope + +
+
+
+ + + + + + + +
+
+
+ Application Test Scope +
+
+
+
+ + Application Test Scope + +
+
+
+ + + + + + + +
+
+
+ Unit Test Scope +
+
+
+
+ + Unit Test Scope + +
+
+
+ + + + + + + + + + + +
+
+
+ Downstream-Compatiblity Test Scope +
+
+
+
+ + Downstream-Compatiblity Test Scope + +
+
+
+ + + + + + + +
+
+
+ Module Test Scope +
+
+
+
+ + Module Test Scope + +
+
+
+ + + + + + + +
+
+
+ docs-bzl Scope +
+
+
+
+ + docs-bzl Scope + +
+
+
+ + + + + + + +
+
+
+ docs-as-code Testing Scopes +
+
+
+
+ + docs-as-code Testing Scopes + +
+
+
+ + + + + + + +
+
+
+ Each overlay shows which part of the architecture is exercised by a test level. +
+
+
+
+ + Each overlay shows which part of the architecture is exercised by a test level. + +
+
+
+ + + + + + + +
+
+
+ MODULE.bazel +
+
+
+
+ + MODULE.bazel + +
+
+
+ + + + + + + +
+
+
+ docs() macro +
+
+
+
+ + docs() macro + +
+
+
+ + + + + + + + + + + +
+
+
+ Sphinx application +
+
+
+
+ + Sphinx application + +
+
+
+ + + + + + + +
+
+
+ Python functions +
+
+
+
+ + Python functions + +
+
+
+ + + + + + + +
+
+
+ HTML output +
+
+
+
+ + HTML output + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Downstream +
+ consumers +
+
+
+
+ + Downstream... + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
From 20f6026c947d7a2bbc2d5d5a42774ff2de70dfd3 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 13:38:22 +0200 Subject: [PATCH 04/13] fix workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61ed36d10..cba682d63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: cache-save: ${{ github.event_name == 'push' }} - name: Run docs_bzl tests - run: pytest -vv src/tests/docs_bzl + run: python -m pytest -vv src/tests/docs_bzl - name: Run bazel test targets run: | From f0235a18cc4a5991e593afa3f41696a4b2d632b1 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 13:42:03 +0200 Subject: [PATCH 05/13] fix workflow 2 --- ...{consumer_test.yml => downstream_compatibility.yml} | 2 +- .github/workflows/test.yml | 4 +++- ...st_consumer.py => test_downstream_compatibility.py} | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) rename .github/workflows/{consumer_test.yml => downstream_compatibility.yml} (93%) rename src/tests/downstream_compatibility/{test_consumer.py => test_downstream_compatibility.py} (96%) diff --git a/.github/workflows/consumer_test.yml b/.github/workflows/downstream_compatibility.yml similarity index 93% rename from .github/workflows/consumer_test.yml rename to .github/workflows/downstream_compatibility.yml index 680fda38b..b26302876 100644 --- a/.github/workflows/consumer_test.yml +++ b/.github/workflows/downstream_compatibility.yml @@ -48,7 +48,7 @@ jobs: run: bazel run //:ide_support - name: Run Consumer tests - run: .venv_docs/bin/python -m pytest -vv -s src/tests/test_consumer.py -k "$CONSUMER" + run: .venv_docs/bin/python -m pytest -vv -s src/tests/downstream_compatibility -k "$CONSUMER" env: FORCE_COLOR: "1" TERM: xterm-256color diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cba682d63..e25c356dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,9 @@ jobs: cache-save: ${{ github.event_name == 'push' }} - name: Run docs_bzl tests - run: python -m pytest -vv src/tests/docs_bzl + run: + pip install pytest + pytest -vv src/tests/docs_bzl - name: Run bazel test targets run: | diff --git a/src/tests/downstream_compatibility/test_consumer.py b/src/tests/downstream_compatibility/test_downstream_compatibility.py similarity index 96% rename from src/tests/downstream_compatibility/test_consumer.py rename to src/tests/downstream_compatibility/test_downstream_compatibility.py index 32c132fdf..d22cb77dd 100644 --- a/src/tests/downstream_compatibility/test_consumer.py +++ b/src/tests/downstream_compatibility/test_downstream_compatibility.py @@ -11,14 +11,14 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* """ -Consumer tests: verify that downstream repos build successfully against this branch. +Downstream compatibility tests: verify that downstream repos build successfully against this branch. Via Python (requires ide_support to have been run first): bazel run //:ide_support # once, to set up .venv_docs - python -m pytest src/tests/downstream_compatibility/test_consumer.py -s - python -m pytest src/tests/downstream_compatibility/test_consumer.py -k "score and local" - python -m pytest src/tests/downstream_compatibility/test_consumer.py -k "process_description" - python -m pytest src/tests/downstream_compatibility/test_consumer.py --disable-cache + python -m pytest src/tests/downstream_compatibility -s + python -m pytest src/tests/downstream_compatibility -k "score and local" + python -m pytest src/tests/downstream_compatibility -k "process_description" + python -m pytest src/tests/downstream_compatibility --disable-cache Known non-passing tests are sometimes marked xfail and do not count as failures. """ From 8e702aba325413feb84daad8abfedce6993f0700 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 13:48:24 +0200 Subject: [PATCH 06/13] fix workflow 3 --- .github/workflows/downstream_compatibility.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/downstream_compatibility.yml b/.github/workflows/downstream_compatibility.yml index b26302876..27cb5ac91 100644 --- a/.github/workflows/downstream_compatibility.yml +++ b/.github/workflows/downstream_compatibility.yml @@ -11,7 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -name: Consumer Tests +name: Downstream Compatibility on: workflow_call: pull_request: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e25c356dc..28f9d7ab7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: cache-save: ${{ github.event_name == 'push' }} - name: Run docs_bzl tests - run: + run: | pip install pytest pytest -vv src/tests/docs_bzl From a699ff6106a08e9989ec7bed302a6322e486deac Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 14:37:29 +0200 Subject: [PATCH 07/13] fix workflow 4 --- .github/workflows/test.yml | 7 ++++--- pyproject.toml | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28f9d7ab7..7199e7f0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,10 +40,11 @@ jobs: bazelisk-cache: true cache-save: ${{ github.event_name == 'push' }} + - name: Install pytest + run: pip install pytest + - name: Run docs_bzl tests - run: | - pip install pytest - pytest -vv src/tests/docs_bzl + run: pytest -vv src/tests/docs_bzl - name: Run bazel test targets run: | diff --git a/pyproject.toml b/pyproject.toml index 277775e0d..cb555641f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,5 +109,6 @@ filterwarnings = [ ] pythonpath = [ + ".", "src/extensions/", ] From a41a20d1ce57a524208ad008935f2b63533c3c0f Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 14:41:22 +0200 Subject: [PATCH 08/13] fix globbing --- src/tests/docs_bzl/BUILD | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/tests/docs_bzl/BUILD diff --git a/src/tests/docs_bzl/BUILD b/src/tests/docs_bzl/BUILD new file mode 100644 index 000000000..a9f47d3de --- /dev/null +++ b/src/tests/docs_bzl/BUILD @@ -0,0 +1,14 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Empty BUILD file ensures bazel will not see this directory when globbing From 77416552d0f411020bef5e0287b6fda44479744d Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 17:30:45 +0200 Subject: [PATCH 09/13] fix workflow 5 --- .github/workflows/test.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7199e7f0c..340501df0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,19 +40,17 @@ jobs: bazelisk-cache: true cache-save: ${{ github.event_name == 'push' }} - - name: Install pytest - run: pip install pytest + - name: Setup venv + run: bazel run --lockfile_mode=error //:ide_support - name: Run docs_bzl tests - run: pytest -vv src/tests/docs_bzl + run: .venv/bin/python -m pytest -vv src/tests/docs_bzl - name: Run bazel test targets - run: | - bazel run --lockfile_mode=error //:ide_support - bazel test --lockfile_mode=error //... + run: bazel test --lockfile_mode=error //... - name: Prepare bundled test report - if: always() + if: ! cancelled() # Creating tests-report directory # Follow Symlinks via '-L' to copy correctly # Copy everything inside the 'test-reports' folder From e3a31e6a124826f53af2467275498b9456b5ed96 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 17:38:17 +0200 Subject: [PATCH 10/13] fix workflow 6 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 340501df0..40953e2bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: run: bazel run --lockfile_mode=error //:ide_support - name: Run docs_bzl tests - run: .venv/bin/python -m pytest -vv src/tests/docs_bzl + run: .venv_docs/bin/python -m pytest -vv src/tests/docs_bzl - name: Run bazel test targets run: bazel test --lockfile_mode=error //... From 9f7423198352716ddf990a104b97d9d4e61957eb Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 17:48:36 +0200 Subject: [PATCH 11/13] exclude downstream_compatibility from test //... --- src/tests/downstream_compatibility/BUILD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/downstream_compatibility/BUILD b/src/tests/downstream_compatibility/BUILD index 7c906e511..1a3698646 100644 --- a/src/tests/downstream_compatibility/BUILD +++ b/src/tests/downstream_compatibility/BUILD @@ -23,4 +23,7 @@ score_pytest( data = [], deps = [], env = {}, + + # These tests are huge, don't run them on //... + tags = ["manual"] ) From 489dd9c368b0972da029ffaa1bcc7aef564eaedd Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 18:36:12 +0200 Subject: [PATCH 12/13] fix workflow 7 --- .github/workflows/test.yml | 6 +++++- docs.bzl | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40953e2bc..8773ef2dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,11 @@ jobs: run: .venv_docs/bin/python -m pytest -vv src/tests/docs_bzl - name: Run bazel test targets - run: bazel test --lockfile_mode=error //... + run: bazel test --lockfile_mode=error //... --build_tests_only + + - name: Build all targets + # Skip tests, since some are non buildable negative tests + run: bazel build --lockfile_mode=error //... -- -//src/tests/... - name: Prepare bundled test report if: ! cancelled() diff --git a/docs.bzl b/docs.bzl index a03d54e67..30c0cdaa3 100644 --- a/docs.bzl +++ b/docs.bzl @@ -50,8 +50,8 @@ def _rewrite_needs_json_to_docs_sources(labels): out = [] for x in labels: s = str(x) - if s.endswith("//:needs_json"): - out.append(s.replace("//:needs_json", "//:docs_sources")) + if s.endswith(":needs_json"): + out.append(s.replace(":needs_json", ":docs_sources")) else: out.append(s) return out @@ -61,9 +61,9 @@ def _rewrite_needs_json_to_sourcelinks(labels): out = [] for x in labels: s = str(x) - if s.endswith("//:needs_json"): - out.append(s.replace("//:needs_json", "//:sourcelinks_json")) - #Items which do not end up with '//:needs_json' shall not be appended to 'out'. + if s.endswith(":needs_json"): + out.append(s.replace(":needs_json", ":sourcelinks_json")) + #Items which do not end up with ':needs_json' shall not be appended to 'out'. #They are treated separately and are not related to source code linking. return out From b290cba7857b02b9bf41bab8e4b56a1c5bb78c4f Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 24 Jul 2026 19:19:41 +0200 Subject: [PATCH 13/13] drop downstream_compatibility from bazel tests --- src/tests/{docs_bzl => }/BUILD | 0 src/tests/downstream_compatibility/BUILD | 29 ------------------------ 2 files changed, 29 deletions(-) rename src/tests/{docs_bzl => }/BUILD (100%) delete mode 100644 src/tests/downstream_compatibility/BUILD diff --git a/src/tests/docs_bzl/BUILD b/src/tests/BUILD similarity index 100% rename from src/tests/docs_bzl/BUILD rename to src/tests/BUILD diff --git a/src/tests/downstream_compatibility/BUILD b/src/tests/downstream_compatibility/BUILD deleted file mode 100644 index 1a3698646..000000000 --- a/src/tests/downstream_compatibility/BUILD +++ /dev/null @@ -1,29 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -load("//:score_pytest.bzl", "score_pytest") - -score_pytest( - name = "test", - size = "large", - srcs = [ - "conftest.py", - "test_consumer.py", - ], - data = [], - deps = [], - env = {}, - - # These tests are huge, don't run them on //... - tags = ["manual"] -)