Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ load(
"DependableElementInfo",
"DependableElementLobsterInfo",
"FeatureRequirementsInfo",
"SphinxIndexFileInfo",
"SphinxModuleInfo",
"SphinxNeedsInfo",
"SphinxSourcesInfo",
Expand Down Expand Up @@ -967,6 +968,7 @@ def _dependable_element_index_impl(ctx):

return [
DefaultInfo(files = depset(output_files)),
SphinxIndexFileInfo(index_file = index_rst),
CertifiedScope(transitive_scopes = depset(transitive = collected_certified_scopes)),
DependableElementInfo(
integrity_level = ctx.attr.integrity_level,
Expand Down
28 changes: 21 additions & 7 deletions bazel/rules/rules_score/private/sphinx_module.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,21 @@
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
load("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo")
load("//bazel/rules/rules_score:providers.bzl", "FilteredExecpathInfo", "SphinxModuleInfo", "SphinxNeedsInfo")
load("//bazel/rules/rules_score:providers.bzl", "FilteredExecpathInfo", "SphinxIndexFileInfo", "SphinxModuleInfo", "SphinxNeedsInfo")

def _get_index_file(ctx):
"""Extract the index file from the index attribute.

If the target provides SphinxIndexFileInfo, use that. Otherwise expect
exactly one file and use it directly.
"""
target = ctx.attr.index
if SphinxIndexFileInfo in target:
return target[SphinxIndexFileInfo].index_file
files = target.files.to_list()
if len(files) != 1:
fail("'index' target must provide SphinxIndexFileInfo or produce exactly one file, got %d files" % len(files))
return files[0]

def _create_config_py(ctx):
"""Get or generate the conf.py configuration file.
Expand Down Expand Up @@ -70,7 +84,7 @@ def _score_needs_impl(ctx):
needs_inputs = ctx.files.srcs + [config_file]
needs_args = [
"--index_file",
ctx.attr.index.files.to_list()[0].path,
_get_index_file(ctx).path,
"--output_dir",
needs_output.dirname,
"--config",
Expand Down Expand Up @@ -181,12 +195,12 @@ def _score_html_impl(ctx):
# Sphinx only accepts a single directory to read its doc sources from.
# Because plain files and generated files are in different directories,
# we need to merge the two into a single directory.
for orig_file in ctx.files.srcs:
_relocate(orig_file)
index_source_file = _get_index_file(ctx)
relocated_index_file = ""
for input_file in sphinx_source_files:
if input_file.path.endswith("/index.rst"):
relocated_index_file = input_file.path
for orig_file in ctx.files.srcs:
dest = _relocate(orig_file)
if orig_file.path == index_source_file.path:
relocated_index_file = dest.path

# Build HTML with external needs
html_inputs = sphinx_source_files + ctx.files.needs + filtered_files + [config_file, needs_external_needs_json]
Expand Down
7 changes: 7 additions & 0 deletions bazel/rules/rules_score/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ DependableElementLobsterInfo = provider(
},
)

SphinxIndexFileInfo = provider(
doc = "Provider carrying the single index.rst file for a Sphinx documentation module.",
fields = {
"index_file": "File – the index.rst file to use as the Sphinx master document.",
},
)

SphinxModuleInfo = provider(
doc = "Provider for Sphinx HTML module documentation",
fields = {
Expand Down
29 changes: 29 additions & 0 deletions bazel/rules/rules_score/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ load(
)
load(
":seooc_test.bzl",
"seooc_all_sources_relocated_test",
"seooc_artifacts_copied_test",
"seooc_index_file_provider_test",
"seooc_index_generation_test",
"seooc_multi_index_files_exist_test",
"seooc_needs_provider_test",
"seooc_sphinx_entry_point_is_root_index_test",
"seooc_sphinx_module_generated_test",
)
load(
Expand Down Expand Up @@ -496,6 +500,27 @@ seooc_index_generation_test(
target_under_test = ":seooc_test_lib_index",
)

# Regression tests: correct index.rst resolution when multiple index.rst files exist
seooc_multi_index_files_exist_test(
name = "seooc_tests_multi_index_files_exist",
target_under_test = ":test_dependable_element_index",
)

seooc_sphinx_entry_point_is_root_index_test(
name = "seooc_tests_sphinx_entry_point_is_root_index",
target_under_test = ":test_dependable_element_doc",
)

seooc_all_sources_relocated_test(
name = "seooc_tests_all_sources_relocated",
target_under_test = ":test_dependable_element_doc",
)

seooc_index_file_provider_test(
name = "seooc_tests_index_file_provider",
target_under_test = ":test_dependable_element_index",
)

# ============================================================================
# Test Suites
# ============================================================================
Expand Down Expand Up @@ -560,9 +585,13 @@ sphinx_module_providers_test_suite(name = "sphinx_module_providers_tests")
test_suite(
name = "seooc_tests",
tests = [
":seooc_tests_all_sources_relocated",
":seooc_tests_artifacts_copied",
":seooc_tests_index_file_provider",
":seooc_tests_index_generation",
":seooc_tests_multi_index_files_exist",
":seooc_tests_needs_provider",
":seooc_tests_sphinx_entry_point_is_root_index",
":seooc_tests_sphinx_module_generated",
],
)
Expand Down
203 changes: 202 additions & 1 deletion bazel/rules/rules_score/test/seooc_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Tests the SEooC (Safety Element out of Context) functionality including:
"""

load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
load("@score_tooling//bazel/rules/rules_score:providers.bzl", "SphinxModuleInfo", "SphinxNeedsInfo")
load("@score_tooling//bazel/rules/rules_score:providers.bzl", "SphinxIndexFileInfo", "SphinxModuleInfo", "SphinxNeedsInfo")

def _seooc_index_generation_test_impl(ctx):
"""Test that dependable_element generates proper index.rst file."""
Expand Down Expand Up @@ -115,3 +115,204 @@ def _seooc_needs_provider_test_impl(ctx):
seooc_needs_provider_test = analysistest.make(
impl = _seooc_needs_provider_test_impl,
)

# ============================================================================
# Regression tests: correct index.rst resolution (multi-index scenario)
#
# These tests guard against a bug where _score_html_impl scanned all relocated
# source files for any path ending with "/index.rst" and used the last match.
# A dependable_element with components generates both:
# <name>/index.rst (root – correct Sphinx entry point)
# <name>/components/index.rst (sub-page – must NOT be the entry point)
# The old code would pick the sub-page as the Sphinx entry point.
# ============================================================================

def _seooc_multi_index_files_exist_test_impl(ctx):
"""
Given a dependable_element with at least one component,
When the _index rule generates its output file set,
Then both a root index.rst and a components/index.rst must be present,
confirming the fixture exercises the multi-index scenario.
"""
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)

# When: collect all output paths
files = target_under_test[DefaultInfo].files.to_list()
paths = [f.path for f in files]

# Then: a components/index.rst sub-page exists
components_index_paths = [p for p in paths if p.endswith("components/index.rst")]
asserts.true(
env,
len(components_index_paths) >= 1,
"Expected a components/index.rst to be generated (fixture must have at least one component)",
)

# Then: a root index.rst (not inside components/) also exists
root_index_paths = [
p
for p in paths
if p.endswith("index.rst") and "components/" not in p
]
asserts.true(
env,
len(root_index_paths) >= 1,
"Expected a root index.rst (outside components/) to be generated",
)

return analysistest.end(env)

seooc_multi_index_files_exist_test = analysistest.make(
impl = _seooc_multi_index_files_exist_test_impl,
)

def _seooc_sphinx_entry_point_is_root_index_test_impl(ctx):
"""
Given a dependable_element whose source tree contains both a root index.rst
and a components/index.rst,
When the Sphinx HTML build action is constructed,
Then the --index_file argument passed to Sphinx must point to the root
index.rst and must NOT point to the components/index.rst sub-page.
"""
env = analysistest.begin(ctx)

# When: inspect all actions produced for this target
actions = analysistest.target_actions(env)

# Find the Sphinx HTML action: it is a run() action whose outputs contain
# the "_html" directory (declared as <name>/_html).
sphinx_html_action = None
for action in actions:
for output in action.outputs.to_list():
if output.basename == "_html":
sphinx_html_action = action
break
if sphinx_html_action:
break

asserts.true(
env,
sphinx_html_action != None,
"Expected to find the Sphinx HTML build action (output ending in '_html')",
)

# Then: extract the value that follows --index_file in the argument list
argv = sphinx_html_action.argv
index_path = None
for i, arg in enumerate(argv):
if arg == "--index_file" and i + 1 < len(argv):
index_path = argv[i + 1]
break

asserts.true(
env,
index_path != None,
"Sphinx HTML action must contain a --index_file argument",
)

asserts.true(
env,
index_path.endswith("index.rst"),
"The --index_file value must end with index.rst; got: " + str(index_path),
)

asserts.false(
env,
"components/" in index_path,
"The --index_file value must NOT point to components/index.rst; got: " + str(index_path),
)

return analysistest.end(env)

seooc_sphinx_entry_point_is_root_index_test = analysistest.make(
impl = _seooc_sphinx_entry_point_is_root_index_test_impl,
)

def _seooc_all_sources_relocated_test_impl(ctx):
"""
Given a dependable_element that provides source files via SphinxSourcesInfo,
When the Sphinx HTML build action is constructed,
Then all source files must appear as inputs to the action, confirming that
every file is relocated (symlinked) and none are silently dropped.
"""
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)

# When: find the Sphinx HTML action (same selection as the entry-point test)
actions = analysistest.target_actions(env)
sphinx_html_action = None
for action in actions:
for output in action.outputs.to_list():
if output.basename == "_html":
sphinx_html_action = action
break
if sphinx_html_action:
break

asserts.true(
env,
sphinx_html_action != None,
"Expected to find the Sphinx HTML build action (output ending in '_html')",
)

# Then: every .rst / .md / .puml / .json source from the index target's
# output file set must appear as an input to the HTML action.
index_files = target_under_test[DefaultInfo].files.to_list()
source_exts = ("rst", "md", "puml", "plantuml", "json")
expected_sources = [
f
for f in index_files
if f.extension in source_exts
]

action_input_paths = {f.path: True for f in sphinx_html_action.inputs.to_list()}

for src in expected_sources:
asserts.true(
env,
src.path in action_input_paths,
"Source file '{}' was not passed as input to the Sphinx HTML action".format(src.path),
)

return analysistest.end(env)

seooc_all_sources_relocated_test = analysistest.make(
impl = _seooc_all_sources_relocated_test_impl,
)

def _seooc_index_file_provider_test_impl(ctx):
"""
Given a _dependable_element_index target,
When its providers are inspected,
Then SphinxIndexFileInfo must be present and its index_file must be the
root index.rst (not a components/index.rst sub-page).
"""
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)

asserts.true(
env,
SphinxIndexFileInfo in target_under_test,
"_dependable_element_index must provide SphinxIndexFileInfo",
)

index_file = target_under_test[SphinxIndexFileInfo].index_file

asserts.true(
env,
index_file.basename == "index.rst",
"SphinxIndexFileInfo.index_file must be named index.rst; got: " + index_file.basename,
)

asserts.false(
env,
"components/" in index_file.path,
"SphinxIndexFileInfo.index_file must NOT point to components/index.rst; got: " + index_file.path,
)

return analysistest.end(env)

seooc_index_file_provider_test = analysistest.make(
impl = _seooc_index_file_provider_test_impl,
)
Loading