From 97749dada057885a34cdfabcfa29e01b03280b0f Mon Sep 17 00:00:00 2001 From: Markus Bechter Date: Wed, 4 Feb 2026 18:03:45 +0100 Subject: [PATCH] Add recursive unit and component doc generation - Generate RST docs for units and components, including nested ones - Add templates for unit and component documentation - Update index template to include units and components sections - Add test fixtures for nested components and recursive unit collection --- bazel/rules/rules_score/BUILD | 2 + .../private/assumptions_of_use.bzl | 16 +- .../private/dependability_analysis.bzl | 12 +- .../private/dependable_element.bzl | 319 +++++++++++++++++- .../templates/component.template.rst | 20 ++ .../templates/seooc_index.template.rst | 24 +- .../rules_score/templates/unit.template.rst | 19 ++ bazel/rules/rules_score/test/BUILD | 64 ++++ 8 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 bazel/rules/rules_score/templates/component.template.rst create mode 100644 bazel/rules/rules_score/templates/unit.template.rst diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 03f7457..6b53f2f 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -6,6 +6,8 @@ load( exports_files([ "templates/conf.template.py", "templates/seooc_index.template.rst", + "templates/unit.template.rst", + "templates/component.template.rst", ]) # HTML merge tool diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 158b92b..36c584f 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -20,6 +20,7 @@ operating conditions and constraints for a Safety Element out of Context (SEooC) """ load("//bazel/rules/rules_score:providers.bzl", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:component_requirements.bzl", "ComponentRequirementsInfo") load("//bazel/rules/rules_score/private:feature_requirements.bzl", "FeatureRequirementsInfo") # ============================================================================ @@ -55,13 +56,13 @@ def _assumptions_of_use_impl(ctx): # Collect feature requirements providers feature_reqs = [] - for feat_req in ctx.attr.feature_requirement: + for feat_req in ctx.attr.feature_requirements: if FeatureRequirementsInfo in feat_req: feature_reqs.append(feat_req[FeatureRequirementsInfo]) # Collect transitive sphinx sources from feature requirements transitive = [srcs] - for feat_req in ctx.attr.feature_requirement: + for feat_req in ctx.attr.feature_requirements: if SphinxSourcesInfo in feat_req: transitive.append(feat_req[SphinxSourcesInfo].transitive_srcs) @@ -91,11 +92,16 @@ _assumptions_of_use = rule( mandatory = True, doc = "Source files containing Assumptions of Use specifications", ), - "feature_requirement": attr.label_list( + "feature_requirements": attr.label_list( providers = [FeatureRequirementsInfo], mandatory = False, doc = "List of feature_requirements targets that these Assumptions of Use trace to", ), + "component_requirements": attr.label_list( + providers = [ComponentRequirementsInfo], + mandatory = False, + doc = "List of feature_requirements targets that these Assumptions of Use trace to", + ), }, ) @@ -107,6 +113,7 @@ def assumptions_of_use( name, srcs, feature_requirement = [], + component_requirements = [], visibility = None): """Define Assumptions of Use following S-CORE process guidelines. @@ -141,6 +148,7 @@ def assumptions_of_use( _assumptions_of_use( name = name, srcs = srcs, - feature_requirement = feature_requirement, + feature_requirements = feature_requirement, + component_requirements = component_requirements, visibility = visibility, ) diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 05c7f36..dfe8f7d 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -61,7 +61,7 @@ def _dependability_analysis_impl(ctx): # Collect safety analysis providers safety_analysis_infos = [] safety_analysis_files = [] - for sa in ctx.attr.safety_analysis: + for sa in ctx.attr.security_analysis: if SafetyAnalysisInfo in sa: safety_analysis_infos.append(sa[SafetyAnalysisInfo]) safety_analysis_files.append(sa.files) @@ -78,7 +78,7 @@ def _dependability_analysis_impl(ctx): # Collect transitive sphinx sources from safety analysis and architectural design transitive = [all_files] - for sa in ctx.attr.safety_analysis: + for sa in ctx.attr.security_analysis: if SphinxSourcesInfo in sa: transitive.append(sa[SphinxSourcesInfo].transitive_srcs) if ctx.attr.arch_design and SphinxSourcesInfo in ctx.attr.arch_design: @@ -107,7 +107,8 @@ _dependability_analysis = rule( implementation = _dependability_analysis_impl, doc = "Collects dependability analysis documents for S-CORE process compliance", attrs = { - "safety_analysis": attr.label_list( + "security_analysis": attr.label_list( + # TODO: change provider name providers = [SafetyAnalysisInfo], mandatory = False, doc = "List of safety_analysis targets containing FMEA, FMEDA, FTA results", @@ -183,7 +184,10 @@ def dependability_analysis( """ _dependability_analysis( name = name, - safety_analysis = safety_analysis, + # TODO: this needs to be fixed. A security is not a safety_analysis. + # we leave it for now for compatibility reasons until there is alignment on the a + # attributes of a dependability analysis + security_analysis = safety_analysis, dfa = dfa, fmea = fmea, arch_design = arch_design, diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index f193cc4..2704318 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -22,7 +22,9 @@ assumptions of use, requirements, design, and safety analysis. load( "//bazel/rules/rules_score:providers.bzl", + "ComponentInfo", "SphinxSourcesInfo", + "UnitInfo", ) load("//bazel/rules/rules_score/private:sphinx_module.bzl", "sphinx_module") @@ -252,6 +254,271 @@ def _process_deps(ctx): def _get_component_names(components): return [c.label.name for c in components] +def _collect_units_recursive(components, visited_units = None): + """Iteratively collect all units from components, handling nested components. + + Uses a stack-based approach to avoid Starlark recursion limitations. + + Args: + components: List of component targets + visited_units: Dict of unit names already visited (for deduplication) + + Returns: + Dict mapping unit names to unit targets + """ + if visited_units == None: + visited_units = {} + + # Process components iteratively using a work queue approach + # Since Starlark doesn't support while loops, we use a for loop with a large enough range + # and track our own index + to_process = [] + components + + for _ in range(1000): # Max depth to prevent infinite loops + if not to_process: + break + comp_target = to_process.pop(0) + + # Check if this is a component with ComponentInfo + if ComponentInfo in comp_target: + comp_info = comp_target[ComponentInfo] + + # Process nested components + nested_components = comp_info.components.to_list() + for nested in nested_components: + # Check if nested item is a unit or component + if UnitInfo in nested: + unit_name = nested.label.name + if unit_name not in visited_units: + visited_units[unit_name] = nested + elif ComponentInfo in nested: + # Add nested component to queue for processing + to_process.append(nested) + + # Check if this is directly a unit + elif UnitInfo in comp_target: + unit_name = comp_target.label.name + if unit_name not in visited_units: + visited_units[unit_name] = comp_target + + return visited_units + +def _generate_unit_doc(ctx, unit_target, unit_name, template): + """Generate RST documentation for a single unit. + + Args: + ctx: Rule context + unit_target: The unit target + unit_name: Name of the unit + template: Template file for unit documentation + + Returns: + Tuple of (rst_file, list_of_output_files) + """ + unit_info = unit_target[UnitInfo] + + # Create RST file for this unit + unit_rst = ctx.actions.declare_file(ctx.label.name + "/units/" + unit_name + ".rst") + + # Collect design files - unit_design depset contains File objects + design_files = [] + design_refs = [] + if unit_info.unit_design: + doc_files = _filter_doc_files(unit_info.unit_design.to_list()) + + if doc_files: + # Find common directory + common_dir = _find_common_directory(doc_files) + + for f in doc_files: + relative_path = _compute_relative_path(f, common_dir) + output_file = _create_artifact_symlink( + ctx, + "units/" + unit_name + "_design", + f, + relative_path, + ) + design_files.append(output_file) + + if _is_document_file(f): + doc_ref = ("units/" + unit_name + "_design/" + relative_path) \ + .replace(".rst", "") \ + .replace(".md", "") + design_refs.append(" " + doc_ref) + + # Collect implementation target names + impl_names = [] + if unit_info.implementation: + for impl in unit_info.implementation.to_list(): + impl_names.append(impl.label) + + # Collect test target names + test_names = [] + if unit_info.tests: + for test in unit_info.tests.to_list(): + test_names.append(test.label) + + # Generate RST content using template + underline = "=" * (len("Unit: " + unit_name)) + + design_section = "" + if design_refs: + design_section = """Unit Design +----------- + +.. toctree:: + :maxdepth: 2 + +""" + "\n".join(design_refs) + + implementation_section = "" + if impl_names: + impl_list = "\n".join(["- ``" + str(impl) + "``" for impl in impl_names]) + implementation_section = """Implementation +-------------- + +This unit is implemented by the following targets: + +""" + impl_list + + tests_section = "" + if test_names: + test_list = "\n".join(["- ``" + str(test) + "``" for test in test_names]) + tests_section = """Tests +----- + +This unit is verified by the following test targets: + +""" + test_list + + ctx.actions.expand_template( + template = template, + output = unit_rst, + substitutions = { + "{unit_name}": unit_name, + "{underline}": underline, + "{design_section}": design_section, + "{implementation_section}": implementation_section, + "{tests_section}": tests_section, + }, + ) + + return (unit_rst, design_files) + +def _generate_component_doc(ctx, comp_target, comp_name, unit_names, template): + """Generate RST documentation for a single component. + + Args: + ctx: Rule context + comp_target: The component target + comp_name: Name of the component + unit_names: List of unit names that belong to this component + template: Template file for component documentation + + Returns: + Tuple of (rst_file, list_of_output_files) + """ + comp_info = comp_target[ComponentInfo] + + # Create RST file for this component + comp_rst = ctx.actions.declare_file(ctx.label.name + "/components/" + comp_name + ".rst") + + # Collect requirements files - requirements depset contains File objects + req_files = [] + req_refs = [] + if comp_info.requirements: + doc_files = _filter_doc_files(comp_info.requirements.to_list()) + + if doc_files: + # Find common directory + common_dir = _find_common_directory(doc_files) + + for f in doc_files: + relative_path = _compute_relative_path(f, common_dir) + output_file = _create_artifact_symlink( + ctx, + "components/" + comp_name + "_requirements", + f, + relative_path, + ) + req_files.append(output_file) + + if _is_document_file(f): + doc_ref = ("components/" + comp_name + "_requirements/" + relative_path) \ + .replace(".rst", "") \ + .replace(".md", "") + req_refs.append(" " + doc_ref) + + # Collect implementation target names + impl_names = [] + if comp_info.implementation: + for impl in comp_info.implementation.to_list(): + impl_names.append(impl.label) + + # Collect test target names + test_names = [] + if comp_info.tests: + for test in comp_info.tests.to_list(): + test_names.append(test.label) + + # Generate RST content using template + underline = "=" * (len("Component: " + comp_name)) + + requirements_section = "" + if req_refs: + requirements_section = """Component Requirements +---------------------- + +.. toctree:: + :maxdepth: 2 + +""" + "\n".join(req_refs) + + units_section = "" + if unit_names: + unit_links = "\n".join(["- :doc:`../units/" + unit_name + "`" for unit_name in unit_names]) + units_section = """Units +----- + +This component is composed of the following units: + +""" + unit_links + + implementation_section = "" + if impl_names: + impl_list = "\n".join(["- ``" + str(impl) + "``" for impl in impl_names]) + implementation_section = """Implementation +-------------- + +This component includes the following implementation targets: + +""" + impl_list + + tests_section = "" + if test_names: + test_list = "\n".join(["- ``" + str(test) + "``" for test in test_names]) + tests_section = """Tests +----- + +This component is verified by the following test targets: + +""" + test_list + + ctx.actions.expand_template( + template = template, + output = comp_rst, + substitutions = { + "{component_name}": comp_name, + "{underline}": underline, + "{requirements_section}": requirements_section, + "{units_section}": units_section, + "{implementation_section}": implementation_section, + "{tests_section}": tests_section, + }, + ) + + return (comp_rst, req_files) + # ============================================================================ # Index Generation Rule Implementation # ============================================================================ @@ -291,13 +558,45 @@ def _dependable_element_index_impl(ctx): output_files.extend(files) artifacts_by_type[artifact_name] = refs + # Collect all units recursively from components + all_units = _collect_units_recursive(ctx.attr.components) + + # Generate documentation for each unit + unit_refs = [] + for unit_name, unit_target in all_units.items(): + unit_rst, unit_files = _generate_unit_doc(ctx, unit_target, unit_name, ctx.file._unit_template) + output_files.append(unit_rst) + output_files.extend(unit_files) + unit_refs.append(" units/" + unit_name) + + # Generate documentation for each component + component_refs = [] + for comp_target in ctx.attr.components: + if ComponentInfo in comp_target: + comp_info = comp_target[ComponentInfo] + comp_name = comp_info.name + + # Collect units that belong to this component + comp_unit_names = [] + for nested in comp_info.components.to_list(): + if UnitInfo in nested: + comp_unit_names.append(nested.label.name) + elif ComponentInfo in nested: + # For nested components, collect their units recursively + nested_units = _collect_units_recursive([nested]) + comp_unit_names.extend(nested_units.keys()) + + comp_rst, comp_files = _generate_component_doc(ctx, comp_target, comp_name, comp_unit_names, ctx.file._component_template) + output_files.append(comp_rst) + output_files.extend(comp_files) + component_refs.append(" components/" + comp_name) + # Process dependencies (submodules) deps_links = _process_deps(ctx) # Generate index file from template title = ctx.attr.module_name underline = "=" * len(title) - component_names = _get_component_names(ctx.attr.components) # Collect list of components ctx.actions.expand_template( template = ctx.file.template, @@ -306,7 +605,8 @@ def _dependable_element_index_impl(ctx): "{title}": title, "{underline}": underline, "{description}": ctx.attr.description, - "{components}": "\n- ".join(component_names), + "{units}": "\n".join(unit_refs) if unit_refs else " (none)", + "{components}": "\n".join(component_refs) if component_refs else " (none)", "{assumptions_of_use}": "\n ".join(artifacts_by_type["assumptions_of_use"]), "{component_requirements}": "\n ".join(artifacts_by_type["requirements"]), "{architectural_design}": "\n ".join(artifacts_by_type["architectural_design"]), @@ -352,6 +652,10 @@ _dependable_element_index = rule( default = [], doc = "Safety checklists targets or files.", ), + "tests": attr.label_list( + default = [], + doc = "Integration tests for the dependable element.", + ), "checklists": attr.label_list( default = [], doc = "Safety checklists targets or files.", @@ -361,6 +665,16 @@ _dependable_element_index = rule( mandatory = True, doc = "Template file for generating index.rst", ), + "_unit_template": attr.label( + default = Label("//bazel/rules/rules_score:templates/unit.template.rst"), + allow_single_file = True, + doc = "Template for generating unit documentation", + ), + "_component_template": attr.label( + default = Label("//bazel/rules/rules_score:templates/component.template.rst"), + allow_single_file = True, + doc = "Template for generating component documentation", + ), "deps": attr.label_list( default = [], doc = "Dependencies on other dependable element modules (submodules).", @@ -441,6 +755,7 @@ def dependable_element( architectural_design = architectural_design, dependability_analysis = dependability_analysis, checklists = checklists, + tests = tests, deps = deps, testonly = testonly, visibility = ["//visibility:private"], diff --git a/bazel/rules/rules_score/templates/component.template.rst b/bazel/rules/rules_score/templates/component.template.rst new file mode 100644 index 0000000..73c0773 --- /dev/null +++ b/bazel/rules/rules_score/templates/component.template.rst @@ -0,0 +1,20 @@ +.. ******************************************************************************* +.. 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 +.. ******************************************************************************* + +Component: {component_name} +{underline} + +{requirements_section} +{units_section} +{implementation_section} +{tests_section} diff --git a/bazel/rules/rules_score/templates/seooc_index.template.rst b/bazel/rules/rules_score/templates/seooc_index.template.rst index c833b7b..5def2dc 100644 --- a/bazel/rules/rules_score/templates/seooc_index.template.rst +++ b/bazel/rules/rules_score/templates/seooc_index.template.rst @@ -16,6 +16,15 @@ Dependable element: {title} {description} +Architectural Design +-------------------- + +.. toctree:: + :maxdepth: 2 + + {architectural_design} + + Assumptions of Use ------------------ @@ -27,15 +36,22 @@ Assumptions of Use Components ---------- +.. toctree:: + :maxdepth: 1 + {components} -Architectural Design --------------------- + +Units +----- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + +{units} + + - {architectural_design} Dependability Analysis ---------------------- diff --git a/bazel/rules/rules_score/templates/unit.template.rst b/bazel/rules/rules_score/templates/unit.template.rst new file mode 100644 index 0000000..af3efc1 --- /dev/null +++ b/bazel/rules/rules_score/templates/unit.template.rst @@ -0,0 +1,19 @@ +.. ******************************************************************************* +.. 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 +.. ******************************************************************************* + +Unit: {unit_name} +{underline} + +{design_section} +{implementation_section} +{tests_section} diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index f81b2cf..dc78a44 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -213,6 +213,70 @@ dependable_element( tests = [], # Empty for testing ) +# ============================================================================ +# Test Fixtures - Nested Components for Recursive Testing +# ============================================================================ + +# Additional mock implementations +cc_library( + name = "mock_lib3", + srcs = ["fixtures/mock_lib1.cc"], # Reuse same source for testing +) + +cc_test( + name = "test_unit2_tests", + testonly = True, + srcs = ["fixtures/test_unit_test.cc"], + tags = ["manual"], + deps = [":mock_lib3"], +) + +# Second unit that will be shared between components +unit( + name = "test_unit2", + testonly = True, + tests = [":test_unit2_tests"], + unit_design = [":arch_design"], + implementation = [":mock_lib3"], +) + +# Nested component containing unit2 +component( + name = "test_nested_component", + testonly = True, + components = [":test_unit2"], + requirements = [":comp_req"], + tests = [], + implementation = [], +) + +# Parent component containing nested component and shared unit +component( + name = "test_parent_component", + testonly = True, + components = [ + ":test_nested_component", + ":test_unit2", # Same unit appears here and in nested component + ":test_unit", # Different unit + ], + requirements = [":comp_req"], + tests = [], + implementation = [], +) + +# Dependable element with nested components to test recursive collection +dependable_element( + name = "test_dependable_element_nested", + testonly = True, + architectural_design = [":arch_design"], + assumptions_of_use = [":aous"], + components = [":test_parent_component"], + dependability_analysis = [":dependability_analysis_target"], + description = "Test dependable element with nested components for testing recursive unit collection and deduplication", + requirements = [":comp_req"], + tests = [], +) + # ============================================================================ # Test Instantiations - HTML Generation Tests # ============================================================================