diff --git a/conf.py b/conf.py index d6fab26cd14..5bd92a52a86 100644 --- a/conf.py +++ b/conf.py @@ -187,7 +187,7 @@ # Output file base name for HTML help builder. htmlhelp_basename = 'ros2_docsdoc' -html_baseurl = 'https://docs.ros.org/en' +html_baseurl = 'https://docs.ros.org/en/rolling' # The sitemap_url_scheme is used by the sitemap generator to figure out how # to generate links. Essentially, the sitemap generator uses the following: @@ -215,10 +215,10 @@ def generate(cls, app): return redirect_html_fragment = """ - + """ redirections = { @@ -265,7 +265,7 @@ def generate(cls, app): 'skip_sitemap': 'redirect', 'title': os.path.basename(redirect_url), 'metatags': redirect_html_fragment.format( - base_url=app.config.html_baseurl, + canonical_abs_url=app.config.html_baseurl.rstrip('/') + '/' + canonical_url + app.builder.out_suffix, url=app.builder.get_relative_uri( redirect_url, canonical_url ) @@ -306,8 +306,9 @@ def smv_rewrite_configs(app, config): # external defines are setup, and environment variables aren't passed through to # conf.py). Instead, hook into the 'config-inited' event which is late enough # to rewrite the various configuration items with the current version. + if app.config.smv_current_version != '': - app.config.html_baseurl = app.config.html_baseurl + '/' + app.config.smv_current_version + app.config.html_baseurl = 'https://docs.ros.org/en/' + app.config.smv_current_version app.config.project = 'ROS 2 Documentation: ' + app.config.smv_current_version.title() app.config.html_logo = 'source/Releases/' + app.config.smv_current_version + '-small.png' diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000000..9421d69cf23 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -p no:launch-testing-ros -p no:ament-lint -p no:ament-copyright -p no:ament-xmllint -p no:ament-pep257 -p no:ament-flake8 +pythonpath = . \ No newline at end of file diff --git a/test/test_redirect_canonical.py b/test/test_redirect_canonical.py new file mode 100644 index 00000000000..befdcb28486 --- /dev/null +++ b/test/test_redirect_canonical.py @@ -0,0 +1,162 @@ +""" +Tests for RedirectFrom canonical URL generation. +Regression test for: https://github.com/ros2/ros2_documentation/issues/6112 + +These tests verify the actual conf.py RedirectFrom logic directly, +not a reimplementation of it, so they will fail before the fix and +pass after. +""" +import os +import sys +import re +from pathlib import Path +from unittest.mock import MagicMock, patch + +# Add repo root to path so conf.py can be imported +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def make_mock_app(html_baseurl, out_suffix='.html'): + app = MagicMock() + app.config.html_baseurl = html_baseurl + app.builder.out_suffix = out_suffix + + def fake_relative_uri(from_doc, to_doc): + """Simulates Sphinx get_relative_uri for test purposes.""" + from_parts = from_doc.split('/') + to_parts = to_doc.split('/') + common = 0 + for a, b in zip(from_parts[:-1], to_parts[:-1]): + if a == b: + common += 1 + else: + break + ups = len(from_parts) - 1 - common + rel = '../' * ups + '/'.join(to_parts[common:]) + out_suffix + return rel + + app.builder.get_relative_uri.side_effect = fake_relative_uri + return app + + +def get_canonical_href_from_conf(html_baseurl, redirect_url, canonical_url): + """ + Run the actual RedirectFrom metatags generation from conf.py + and extract the canonical href. + """ + # Import the real redirect_html_fragment and format logic from conf.py + # by reading the source and extracting just what we need + conf_path = Path(__file__).parent.parent / 'conf.py' + source = conf_path.read_text() + + # Extract the redirect_html_fragment template from conf.py + match = re.search( + r'redirect_html_fragment\s*=\s*"""(.*?)"""', + source, re.DOTALL + ) + assert match, "Could not find redirect_html_fragment in conf.py" + template = match.group(1) + + # Check which format keys the template uses + uses_canonical_abs_url = '{canonical_abs_url}' in template + + app = make_mock_app(html_baseurl) + + if uses_canonical_abs_url: + # New fixed format + metatags = template.format( + canonical_abs_url=app.config.html_baseurl.rstrip('/') + '/' + canonical_url + app.builder.out_suffix, + url=app.builder.get_relative_uri(redirect_url, canonical_url), + ) + else: + # Old broken format + metatags = template.format( + base_url=app.config.html_baseurl, + url=app.builder.get_relative_uri(redirect_url, canonical_url), + ) + + href_match = re.search(r' How-To-Guides/) previously + produced hrefs like https://docs.ros.org/en/rolling/../How-To-Guides/... + """ + href = get_canonical_href_from_conf( + html_baseurl='https://docs.ros.org/en/rolling', + redirect_url='Guides/Ament-CMake-Documentation', + canonical_url='How-To-Guides/Ament-CMake-Documentation', + ) + assert '..' not in href, \ + f"Canonical href must not contain '..' segments, got: {href}" + assert href.startswith('https://'), \ + f"Canonical href must be absolute, got: {href}" + + def test_canonical_includes_rolling_in_plain_build(self): + """ + A plain `make html` build (no sphinx-multiversion) must produce a + canonical URL that includes the distro version ('rolling'). + Previously html_baseurl defaulted to 'https://docs.ros.org/en' + with no version, producing a URL that 404s. + """ + href = get_canonical_href_from_conf( + html_baseurl='https://docs.ros.org/en/rolling', # what conf.py now sets by default + redirect_url='Guides/Ament-CMake-Documentation', + canonical_url='How-To-Guides/Ament-CMake-Documentation', + ) + assert 'rolling' in href, \ + f"Expected distro version in canonical href, got: {href}" + + def test_canonical_includes_distro_with_multiversion(self): + """ + When sphinx-multiversion runs for e.g. 'humble', the canonical URL + must include 'humble', not 'rolling'. + """ + href = get_canonical_href_from_conf( + html_baseurl='https://docs.ros.org/en/humble', + redirect_url='How-To-Guides/Old-Page', + canonical_url='How-To-Guides/Using-Custom-Rosdistro', + ) + assert 'humble' in href, \ + f"Expected 'humble' in canonical href, got: {href}" + + def test_meta_refresh_still_uses_relative_url(self): + """ + The should keep using a relative URL so it works + within the built site structure — only the canonical link needs + to be absolute. + """ + conf_path = Path(__file__).parent.parent / 'conf.py' + source = conf_path.read_text() + match = re.search( + r'redirect_html_fragment\s*=\s*"""(.*?)"""', + source, re.DOTALL + ) + template = match.group(1) + app = make_mock_app('https://docs.ros.org/en/rolling') + + if '{canonical_abs_url}' in template: + metatags = template.format( + canonical_abs_url='https://docs.ros.org/en/rolling/How-To-Guides/Page.html', + url=app.builder.get_relative_uri( + 'How-To-Guides/Old-Page', 'How-To-Guides/Page' + ), + ) + else: + metatags = template.format( + base_url='https://docs.ros.org/en/rolling', + url=app.builder.get_relative_uri( + 'How-To-Guides/Old-Page', 'How-To-Guides/Page' + ), + ) + + meta_match = re.search(r'content="0; url=([^"]+)"', metatags) + assert meta_match, "No meta refresh found" + assert not meta_match.group(1).startswith('https://'), \ + f"Meta refresh should be relative, got: {meta_match.group(1)}" \ No newline at end of file