From 4deec1a1dbc6cc8d924ecec16c6e780e34928604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20S=C3=B6ren=20Pollak?= Date: Wed, 13 Aug 2025 16:01:12 +0200 Subject: [PATCH 1/5] Added helper_lib --- docs/conf.py | 1 - src/extensions/score_metamodel/BUILD | 2 +- src/extensions/score_metamodel/__init__.py | 7 + .../score_metamodel/external_needs.py | 1 - src/extensions/score_source_code_linker/BUILD | 1 + .../score_source_code_linker/__init__.py | 96 ++--------- .../generate_source_code_links_json.py | 25 --- .../tests/test_requirement_links.py | 100 ++--------- .../tests/test_source_link.py | 12 +- src/helper_lib/BUILD | 31 ++++ src/helper_lib/__init__.py | 159 ++++++++++++++++++ src/helper_lib/test_helper_lib.py | 70 ++++++++ src/tests/test_consumer.py | 26 ++- 13 files changed, 315 insertions(+), 216 deletions(-) create mode 100644 src/helper_lib/BUILD create mode 100644 src/helper_lib/__init__.py create mode 100644 src/helper_lib/test_helper_lib.py diff --git a/docs/conf.py b/docs/conf.py index 96074dce5..fc6460266 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,7 +29,6 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - extensions = [ "sphinx_design", "sphinx_needs", diff --git a/src/extensions/score_metamodel/BUILD b/src/extensions/score_metamodel/BUILD index fa6b976eb..014b6ca44 100644 --- a/src/extensions/score_metamodel/BUILD +++ b/src/extensions/score_metamodel/BUILD @@ -25,7 +25,7 @@ py_library( ], visibility = ["//visibility:public"], # TODO: Figure out if all requirements are needed or if we can break it down a bit - deps = all_requirements, + deps = all_requirements + ["@score_docs_as_code//src/helper_lib"], ) score_py_pytest( diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 1127a3b4b..7f5cbdd2d 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -24,6 +24,13 @@ from sphinx_needs.config import NeedType from sphinx_needs.data import NeedsInfoType, NeedsView, SphinxNeedsData +from src.helper_lib import ( + find_git_root, + find_ws_root, + get_current_git_hash, + get_github_repo_info, +) + from .external_needs import connect_external_needs from .log import CheckLogger diff --git a/src/extensions/score_metamodel/external_needs.py b/src/extensions/score_metamodel/external_needs.py index 913392aee..1e94388eb 100644 --- a/src/extensions/score_metamodel/external_needs.py +++ b/src/extensions/score_metamodel/external_needs.py @@ -52,7 +52,6 @@ def _parse_bazel_external_need(s: str) -> ExternalNeedsSource | None: return ExternalNeedsSource( bazel_module=repo, path_to_target=path_to_target, target=target ) - # Unknown data target. Probably not a needs.json file. return None diff --git a/src/extensions/score_source_code_linker/BUILD b/src/extensions/score_source_code_linker/BUILD index 9828b22bc..08735e007 100644 --- a/src/extensions/score_source_code_linker/BUILD +++ b/src/extensions/score_source_code_linker/BUILD @@ -24,6 +24,7 @@ py_library( ), imports = ["."], visibility = ["//visibility:public"], + deps = ["@score_docs_as_code//src/helper_lib"], ) score_py_pytest( diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 7d047501e..272f79daa 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -15,7 +15,6 @@ source code links from a JSON file and add them to the needs. """ -import subprocess from collections import defaultdict from copy import deepcopy from pathlib import Path @@ -37,6 +36,10 @@ NeedLink, load_source_code_links_json, ) +from src.helper_lib import ( + get_current_git_hash, + get_github_base_url, +) LOGGER = get_logger(__name__) # Outcomment this to enable more verbose logging @@ -62,13 +65,13 @@ def setup_once(app: Sphinx, config: Config): LOGGER.debug(f"DEBUG: Git root is {find_git_root()}") # Run only for local files! - # ws_root is not set when running on external repositories (dependencies). + # ws_root is not set when running on any on bazel run command repositories (dependencies) ws_root = find_ws_root() if not ws_root: return # When BUILD_WORKSPACE_DIRECTORY is set, we are inside a git repository. - assert find_git_root(ws_root) + assert find_git_root() # Extension: score_source_code_linker app.add_config_value( @@ -143,92 +146,17 @@ def group_by_need(source_code_links: list[NeedLink]) -> dict[str, list[NeedLink] return source_code_links_by_need -def parse_git_output(str_line: str) -> str: - if len(str_line.split()) < 2: - LOGGER.warning( - "Got wrong input line from 'get_github_repo_info'. " - f"Input: {str_line}." - "Expected example: 'origin git@github.com:user/repo.git'" - ) - return "" - url = str_line.split()[1] # Get the URL part - # Handle SSH format (git@github.com:user/repo.git) - if url.startswith("git@"): - path = url.split(":")[1] - else: - path = "/".join(url.split("/")[3:]) # Get part after github.com/ - return path.replace(".git", "") - - -def get_github_repo_info(git_root_cwd: Path) -> str: - process = subprocess.run( - ["git", "remote", "-v"], capture_output=True, text=True, cwd=git_root_cwd - ) - repo = "" - for line in process.stdout.split("\n"): - if "origin" in line and "(fetch)" in line: - repo = parse_git_output(line) - break - else: - # If we do not find 'origin' we just take the first line - LOGGER.info( - "Did not find origin remote name. " - "Will now take first result from: 'git remote -v'" - ) - repo = parse_git_output(process.stdout.split("\n")[0]) - assert repo != "", ( - "Remote repository is not defined. Make sure you have a remote set. " - "Check this via 'git remote -v'" - ) - return repo - - -def get_git_root(git_root: Path = Path()) -> Path: - # This is kinda ugly, doing this to reduce type errors. - # There might be a nicer way to do this - if git_root == Path(): - passed_git_root = find_git_root() - if passed_git_root is None: - return Path() - else: - passed_git_root = git_root - return passed_git_root - - -def get_github_base_url(git_root: Path = Path()) -> str: - passed_git_root = get_git_root(git_root) - repo_info = get_github_repo_info(passed_git_root) - return f"https://github.com/{repo_info}" - - def get_github_link( - git_root: Path = Path(), needlink: NeedLink = DefaultNeedLink() + needlink: NeedLink = DefaultNeedLink() ) -> str: - passed_git_root = get_git_root(git_root) - base_url = get_github_base_url( - passed_git_root - ) # Pass git_root to avoid double lookup + passed_git_root = find_git_root() + if passed_git_root is None: + passed_git_root = Path() + base_url = get_github_base_url() current_hash = get_current_git_hash(passed_git_root) return f"{base_url}/blob/{current_hash}/{needlink.file}#L{needlink.line}" -def get_current_git_hash(ws_root: Path) -> str: - try: - result = subprocess.run( - ["git", "log", "-n", "1", "--pretty=format:%H"], - cwd=ws_root, - capture_output=True, - check=True, - ) - decoded_result = result.stdout.strip().decode() - - assert all(c in "0123456789abcdef" for c in decoded_result) - return decoded_result - except Exception as e: - LOGGER.warning(f"Unexpected error: {ws_root}", exc_info=e) - raise - - # req-Id: tool_req__docs_dd_link_source_code_link def inject_links_into_needs(app: Sphinx, env: BuildEnvironment) -> None: """ @@ -280,7 +208,7 @@ def inject_links_into_needs(app: Sphinx, env: BuildEnvironment) -> None: need_as_dict = cast(dict[str, object], need) need_as_dict["source_code_link"] = ", ".join( - f"{get_github_link(ws_root, n)}<>{n.file}:{n.line}" for n in needlinks + f"{get_github_link(n)}<>{n.file}:{n.line}" for n in needlinks ) # NOTE: Removing & adding the need is important to make sure diff --git a/src/extensions/score_source_code_linker/generate_source_code_links_json.py b/src/extensions/score_source_code_linker/generate_source_code_links_json.py index 12b714086..18f5ee28d 100644 --- a/src/extensions/score_source_code_linker/generate_source_code_links_json.py +++ b/src/extensions/score_source_code_linker/generate_source_code_links_json.py @@ -25,31 +25,6 @@ store_source_code_links_json, ) - -def find_ws_root() -> Path | None: - """Find the current MODULE.bazel file""" - ws_dir = os.environ.get("BUILD_WORKSPACE_DIRECTORY", None) - return Path(ws_dir) if ws_dir else None - - -def find_git_root(start_path: str | Path = "") -> Path | None: - """Find the git root directory starting from the given path or __file__.""" - if start_path == "": - start_path = __file__ - - git_root = Path(start_path).resolve() - esbonio_search = False - while not (git_root / ".git").exists(): - git_root = git_root.parent - if git_root == Path("/"): - # fallback to cwd when building with python -m sphinx docs _build -T - if esbonio_search: - return None - git_root = Path.cwd().resolve() - esbonio_search = True - return git_root - - TAGS = [ "# " + "req-traceability:", "# " + "req-Id:", diff --git a/src/extensions/score_source_code_linker/tests/test_requirement_links.py b/src/extensions/score_source_code_linker/tests/test_requirement_links.py index 4b57f8b38..1eb748ece 100644 --- a/src/extensions/score_source_code_linker/tests/test_requirement_links.py +++ b/src/extensions/score_source_code_linker/tests/test_requirement_links.py @@ -28,17 +28,19 @@ from src.extensions.score_source_code_linker import ( find_need, get_cache_filename, - get_current_git_hash, get_github_link, - get_github_repo_info, group_by_need, - parse_git_output, ) from src.extensions.score_source_code_linker.needlinks import ( NeedLink, load_source_code_links_json, store_source_code_links_json, ) +from src.helper_lib import ( + get_current_git_hash, + get_github_repo_info, + parse_remote_git_output, +) """ # ────────────────ATTENTION─────────────── @@ -83,7 +85,6 @@ def needlink_test_decoder(d: dict[str, Any]) -> NeedLink | dict[str, Any]: need=d["need"], full_line=decode_comment(d["full_line"]), ) - # It's something else, pass it on to other decoders return d @@ -120,7 +121,6 @@ def git_repo(temp_dir): cwd=git_dir, check=True, ) - return git_dir @@ -355,35 +355,35 @@ def test_group_by_need_empty_list(): def test_parse_git_output_ssh_format(): """Test parsing git remote output in SSH format.""" git_line = "origin git@github.com:test-user/test-repo.git (fetch)" - result = parse_git_output(git_line) + result = parse_remote_git_output(git_line) assert result == "test-user/test-repo" def test_parse_git_output_https_format(): """Test parsing git remote output in HTTPS format.""" git_line = "origin https://github.com/test-user/test-repo.git (fetch)" - result = parse_git_output(git_line) + result = parse_remote_git_output(git_line) assert result == "test-user/test-repo" def test_parse_git_output_ssh_format_without_git_suffix(): """Test parsing git remote output in SSH format without .git suffix.""" git_line = "origin git@github.com:test-user/test-repo (fetch)" - result = parse_git_output(git_line) + result = parse_remote_git_output(git_line) assert result == "test-user/test-repo" def test_parse_git_output_invalid_format(): """Test parsing invalid git remote output.""" git_line = "invalid" - result = parse_git_output(git_line) + result = parse_remote_git_output(git_line) assert result == "" def test_parse_git_output_empty_string(): """Test parsing empty git remote output.""" git_line = "" - result = parse_git_output(git_line) + result = parse_remote_git_output(git_line) assert result == "" @@ -407,9 +407,6 @@ def test_get_github_repo_info_multiple_remotes(git_repo_multiple_remotes): def test_get_current_git_hash(git_repo): """Test getting current git hash.""" - print("==== GIt REPO====") - a = git_repo - print(a) result = get_current_git_hash(git_repo) # Verify it's a valid git hash (40 hex characters) @@ -423,28 +420,6 @@ def test_get_current_git_hash_invalid_repo(temp_dir): get_current_git_hash(temp_dir) -# def test_get_github_base_url_with_real_repo(git_repo): -# """Test getting GitHub base URL with real repository.""" -# # Temporarily set the git repo as the current directory context -# original_cwd = os.getcwd() -# os.chdir(git_repo) -# -# try: -# # We need to temporarily patch find_git_root to return our test repo -# import src.extensions.score_source_code_linker as module -# -# original_find_git_root = module.find_git_root -# module.find_git_root = lambda: git_repo -# -# result = get_github_base_url() -# expected = "https://github.com/test-user/test-repo" -# assert result == expected -# -# finally: -# module.find_git_root = original_find_git_root -# os.chdir(original_cwd) - - def test_get_github_link_with_real_repo(git_repo): """Test generating GitHub link with real repository.""" # Create a needlink @@ -456,7 +431,9 @@ def test_get_github_link_with_real_repo(git_repo): full_line="#" + " req-Id: REQ_001", ) - result = get_github_link(git_repo, needlink) + # Have to change directories in order to ensure that we get the right/any .git file + os.chdir(Path(git_repo).absolute()) + result = get_github_link(needlink) # Should contain the base URL, hash, file path, and line number assert "https://github.com/test-user/test-repo/blob/" in result @@ -511,7 +488,7 @@ def test_cache_file_with_encoded_comments(temp_dir): store_source_code_links_json(cache_file, needlinks) # Check the raw JSON to verify encoding - with open(cache_file, "r") as f: + with open(cache_file) as f: raw_content = f.read() assert "#" + " req-Id:" in raw_content # Should be encoded assert "#-----req-Id:" not in raw_content # Original should not be present @@ -629,10 +606,10 @@ def another_function(): assert len(grouped["TREQ_ID_2"]) == 1 # Test GitHub link generation - + # Have to change directories in order to ensure that we get the right/any .git file os.chdir(Path(git_repo).absolute()) for needlink in loaded_links: - github_link = get_github_link(git_repo, needlink) + github_link = get_github_link(needlink) assert "https://github.com/test-user/test-repo/blob/" in github_link assert f"src/{needlink.file.name}#L{needlink.line}" in github_link @@ -665,48 +642,5 @@ def test_multiple_commits_hash_consistency(git_repo): ) os.chdir(Path(git_repo).absolute()) - github_link = get_github_link(git_repo, needlink) + github_link = get_github_link(needlink) assert new_hash in github_link - - -# Test error handling -def test_git_operations_with_no_commits(temp_dir): - """Test git operations on repo with no commits.""" - git_dir = temp_dir / "empty_repo" - git_dir.mkdir() - - # Initialize git repo but don't commit anything - subprocess.run(["git", "init"], cwd=git_dir, check=True, capture_output=True) - subprocess.run( - ["git", "config", "user.email", "test@example.com"], cwd=git_dir, check=True - ) - subprocess.run(["git", "config", "user.name", "Test User"], cwd=git_dir, check=True) - - os.chdir(Path(git_dir).absolute()) - # Should raise an exception when trying to get hash - with pytest.raises(Exception): - a = get_current_git_hash(git_dir) - - -def test_git_repo_with_no_remotes(temp_dir): - """Test git repository with no remotes.""" - git_dir = temp_dir / "no_remote_repo" - git_dir.mkdir() - - # Initialize git repo - subprocess.run(["git", "init"], cwd=git_dir, check=True, capture_output=True) - subprocess.run( - ["git", "config", "user.email", "test@example.com"], cwd=git_dir, check=True - ) - subprocess.run(["git", "config", "user.name", "Test User"], cwd=git_dir, check=True) - - # Create a test file and commit - test_file = git_dir / "test_file.py" - test_file.write_text("# Test file\nprint('hello')\n") - subprocess.run(["git", "add", "."], cwd=git_dir, check=True) - subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=git_dir, check=True) - os.chdir(git_dir) - - # Should raise an exception when trying to get repo info - with pytest.raises(AssertionError): - get_github_repo_info(git_dir) diff --git a/src/extensions/score_source_code_linker/tests/test_source_link.py b/src/extensions/score_source_code_linker/tests/test_source_link.py index 6c40f5e31..8066000c1 100644 --- a/src/extensions/score_source_code_linker/tests/test_source_link.py +++ b/src/extensions/score_source_code_linker/tests/test_source_link.py @@ -255,7 +255,7 @@ def example_source_link_text_non_existent(sphinx_base_dir): { "TREQ_ID_200": [ NeedLink( - file=Path(f"src/bad_implementation.py"), + file=Path("src/bad_implementation.py"), line=2, tag="#" + " req-Id:", need="TREQ_ID_200", @@ -266,16 +266,16 @@ def example_source_link_text_non_existent(sphinx_base_dir): ] -def make_source_link(ws_root: Path, needlinks): +def make_source_link(needlinks): return ", ".join( - f"{get_github_link(ws_root, n)}<>{n.file}:{n.line}" for n in needlinks + f"{get_github_link(n)}<>{n.file}:{n.line}" for n in needlinks ) def compare_json_files(file1: Path, golden_file: Path): - with open(file1, "r") as f1: + with open(file1) as f1: json1 = json.load(f1, object_hook=needlink_test_decoder) - with open(golden_file, "r") as f2: + with open(golden_file) as f2: json2 = json.load(f2, object_hook=needlink_test_decoder) assert len(json1) == len(json2), ( f"{file1}'s lenth are not the same as in the golden file lenght. " @@ -315,7 +315,7 @@ def test_source_link_integration_ok( assert f"TREQ_ID_{i}" in needs_data need_as_dict = cast(dict[str, object], needs_data[f"TREQ_ID_{i}"]) expected_link = make_source_link( - ws_root, example_source_link_text_all_ok[f"TREQ_ID_{i}"] + example_source_link_text_all_ok[f"TREQ_ID_{i}"] ) # extra_options are only available at runtime # Compare contents, regardless of order. diff --git a/src/helper_lib/BUILD b/src/helper_lib/BUILD new file mode 100644 index 000000000..8e0e16b26 --- /dev/null +++ b/src/helper_lib/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2025 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("@aspect_rules_py//py:defs.bzl", "py_library") +load("@pip_process//:requirements.bzl", "all_requirements") +load("@score_python_basics//:defs.bzl", "score_py_pytest") + +py_library( + name = "helper_lib", + srcs = ["__init__.py"], + imports = ["."], + visibility = ["//visibility:public"], +) + +score_py_pytest( + name = "helper_lib_tests", + size = "small", + srcs = ["test_helper_lib.py"], + deps = [ + ":helper_lib", + ] + all_requirements, +) diff --git a/src/helper_lib/__init__.py b/src/helper_lib/__init__.py new file mode 100644 index 000000000..11988e53a --- /dev/null +++ b/src/helper_lib/__init__.py @@ -0,0 +1,159 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +import subprocess +from pathlib import Path + +from sphinx_needs.logging import get_logger + +LOGGER = get_logger(__name__) + + +def find_ws_root() -> Path | None: + """ + Find the current MODULE.bazel workspace root directory. + + Execution context behavior: + - 'bazel run' => ✅ Full workspace path + - 'bazel build' => ❌ None (sandbox isolation) + - 'direct sphinx' => ❌ None (no Bazel environment) + """ + ws_dir = os.environ.get("BUILD_WORKSPACE_DIRECTORY", None) + return Path(ws_dir) if ws_dir else None + + +def find_git_root() -> Path | None: + """ + Find the git root directory, starting from workspace root or current directory. + + Execution context behavior: + - 'bazel run' => ✅ Git root path (starts from workspace) + - 'bazel build' => ❌ None (sandbox has no .git) + - 'direct sphinx' => ✅ Git root path (fallback to cwd) + """ + start_path = find_ws_root() + if start_path is None: + start_path = Path.cwd() + git_root = Path(start_path).resolve() + while not (git_root / ".git").exists(): + git_root = git_root.parent + if git_root == Path("/"): + return None + return git_root + + +def parse_remote_git_output(str_line: str) -> str: + """ + Parse git remote output and extract / format. + + Example: + Input: 'origin git@github.com:MaximilianSoerenPollak/docs-as-code.git' + Output: 'MaximilianSoerenPollak/docs-as-code' + """ + if len(str_line.split()) < 2: + LOGGER.warning( + f"Got wrong input line from 'get_github_repo_info'. Input: {str_line}. " + + "Expected example: 'origin git@github.com:user/repo.git'" + ) + return "" + url = str_line.split()[1] # Get the URL part + # Handle SSH format (git@github.com:user/repo.git) Get part after github.com/ + path = url.split(":")[1] if url.startswith("git@") else "/".join(url.split("/")[3:]) + return path.replace(".git", "") + + +def get_github_repo_info(git_root_cwd: Path) -> str: + """ + Extract GitHub repository info from git remotes. + + Execution context behavior: + - Works consistently across all contexts when given valid git directory + - Fails only when input path has no git repository + + Args: + git_root_cwd: Path to directory containing .git folder + + Returns: + Repository in format 'user/repo' or 'org/repo' + """ + process = subprocess.run( + ["git", "remote", "-v"], capture_output=True, text=True, cwd=git_root_cwd + ) + repo = "" + for line in process.stdout.split("\n"): + if "origin" in line and "(fetch)" in line: + repo = parse_remote_git_output(line) + break + else: + # If we do not find 'origin' we just take the first line + LOGGER.info( + "Did not find origin remote name. Will now take first result from:" + + "'git remote -v'" + ) + repo = parse_remote_git_output(process.stdout.split("\n")[0]) + assert repo != "", ( + "Remote repository is not defined. Make sure you have a remote set. " + + "Check this via 'git remote -v'" + ) + return repo + + +def get_github_base_url() -> str: + """ + Generate GitHub base URL for the current repository. + + Execution context behavior: + - 'bazel run' => ✅ Correct GitHub URL + - 'bazel build' => ⚠️ Uses Path() fallback when git_root is None + - 'direct sphinx' => ✅ Correct GitHub URL + + Returns: + GitHub URL in format 'https://github.com/user/repo' + """ + passed_git_root = find_git_root() + print("THIS IS PASSED GIT ROOT IN GH BASE URL", passed_git_root) + if passed_git_root is None: + passed_git_root = Path() + repo_info = get_github_repo_info(passed_git_root) + return f"https://github.com/{repo_info}" + + +def get_current_git_hash(git_root: Path) -> str: + """ + Get the current git commit hash. + + Execution context behavior: + - Works consistently across all contexts when given valid git directory + - Fails only when input path has no git repository + + Args: + git_root: Path to directory containing .git folder + + Returns: + Full commit hash (40 character hex string) + """ + try: + result = subprocess.run( + ["git", "log", "-n", "1", "--pretty=format:%H"], + cwd=git_root, + capture_output=True, + check=True, + ) + decoded_result = result.stdout.strip().decode() + + assert all(c in "0123456789abcdef" for c in decoded_result) + return decoded_result + except Exception as e: + LOGGER.warning(f"Unexpected error: {git_root}", exc_info=e) + raise diff --git a/src/helper_lib/test_helper_lib.py b/src/helper_lib/test_helper_lib.py new file mode 100644 index 000000000..78042cb00 --- /dev/null +++ b/src/helper_lib/test_helper_lib.py @@ -0,0 +1,70 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +import subprocess +import tempfile +from pathlib import Path + +import pytest + +from src.helper_lib import get_current_git_hash, get_github_repo_info + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for tests.""" + with tempfile.TemporaryDirectory() as temp_dir: + yield Path(temp_dir) + + +# Test error handling +def test_git_operations_with_no_commits(temp_dir): + """Test git operations on repo with no commits.""" + git_dir = temp_dir / "empty_repo" + git_dir.mkdir() + + # Initialize git repo but don't commit anything + subprocess.run(["git", "init"], cwd=git_dir, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], cwd=git_dir, check=True + ) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=git_dir, check=True) + + os.chdir(Path(git_dir).absolute()) + # Should raise an exception when trying to get hash + with pytest.raises(Exception): + get_current_git_hash(git_dir) + + +def test_git_repo_with_no_remotes(temp_dir): + """Test git repository with no remotes.""" + git_dir = temp_dir / "no_remote_repo" + git_dir.mkdir() + + # Initialize git repo + subprocess.run(["git", "init"], cwd=git_dir, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], cwd=git_dir, check=True + ) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=git_dir, check=True) + + # Create a test file and commit + test_file = git_dir / "test_file.py" + test_file.write_text("# Test file\nprint('hello')\n") + subprocess.run(["git", "add", "."], cwd=git_dir, check=True) + subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=git_dir, check=True) + os.chdir(git_dir) + + # Should raise an exception when trying to get repo info + with pytest.raises(AssertionError): + get_github_repo_info(git_dir) diff --git a/src/tests/test_consumer.py b/src/tests/test_consumer.py index b5e93ae31..ecf046f12 100644 --- a/src/tests/test_consumer.py +++ b/src/tests/test_consumer.py @@ -24,9 +24,7 @@ from rich.table import Table from src.extensions.score_source_code_linker import get_github_base_url -from src.extensions.score_source_code_linker.generate_source_code_links_json import ( - find_git_root, -) +from src.helper_lib import find_git_root """ This script's main usecase is to test consumers of Docs-As-Code with @@ -120,10 +118,9 @@ def sphinx_base_dir(tmp_path_factory: TempPathFactory, pytestconfig) -> Path: temp_dir = tmp_path_factory.mktemp("testing_dir") print(f"[blue]Using temporary directory: {temp_dir}[/blue]") return temp_dir - else: - CACHE_DIR.mkdir(parents=True, exist_ok=True) - print(f"[green]Using persistent cache directory: {CACHE_DIR}[/green]") - return CACHE_DIR + CACHE_DIR.mkdir(parents=True, exist_ok=True) + print(f"[green]Using persistent cache directory: {CACHE_DIR}[/green]") + return CACHE_DIR def get_current_git_commit(curr_path: Path): @@ -175,7 +172,7 @@ def replace_bazel_dep_with_local_override(module_content: str) -> str: """ """ # Pattern to match the bazel_dep line - pattern = rf'bazel_dep\(name = "score_docs_as_code", version = "[^"]+"\)' + pattern = r'bazel_dep\(name = "score_docs_as_code", version = "[^"]+"\)' # Replacement with local_path_override replacement = """bazel_dep(name = "score_docs_as_code", version = "0.0.0") @@ -190,7 +187,7 @@ def replace_bazel_dep_with_local_override(module_content: str) -> str: def replace_bazel_dep_with_git_override( module_content: str, git_hash: str, gh_url: str ) -> str: - pattern = rf'bazel_dep\(name = "score_docs_as_code", version = "[^"]+"\)' + pattern = r'bazel_dep\(name = "score_docs_as_code", version = "[^"]+"\)' replacement = f'''bazel_dep(name = "score_docs_as_code", version = "0.0.0") git_override( @@ -343,9 +340,8 @@ def analyze_build_success(BR: BuildOutput) -> tuple[bool, str]: if logger == "[NO SPECIFIC LOGGER]": # Always ignore these continue - else: - # Any other logger is critical/not ignored - critical_warnings.extend(warnings) + # Any other logger is critical/not ignored + critical_warnings.extend(warnings) if critical_warnings: return False, f"Found {len(critical_warnings)} critical warnings" @@ -483,7 +479,7 @@ def setup_test_environment(sphinx_base_dir, pytestconfig): """Set up the test environment and return necessary paths and metadata.""" os.chdir(sphinx_base_dir) curr_path = Path(__file__).parent - git_root = find_git_root(curr_path) + git_root = find_git_root() verbosity = pytestconfig.get_verbosity() @@ -495,7 +491,7 @@ def setup_test_environment(sphinx_base_dir, pytestconfig): assert False, "Git root was none" # Get GitHub URL and current hash for git override - gh_url = get_github_base_url(git_root) + gh_url = get_github_base_url() current_hash = get_current_git_commit(curr_path) if verbosity >= 2: @@ -562,7 +558,7 @@ def prepare_repo_overrides(repo_name, git_url, current_hash, gh_url, use_cache=T os.chdir(repo_name) # Read original MODULE.bazel - with open("MODULE.bazel", "r") as f: + with open("MODULE.bazel") as f: module_orig = f.read() # Prepare override versions From 4fa2e30e0443566f00d33455ab1a90521eab05de Mon Sep 17 00:00:00 2001 From: Maximilian Pollak Date: Thu, 14 Aug 2025 23:17:00 +0200 Subject: [PATCH 2/5] Formatting --- src/extensions/score_source_code_linker/__init__.py | 12 +++++------- .../tests/test_source_link.py | 4 +--- src/helper_lib/__init__.py | 6 +++--- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 272f79daa..ce6fe2461 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -27,8 +27,6 @@ from sphinx_needs.logging import get_logger from src.extensions.score_source_code_linker.generate_source_code_links_json import ( - find_git_root, - find_ws_root, generate_source_code_links_json, ) from src.extensions.score_source_code_linker.needlinks import ( @@ -37,6 +35,8 @@ load_source_code_links_json, ) from src.helper_lib import ( + find_git_root, + find_ws_root, get_current_git_hash, get_github_base_url, ) @@ -65,7 +65,7 @@ def setup_once(app: Sphinx, config: Config): LOGGER.debug(f"DEBUG: Git root is {find_git_root()}") # Run only for local files! - # ws_root is not set when running on any on bazel run command repositories (dependencies) + # ws_root is not set when running on any on bazel run command repositories (dependencies) ws_root = find_ws_root() if not ws_root: return @@ -146,13 +146,11 @@ def group_by_need(source_code_links: list[NeedLink]) -> dict[str, list[NeedLink] return source_code_links_by_need -def get_github_link( - needlink: NeedLink = DefaultNeedLink() -) -> str: +def get_github_link(needlink: NeedLink = DefaultNeedLink()) -> str: passed_git_root = find_git_root() if passed_git_root is None: passed_git_root = Path() - base_url = get_github_base_url() + base_url = get_github_base_url() current_hash = get_current_git_hash(passed_git_root) return f"{base_url}/blob/{current_hash}/{needlink.file}#L{needlink.line}" diff --git a/src/extensions/score_source_code_linker/tests/test_source_link.py b/src/extensions/score_source_code_linker/tests/test_source_link.py index 8066000c1..21f691797 100644 --- a/src/extensions/score_source_code_linker/tests/test_source_link.py +++ b/src/extensions/score_source_code_linker/tests/test_source_link.py @@ -267,9 +267,7 @@ def example_source_link_text_non_existent(sphinx_base_dir): def make_source_link(needlinks): - return ", ".join( - f"{get_github_link(n)}<>{n.file}:{n.line}" for n in needlinks - ) + return ", ".join(f"{get_github_link(n)}<>{n.file}:{n.line}" for n in needlinks) def compare_json_files(file1: Path, golden_file: Path): diff --git a/src/helper_lib/__init__.py b/src/helper_lib/__init__.py index 11988e53a..615eae54c 100644 --- a/src/helper_lib/__init__.py +++ b/src/helper_lib/__init__.py @@ -83,7 +83,7 @@ def get_github_repo_info(git_root_cwd: Path) -> str: Args: git_root_cwd: Path to directory containing .git folder - + Returns: Repository in format 'user/repo' or 'org/repo' """ @@ -115,7 +115,7 @@ def get_github_base_url() -> str: Execution context behavior: - 'bazel run' => ✅ Correct GitHub URL - - 'bazel build' => ⚠️ Uses Path() fallback when git_root is None + - 'bazel build' => ⚠️ Uses Path() fallback when git_root is None - 'direct sphinx' => ✅ Correct GitHub URL Returns: @@ -139,7 +139,7 @@ def get_current_git_hash(git_root: Path) -> str: Args: git_root: Path to directory containing .git folder - + Returns: Full commit hash (40 character hex string) """ From d334d95fb7df0f573cfded183b8f8cc7d6410177 Mon Sep 17 00:00:00 2001 From: Maximilian Pollak Date: Thu, 14 Aug 2025 23:19:31 +0200 Subject: [PATCH 3/5] Fixing imports --- .../score_source_code_linker/tests/test_source_link.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/extensions/score_source_code_linker/tests/test_source_link.py b/src/extensions/score_source_code_linker/tests/test_source_link.py index 21f691797..3e0b0ed68 100644 --- a/src/extensions/score_source_code_linker/tests/test_source_link.py +++ b/src/extensions/score_source_code_linker/tests/test_source_link.py @@ -26,10 +26,8 @@ from test_requirement_links import needlink_test_decoder from src.extensions.score_source_code_linker import get_github_base_url, get_github_link -from src.extensions.score_source_code_linker.generate_source_code_links_json import ( - find_ws_root, -) from src.extensions.score_source_code_linker.needlinks import NeedLink +from src.helper_lib import find_ws_root @pytest.fixture() From 9a1bad5b43896ba74f96ad7d8eb009b9d1cab69c Mon Sep 17 00:00:00 2001 From: Maximilian Pollak Date: Thu, 14 Aug 2025 23:44:27 +0200 Subject: [PATCH 4/5] Fixed consumer_tests & removed print --- src/helper_lib/__init__.py | 1 - src/tests/test_consumer.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/helper_lib/__init__.py b/src/helper_lib/__init__.py index 615eae54c..08d366ef3 100644 --- a/src/helper_lib/__init__.py +++ b/src/helper_lib/__init__.py @@ -122,7 +122,6 @@ def get_github_base_url() -> str: GitHub URL in format 'https://github.com/user/repo' """ passed_git_root = find_git_root() - print("THIS IS PASSED GIT ROOT IN GH BASE URL", passed_git_root) if passed_git_root is None: passed_git_root = Path() repo_info = get_github_repo_info(passed_git_root) diff --git a/src/tests/test_consumer.py b/src/tests/test_consumer.py index ecf046f12..6be41035d 100644 --- a/src/tests/test_consumer.py +++ b/src/tests/test_consumer.py @@ -477,29 +477,29 @@ def run_test_commands(): def setup_test_environment(sphinx_base_dir, pytestconfig): """Set up the test environment and return necessary paths and metadata.""" - os.chdir(sphinx_base_dir) - curr_path = Path(__file__).parent git_root = find_git_root() + gh_url = get_github_base_url() + current_hash = get_current_git_commit(git_root) + os.chdir(Path(sphinx_base_dir).absolute()) + #curr_path = Path(__file__).parent verbosity = pytestconfig.get_verbosity() if verbosity >= 2: - print(f"[DEBUG] curr_path: {curr_path}") + #print(f"[DEBUG] curr_path: {curr_path}") print(f"[DEBUG] git_root: {git_root}") if git_root is None: assert False, "Git root was none" # Get GitHub URL and current hash for git override - gh_url = get_github_base_url() - current_hash = get_current_git_commit(curr_path) if verbosity >= 2: print(f"[DEBUG] gh_url: {gh_url}") print(f"[DEBUG] current_hash: {current_hash}") print( "[DEBUG] Working directory has uncommitted changes: " - f"{has_uncommitted_changes(curr_path)}" + f"{has_uncommitted_changes(git_root)}" ) # Create symlink for local docs-as-code From 09cd960235cb0724ef51245d93780851a7222f5c Mon Sep 17 00:00:00 2001 From: Maximilian Pollak Date: Thu, 14 Aug 2025 23:46:11 +0200 Subject: [PATCH 5/5] Formatting --- src/tests/test_consumer.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/tests/test_consumer.py b/src/tests/test_consumer.py index 6be41035d..6c722d25f 100644 --- a/src/tests/test_consumer.py +++ b/src/tests/test_consumer.py @@ -478,20 +478,18 @@ def run_test_commands(): def setup_test_environment(sphinx_base_dir, pytestconfig): """Set up the test environment and return necessary paths and metadata.""" git_root = find_git_root() + if git_root is None: + assert False, "Git root was none" gh_url = get_github_base_url() current_hash = get_current_git_commit(git_root) + os.chdir(Path(sphinx_base_dir).absolute()) - #curr_path = Path(__file__).parent verbosity = pytestconfig.get_verbosity() if verbosity >= 2: - #print(f"[DEBUG] curr_path: {curr_path}") print(f"[DEBUG] git_root: {git_root}") - if git_root is None: - assert False, "Git root was none" - # Get GitHub URL and current hash for git override if verbosity >= 2: