diff --git a/.bazelrc b/.bazelrc index 0823a01..3a6ddac 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,2 +1,7 @@ common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ common --registry=https://bcr.bazel.build + +build --java_language_version=17 +build --tool_java_language_version=17 +build --java_runtime_version=remotejdk_17 +build --tool_java_runtime_version=remotejdk_17 diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..2bf50aa --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.3.0 diff --git a/MODULE.bazel b/MODULE.bazel index d193ef2..766145b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -95,3 +95,13 @@ multitool.hub( lockfile = "tools/yamlfmt.lock.json", ) use_repo(multitool, "yamlfmt_hub") + +bazel_dep(name = "score_docs_as_code", version = "2.2.0") +git_override( + module_name = "score_docs_as_code", + commit = "17cf66e3449f0aa9e9fe22fe3ab1fbbffad733cf", + remote = "https://github.com/eclipse-score/docs-as-code.git", +) + +bazel_dep(name = "score_platform", version = "0.5.0") +bazel_dep(name = "score_process", version = "1.3.2") diff --git a/bazel/rules/score_module/BUILD b/bazel/rules/score_module/BUILD new file mode 100644 index 0000000..8647e3a --- /dev/null +++ b/bazel/rules/score_module/BUILD @@ -0,0 +1,51 @@ +load( + "//bazel/rules/score_module:score_module.bzl", + "sphinx_module", +) + +exports_files([ + "templates/conf.template.py", + "templates/seooc_index.template.rst", +]) + +# HTML merge tool +py_binary( + name = "sphinx_html_merge", + srcs = ["src/sphinx_html_merge.py"], + main = "src/sphinx_html_merge.py", + visibility = ["//visibility:public"], +) + +# Sphinx build binary with all required dependencies +py_binary( + name = "score_build", + srcs = ["src/sphinx_wrapper.py"], + data = [], + env = { + "SOURCE_DIRECTORY": "", + "DATA": "", + "ACTION": "check", + }, + main = "src/sphinx_wrapper.py", + visibility = ["//visibility:public"], + deps = [ + "@score_docs_as_code//src:plantuml_for_python", + "@score_docs_as_code//src/extensions/score_sphinx_bundle", + ], +) + +sphinx_module( + name = "score_module_doc", + srcs = glob( + [ + "docs/**/*.rst", + "docs/**/*.puml", + ], + allow_empty = True, + ), + index = "docs/index.rst", + visibility = ["//visibility:public"], + deps = [ + "@score_process//:score_process_module", + ], +) diff --git a/bazel/rules/score_module/docs/index.rst b/bazel/rules/score_module/docs/index.rst new file mode 100644 index 0000000..e1a812f --- /dev/null +++ b/bazel/rules/score_module/docs/index.rst @@ -0,0 +1,359 @@ +SCORE Module Bazel Rules +========================= + +This package provides Bazel build rules for defining and building SCORE documentation modules with integrated Sphinx-based HTML generation. + +.. contents:: Table of Contents + :depth: 2 + :local: + + +Overview +-------- + +The ``sphinx_module`` package provides two complementary Bazel rules for structuring and documenting software modules: + +1. **sphinx_module**: A generic documentation module rule that builds Sphinx-based HTML documentation from RST source files. Suitable for any type of documentation module. + +2. **score_component**: A specialized rule for Safety Elements out of Context (SEooC) that enforces documentation structure with standardized artifacts for assumptions of use, requirements, architecture, and safety analysis. + +Both rules support **cross-module dependencies** through the ``deps`` attribute, enabling automatic integration of external sphinx-needs references and HTML merging for comprehensive documentation sets. + +.. uml:: score_module_overview.puml + + +Rules and Macros +---------------- + +sphinx_module +~~~~~~~~~~~~ + +**File:** ``score_module.bzl`` + +**Purpose:** Generic rule for building Sphinx-based HTML documentation modules from RST source files with support for dependencies and cross-referencing. + +**Usage:** + +.. code-block:: python + + sphinx_module( + name = "my_documentation", + srcs = glob(["docs/**/*.rst"]), + index = "docs/index.rst", + deps = [ + "@score_process//:score_process_module", + "//other_module:documentation", + ], + sphinx = "//bazel/rules/score_module:score_build", + visibility = ["//visibility:public"] + ) + +**Parameters:** + +- ``name``: The name of the documentation module +- ``srcs``: List of RST source files for the documentation +- ``index``: Path to the main index.rst file +- ``deps``: Optional list of other ``sphinx_module`` or ``score_component`` targets that this module depends on. Dependencies are automatically integrated for cross-referencing via sphinx-needs and their HTML is merged into the output. +- ``sphinx``: Label to the Sphinx build binary (default: ``//bazel/rules/score_module:score_build``) +- ``config``: Optional custom conf.py file. If not provided, a default configuration is generated. +- ``visibility``: Bazel visibility specification + +**Generated Targets:** + +- ````: Main target producing the HTML documentation directory +- ``_needs``: Internal target generating the sphinx-needs JSON file for cross-referencing + +**Output:** + +- ``/html``: Directory containing the built HTML documentation with integrated dependencies +- ``/needs.json``: Sphinx-needs JSON file for external cross-references + +**Build Strategy** + +The ``sphinx_module`` rule implements a multi-phase build strategy to ensure proper dependency resolution and documentation integration: + +**Phase 1: Generate Needs JSON** + +First, the rule builds a ``needs.json`` file for the current module by running Sphinx in a preliminary pass. This JSON file contains all sphinx-needs definitions (requirements, architecture elements, test cases, etc.) from the module's documentation. The needs.json is generated using the ``score_needs`` internal rule. + +**Phase 2: Build Dependent Modules** + +Before building the main module's HTML, Bazel ensures all modules listed in the ``deps`` attribute are built first. This gives us: + +- The ``needs.json`` files from all dependencies for external cross-referencing +- The complete HTML documentation trees from all dependencies for merging + +This phase leverages Bazel's dependency graph to parallelize builds where possible. + +**Phase 3: Generate Main Module HTML** + +With all dependency needs.json files available, Sphinx builds the main module's HTML documentation. During this phase: + +- The ``needs_external_needs`` configuration is automatically populated with paths to all dependency needs.json files +- Sphinx resolves ``:need:`` references across module boundaries +- HTML pages are generated in a temporary ``_html`` directory + +**Phase 4: Merge HTML Documentation** + +Finally, the ``sphinx_html_merge`` tool combines the documentation: + +1. Copies the main module's HTML from ``_html/`` to the final ``html/`` output directory +2. For each dependency, copies its ``html/`` directory into the output as a subdirectory +3. Preserves the module hierarchy, enabling navigation between related documentation + +The result is a unified documentation tree where users can seamlessly navigate from the main module to any of its dependencies. + +**Build Artifacts** + +Each successful build produces: + +- ``/html/``: Complete merged HTML documentation +- ``/needs.json``: Sphinx-needs export for this module +- ``/_html/``: Intermediate HTML (before merging) + + +score_component +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**File:** ``score_module.bzl`` + +**Purpose:** Specialized macro for defining a Safety Element out of Context (SEooC) module documentation structure and automatic index generation. + +**Usage:** + +.. code-block:: python + + score_component( + name = "my_seooc", + assumptions_of_use = ["docs/assumptions_of_use.rst"], + component_requirements = ["docs/requirements.rst"], + architectural_design = ["docs/architecture.rst"], + dependability_analysis = ["docs/dependability_analysis.rst"], + deps = [ + "@score_platform//:score_platform_module", + "@score_process//:score_process_module", + ], + implementations = [":my_lib"], + tests = [":my_lib_test"], + visibility = ["//visibility:public"] + ) + +**Parameters:** + +- ``name``: The name of the safety element module +- ``assumptions_of_use``: List of labels to ``.rst`` or ``.md`` files containing Assumptions of Use documentation +- ``component_requirements``: List of labels to ``.rst`` or ``.md`` files containing component requirements specification +- ``architectural_design``: List of labels to ``.rst`` or ``.md`` files containing architectural design specification +- ``dependability_analysis``: List of labels to ``.rst`` or ``.md`` files containing safety analysis documentation (FMEA, DFA, etc.) +- ``deps``: Optional list of other ``sphinx_module`` or ``score_component`` targets that this SEooC depends on. Dependencies enable cross-referencing between modules and merge their HTML documentation into the final output. +- ``implementations``: List of labels to implementation targets (cc_library, cc_binary, etc.) that realize the component requirements +- ``tests``: List of labels to test targets (cc_test, py_test, etc.) that verify the implementation against requirements +- ``sphinx``: Label to the Sphinx build binary (default: ``//bazel/rules/score_module:score_build``) +- ``visibility``: Bazel visibility specification + +**Generated Targets:** + +- ``_seooc_index``: Internal target that generates index.rst and symlinks all artifact files +- ````: Main SEooC target (internally calls ``sphinx_module``) producing HTML documentation +- ``_needs``: Sphinx-needs JSON file for cross-referencing + +**Implementation Details:** + +The macro automatically: + +- Generates an index.rst file with a toctree referencing all provided artifacts +- Creates symlinks to artifact files (assumptions of use, requirements, architecture, safety analysis) for co-location with the generated index +- Delegates to ``sphinx_module`` for actual Sphinx build and HTML generation +- Integrates dependencies for cross-module referencing and HTML merging + +Dependency Management +--------------------- + +Both ``sphinx_module`` and ``score_component`` support cross-module dependencies through the ``deps`` attribute. This enables: + +**Cross-Referencing with Sphinx-Needs** + +Dependencies are automatically configured for sphinx-needs external references, allowing documents to reference requirements, architecture elements, and other needs across module boundaries using the ``:need:`` role. + +**HTML Documentation Merging** + +When building a module with dependencies, the HTML output from all dependent modules is merged into a unified documentation tree. For example: + +.. code-block:: text + + /html/ + ├── index.html # Main module documentation + ├── _static/ # Sphinx static assets + ├── dependency_module_1/ # Merged from first dependency + │ └── index.html + └── dependency_module_2/ # Merged from second dependency + └── index.html + +This allows seamless navigation between related documentation modules while maintaining independent build targets. + +**Example with Dependencies:** + +.. code-block:: python + + # Process module (external dependency) + # @score_process//:score_process_module + + # Platform module (external dependency) + # @score_platform//:score_platform_module + + # My component that depends on process and platform + score_component( + name = "my_component_seooc", + assumptions_of_use = ["docs/assumptions.rst"], + component_requirements = ["docs/requirements.rst"], + architectural_design = ["docs/architecture.rst"], + dependability_analysis = ["docs/safety.rst"], + deps = [ + "@score_process//:score_process_module", + "@score_platform//:score_platform_module", + ], + ) + +Documentation Structure +----------------------- + +**For score_component:** + +The macro automatically generates an index.rst and organizes files:: + + bazel-bin/_seooc_index/ + ├── index.rst # Generated toctree + ├── assumptions_of_use.rst # Symlinked artifact + ├── component_requirements.rst # Symlinked artifact + ├── architectural_design.rst # Symlinked artifact + └── dependability_analysis.rst # Symlinked artifact + +**For sphinx_module:** + +User provides the complete source structure:: + + docs/ + ├── index.rst # User-provided + ├── section1.rst + ├── section2.rst + └── subsection/ + └── details.rst + +**Output Structure:** + +Both rules produce a standardized output:: + + bazel-bin// + ├── html/ # Built HTML documentation + │ ├── index.html + │ ├── _static/ + │ ├── / # Merged dependency HTML + │ └── / # Merged dependency HTML + └── needs.json # Sphinx-needs export + +Integration with Sphinx +------------------------ + +The rules provide first-class Sphinx integration: + +**Sphinx-Needs Support** + +- Automatic configuration of external needs references from dependencies +- Export of needs.json for downstream consumers +- Cross-module traceability using ``:need:`` references + +**Sphinx Extensions** + +The default configuration includes common SCORE extensions: + +- sphinx-needs: Requirements management and traceability +- sphinx_design: Modern UI components +- myst_parser: Markdown support alongside RST + +**Custom Configuration** + +For ``sphinx_module``, provide a custom conf.py via the ``config`` attribute to override the default Sphinx configuration. + + +Usage Examples +-------------- + +Example 1: Generic Documentation Module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + load("//bazel/rules/score_module:score_module.bzl", "sphinx_module") + + sphinx_module( + name = "platform_docs", + srcs = glob(["docs/**/*.rst"]), + index = "docs/index.rst", + deps = [ + "@score_process//:score_process_module", + ], + ) + +Build and view: + +.. code-block:: bash + + bazel build //:platform_docs + # Output: bazel-bin/platform_docs/html/ + +Example 2: Safety Element out of Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + load("//bazel/rules/score_module:score_module.bzl", + "score_component") + + # Implementation + cc_library( + name = "kvs_lib", + srcs = ["kvs.cpp"], + hdrs = ["kvs.h"], + ) + + # Tests + cc_test( + name = "kvs_test", + srcs = ["kvs_test.cpp"], + deps = [":kvs_lib"], + ) + + # SEooC with dependencies + score_component( + name = "kvs_seooc", + assumptions_of_use = ["docs/assumptions.rst"], + component_requirements = ["docs/requirements.rst"], + architectural_design = ["docs/architecture.rst"], + dependability_analysis = ["docs/fmea.rst", "docs/dfa.rst"], + deps = [ + "@score_platform//:score_platform_module", + "@score_process//:score_process_module", + ], + implementations = [":kvs_lib"], + tests = [":kvs_test"], + ) + +Build and view: + +.. code-block:: bash + + bazel build //:kvs_seooc + # Output: bazel-bin/kvs_seooc/html/ + # Includes merged HTML from score_platform and score_process modules + +Design Rationale +---------------- + +These rules provide a structured approach to documentation by: + +1. **Two-Tier Architecture**: Generic ``sphinx_module`` for flexibility, specialized ``score_component`` for safety-critical work +2. **Dependency Management**: Automatic cross-referencing and HTML merging across modules +3. **Standardization**: SEooC enforces consistent structure for safety documentation +4. **Traceability**: Sphinx-needs integration enables bidirectional traceability +5. **Automation**: Index generation, symlinking, and configuration management are automatic +6. **Build System Integration**: Bazel ensures reproducible, cacheable documentation builds diff --git a/bazel/rules/score_module/docs/score_module_overview.puml b/bazel/rules/score_module/docs/score_module_overview.puml new file mode 100644 index 0000000..75aaa3a --- /dev/null +++ b/bazel/rules/score_module/docs/score_module_overview.puml @@ -0,0 +1,72 @@ +@startuml + +skinparam linetype ortho +skinparam artifact<> { + BackgroundColor LightGreen + BorderColor Green +} +skinparam component<> { + BackgroundColor LightBlue + BorderColor Blue +} + +component "sphinx_module" as SM <> + +component "bzlmod" as bzlmod <> +component "score_component" as score_component <> +component "score_unit" as score_unit <> + +' sphinx_module structure + +' module-specific artifacts +artifact "manuals" as score_manual <> +artifact "dependability_management" as dependability_management <> +artifact release_notes <> + + +' score_component-specific artifacts +artifact "Assumptions of Use" as assumptions_of_use <> +artifact "Component Requirements" as component_requirements <> +artifact "Architecture Design" as architectural_design <> +artifact "Dependability Analysis" as dependability_analysis <> +artifact "Checklists" as checklists <> +artifact "VerificationReport" as verification_report <> + +artifact "Detailed Design" as detailed_design <> + + +' Implementation artifacts +card "Implementation" as Impl <> +card "Test Suite" as Test <> +bzlmod *-- score_component +SM <|-up- score_component + +' Relationships for bzlmod +bzlmod *-- score_manual +bzlmod *-- dependability_management +bzlmod *-- release_notes +bzlmod *-- checklists +bzlmod *-- verification_report + + +' Relationships for score_component +score_component *-- assumptions_of_use +score_component *-- component_requirements +score_component *-- architectural_design +score_component *-- dependability_analysis +score_component *-- Impl +score_component *-- Test +score_component *-- score_unit +score_component *-- checklists +score_component *-- score_component +score_component --> verification_report : creates + + +score_unit *-- detailed_design + + + +' Cross-module dependencies +SM ..> SM : can depend on + +@enduml diff --git a/bazel/rules/score_module/private/BUILD b/bazel/rules/score_module/private/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/bazel/rules/score_module/private/score_component.bzl b/bazel/rules/score_module/private/score_component.bzl new file mode 100644 index 0000000..97d5261 --- /dev/null +++ b/bazel/rules/score_module/private/score_component.bzl @@ -0,0 +1,246 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Safety Element out of Context (SEooC) build rules for S-CORE projects. + +This module provides macros and rules for building SEooC documentation modules +following S-CORE process guidelines. A SEooC is a safety-related element developed +independently of a specific vehicle project. +""" + +load("//bazel/rules/score_module/private:sphinx_module.bzl", "sphinx_module") + +# ============================================================================ +# Private Rule Implementation +# ============================================================================ + +def _software_component_index_impl(ctx): + """Generate index.rst file with references to all SEooC artifacts. + + This rule creates a Sphinx index.rst file that includes references to all + the SEooC documentation artifacts (assumptions of use, requirements, design, + and safety analysis). + + Args: + ctx: Rule context + + Returns: + DefaultInfo provider with generated index.rst file + """ + + # Declare output index.rst file + index_rst = ctx.actions.declare_file(ctx.label.name + "/index.rst") + + # Collect all artifact files and create symlinks + output_files = [index_rst] + artifacts_by_type = { + "assumptions_of_use": [], + "component_requirements": [], + "architectural_design": [], + "dependability_analysis": [], + "checklists": [], + } + + # Process each artifact type + for artifact_name in artifacts_by_type: + attr_list = getattr(ctx.attr, artifact_name) + if attr_list: + # For label_list attributes, iterate over each label + for label in attr_list: + files = label.files.to_list() + for artifact_file in files: + # Check that artifact is not named index.rst + if artifact_file.basename == "index.rst": + fail("Error in {}: Artifact file '{}' in '{}' cannot be named 'index.rst' as this file is generated by the SEooC rule and would be overwritten.".format( + ctx.label, + artifact_file.path, + artifact_name, + )) + + # Create symlink in same directory as index + output_file = ctx.actions.declare_file( + ctx.label.name + "/" + artifact_file.basename, + ) + output_files.append(output_file) + + # Symlink instead of copying for better performance + ctx.actions.symlink( + output = output_file, + target_file = artifact_file, + ) + + # Add reference to index (without file extension) + doc_ref = artifact_file.basename.replace(".rst", "").replace(".md", "") + artifacts_by_type[artifact_name].append(doc_ref) + + # Substitute template variables (template handles indentation) + title = ctx.attr.module_name + underline = "=" * len(title) + + ctx.actions.expand_template( + template = ctx.file.template, + output = index_rst, + substitutions = { + "{title}": title, + "{underline}": underline, + "{description}": ctx.attr.description, + "{assumptions_of_use}": "\n ".join(artifacts_by_type["assumptions_of_use"]), + "{component_requirements}": "\n ".join(artifacts_by_type["component_requirements"]), + "{architectural_design}": "\n ".join(artifacts_by_type["architectural_design"]), + "{dependability_analysis}": "\n ".join(artifacts_by_type["dependability_analysis"]), + "{checklists}": "\n ".join(artifacts_by_type["checklists"]), + }, + ) + + return [ + DefaultInfo(files = depset(output_files)), + ] + +# ============================================================================ +# Private Rule Definition +# ============================================================================ + +_software_component_index = rule( + implementation = _software_component_index_impl, + doc = "Generates index.rst file with references to SEooC artifacts", + attrs = { + "module_name": attr.string( + mandatory = True, + doc = "Name of the SEooC module (used as document title)", + ), + "description": attr.string( + mandatory = True, + doc = "Description of the SEooC component that appears at the beginning of the documentation. Supports RST formatting.", + ), + "template": attr.label( + allow_single_file = [".rst"], + mandatory = True, + doc = "Template file for generating index.rst", + ), + "assumptions_of_use": attr.label_list( + allow_files = [".rst", ".md"], + mandatory = True, + doc = "Assumptions of Use document as defined in the S-CORE process", + ), + "component_requirements": attr.label_list( + allow_files = [".rst", ".md"], + mandatory = True, + doc = "Component requirements specification as defined in the S-CORE process", + ), + "architectural_design": attr.label_list( + allow_files = [".rst", ".md"], + mandatory = True, + doc = "Architectural design specification as defined in the S-CORE process", + ), + "dependability_analysis": attr.label_list( + allow_files = [".rst", ".md"], + mandatory = True, + doc = "Safety analysis documentation as defined in the S-CORE process", + ), + "checklists": attr.label_list( + allow_files = [".rst", ".md"], + mandatory = True, + doc = "Safety analysis documentation as defined in the S-CORE process", + ), + }, +) + +# ============================================================================ +# Public Macro +# ============================================================================ + +def score_component( + name, + assumptions_of_use, + component_requirements, + architectural_design, + dependability_analysis, + description, + checklists = [], + implementations = [], + tests = [], + deps = [], + sphinx = "//bazel/rules/score_module:score_build", + visibility = None): + """Define a Safety Element out of Context (SEooC) following S-CORE process guidelines. + + This macro creates a complete SEooC module with integrated documentation + generation. It generates an index.rst file referencing all SEooC artifacts + and builds HTML documentation using the sphinx_module infrastructure. + + A SEooC is a safety-related architectural element (e.g., a software component) + that is developed independently of a specific vehicle project and can be + integrated into different vehicle platforms. + + Args: + name: The name of the safety element module. Used as the base name for + all generated targets. + assumptions_of_use: Label to a .rst or .md file containing the Assumptions + of Use, which define the safety-relevant operating conditions and + constraints for the SEooC as defined in the S-CORE process. + component_requirements: Label to a .rst or .md file containing the + component requirements specification, defining functional and safety + requirements as defined in the S-CORE process. + architectural_design: Label to a .rst or .md file containing the + architectural design specification, describing the software + architecture and design decisions as defined in the S-CORE process. + dependability_analysis: Label to a .rst or .md file containing the safety + analysis, including FMEA, FMEDA, FTA, or other safety analysis + results as defined in the S-CORE process. + description: String containing a high-level description of the SEooC + component. This text appears at the beginning of the generated documentation, + providing context about what the component does and its purpose. + Supports RST formatting. + implementations: Optional list of labels to Bazel targets representing + the actual software implementation (cc_library, cc_binary, etc.) + that realizes the component requirements. This is the source code + that implements the safety functions as defined in the S-CORE process. + tests: Optional list of labels to Bazel test targets (cc_test, py_test, etc.) + that verify the implementation against requirements. Includes unit + tests and integration tests as defined in the S-CORE process. + deps: Optional list of other sphinx_module or SEooC targets this module + depends on. Cross-references will work automatically. + sphinx: Label to sphinx build binary. Default: //bazel/rules/score_module:score_build + visibility: Bazel visibility specification for the generated SEooC targets. + + Generated Targets: + _seooc_index: Internal rule that generates index.rst and copies artifacts + : Main SEooC target (sphinx_module) with HTML documentation + _needs: Internal target for sphinx-needs JSON generation + """ + + # Step 1: Generate index.rst and collect all artifacts + _software_component_index( + name = name + "_seooc_index", + module_name = name, + description = description, + template = "//bazel/rules/score_module:templates/seooc_index.template.rst", + assumptions_of_use = assumptions_of_use, + component_requirements = component_requirements, + architectural_design = architectural_design, + dependability_analysis = dependability_analysis, + checklists = checklists, + visibility = ["//visibility:private"], + ) + + # Step 2: Create sphinx_module using generated index and artifacts + # The index file is part of the _seooc_index target outputs + sphinx_module( + name = name, + srcs = [":" + name + "_seooc_index"], + index = ":" + name + "_seooc_index", # Label to the target, not a path + deps = deps, + sphinx = sphinx, + visibility = visibility, + ) diff --git a/bazel/rules/score_module/private/sphinx_module.bzl b/bazel/rules/score_module/private/sphinx_module.bzl new file mode 100644 index 0000000..f14f3f8 --- /dev/null +++ b/bazel/rules/score_module/private/sphinx_module.bzl @@ -0,0 +1,299 @@ +# ====================================================================================== +# Providers +# ====================================================================================== + +ScoreModuleInfo = provider( + doc = "Provider for Sphinx HTML module documentation", + fields = { + "html_dir": "Directory containing HTML files", + }, +) + +ScoreNeedsInfo = provider( + doc = "Provider for sphinx-needs info", + fields = { + "needs_json_file": "Direct needs.json file for this module", + "needs_json_files": "Depset of needs.json files including transitive dependencies", + }, +) + +# ====================================================================================== +# Helpers +# ====================================================================================== +def _create_config_py(ctx): + """Get or generate the conf.py configuration file. + + Args: + ctx: Rule context + """ + if ctx.attr.config: + config_file = ctx.attr.config.files.to_list()[0] + else: + config_file = ctx.actions.declare_file(ctx.label.name + "/conf.py") + template = ctx.file._config_template + + # Read template and substitute PROJECT_NAME + ctx.actions.expand_template( + template = template, + output = config_file, + substitutions = { + "{PROJECT_NAME}": ctx.label.name.replace("_", " ").title(), + }, + ) + return config_file + +# ====================================================================================== +# Common attributes for Sphinx rules +# ====================================================================================== +sphinx_rule_attrs = { + "srcs": attr.label_list( + allow_files = True, + doc = "List of source files for the Sphinx documentation.", + ), + "sphinx": attr.label( + doc = "The Sphinx build binary to use.", + mandatory = True, + executable = True, + cfg = "exec", + ), + "config": attr.label( + allow_files = [".py"], + doc = "Configuration file (conf.py) for the Sphinx documentation. If not provided, a default config will be generated.", + mandatory = False, + ), + "index": attr.label( + allow_files = [".rst"], + doc = "Index file (index.rst) for the Sphinx documentation.", + mandatory = True, + ), + "deps": attr.label_list( + doc = "List of other sphinx_module targets this module depends on for intersphinx.", + ), + "_config_template": attr.label( + default = Label("//bazel/rules/score_module:templates/conf.template.py"), + allow_single_file = True, + doc = "Template for generating default conf.py", + ), + "_html_merge_tool": attr.label( + default = Label("//bazel/rules/score_module:sphinx_html_merge"), + executable = True, + cfg = "exec", + doc = "Tool for merging HTML directories", + ), +} + +# ====================================================================================== +# Rule implementations +# ====================================================================================== +def _score_needs_impl(ctx): + output_path = ctx.label.name.replace("_needs", "") + "/needs.json" + needs_output = ctx.actions.declare_file(output_path) + + # Get config file (generate or use provided) + config_file = _create_config_py(ctx) + + # Phase 1: Build needs.json (without external needs) + needs_inputs = ctx.files.srcs + [config_file] + + if ctx.attr.config: + needs_inputs = needs_inputs + ctx.files.config + + needs_args = [ + "--index_file", + ctx.attr.index.files.to_list()[0].path, + "--output_dir", + needs_output.dirname, + "--config", + config_file.path, + "--builder", + "needs", + ] + + ctx.actions.run( + inputs = needs_inputs, + outputs = [needs_output], + arguments = needs_args, + progress_message = "Generating needs.json for: %s" % ctx.label.name, + executable = ctx.executable.sphinx, + ) + + transitive_needs = [dep[ScoreNeedsInfo].needs_json_files for dep in ctx.attr.deps if ScoreNeedsInfo in dep] + needs_json_files = depset([needs_output], transitive = transitive_needs) + + return [ + DefaultInfo( + files = needs_json_files, + ), + ScoreNeedsInfo( + needs_json_file = needs_output, # Direct file only + needs_json_files = needs_json_files, # Transitive depset + ), + ] + +def _score_html_impl(ctx): + """Implementation for building a Sphinx module with two-phase build. + + Phase 1: Generate needs.json for this module and collect from all deps + Phase 2: Generate HTML with external needs and merge all dependency HTML + """ + + # Collect all transitive dependencies with deduplication + modules = [] + + needs_external_needs = {} + for dep in ctx.attr.needs: + if ScoreNeedsInfo in dep: + dep_name = dep.label.name.replace("_needs", "") + needs_external_needs[dep.label.name] = { + "base_url": dep_name, # Relative path to the subdirectory where dep HTML is copied + "json_path": dep[ScoreNeedsInfo].needs_json_file.path, # Use direct file + "id_prefix": "", + "css_class": "", + } + + for dep in ctx.attr.deps: + if ScoreModuleInfo in dep: + modules.extend([dep[ScoreModuleInfo].html_dir]) + + needs_external_needs_json = ctx.actions.declare_file(ctx.label.name + "/needs_external_needs.json") + + ctx.actions.write( + output = needs_external_needs_json, + content = json.encode_indent(needs_external_needs, indent = " "), + ) + + # Read template and substitute PROJECT_NAME + config_file = ctx.actions.declare_file(ctx.label.name + "/conf.py") + template = ctx.file._config_template + + ctx.actions.expand_template( + template = template, + output = config_file, + substitutions = { + "{PROJECT_NAME}": ctx.label.name.replace("_", " ").title(), + }, + ) + + # Build HTML with external needs + html_inputs = ctx.files.srcs + ctx.files.needs + [config_file, needs_external_needs_json] + sphinx_html_output = ctx.actions.declare_directory(ctx.label.name + "/_html") + html_args = [ + "--index_file", + ctx.attr.index.files.to_list()[0].path, + "--output_dir", + sphinx_html_output.path, + "--config", + config_file.path, + "--builder", + "html", + ] + + ctx.actions.run( + inputs = html_inputs, + outputs = [sphinx_html_output], + arguments = html_args, + progress_message = "Building HTML with external needs: %s" % ctx.label.name, + executable = ctx.executable.sphinx, + ) + + # Create final HTML output directory with dependencies using Python merge script + html_output = ctx.actions.declare_directory(ctx.label.name + "/html") + + # Build arguments for the merge script + merge_args = [ + "--output", + html_output.path, + "--main", + sphinx_html_output.path, + ] + + merge_inputs = [sphinx_html_output] + + # Add each dependency + for dep in ctx.attr.deps: + if ScoreModuleInfo in dep: + dep_html_dir = dep[ScoreModuleInfo].html_dir + dep_name = dep.label.name + merge_inputs.append(dep_html_dir) + merge_args.extend(["--dep", dep_name + ":" + dep_html_dir.path]) + + # Merging html files + ctx.actions.run( + inputs = merge_inputs, + outputs = [html_output], + arguments = merge_args, + progress_message = "Merging HTML with dependencies for %s" % ctx.label.name, + executable = ctx.executable._html_merge_tool, + ) + + return [ + DefaultInfo(files = depset(ctx.files.needs + [html_output])), + ScoreModuleInfo( + html_dir = html_output, + ), + ] + +# ====================================================================================== +# Rule definitions +# ====================================================================================== + +_score_needs = rule( + implementation = _score_needs_impl, + attrs = sphinx_rule_attrs, +) + +_score_html = rule( + implementation = _score_html_impl, + attrs = dict(sphinx_rule_attrs, needs = attr.label_list( + allow_files = True, + doc = "Submodule symbols.needs targets for this module.", + )), +) + +# ====================================================================================== +# Rule wrappers +# ====================================================================================== + +def sphinx_module( + name, + srcs, + index, + config = None, + deps = [], + sphinx = "@//bazel/rules/score_module:score_build", + visibility = ["//visibility:public"]): + """Build a Sphinx module with transitive HTML dependencies. + + This rule builds documentation modules into complete HTML sites with + transitive dependency collection. All dependencies are automatically + included in a modules/ subdirectory for intersphinx cross-referencing. + + Args: + name: Name of the target + srcs: List of source files (.rst, .md) with index file first + index: Label to index.rst file + config: Label to conf.py configuration file (optional, will be auto-generated if not provided) + deps: List of other sphinx_module targets this module depends on + sphinx: Label to sphinx build binary (default: :sphinx_build) + visibility: Bazel visibility + """ + _score_needs( + name = name + "_needs", + srcs = srcs, + config = config, + index = index, + deps = [d + "_needs" for d in deps], + sphinx = sphinx, + visibility = visibility, + ) + + _score_html( + name = name, + srcs = srcs, + config = config, + index = index, + deps = deps, + needs = [d + "_needs" for d in deps], + sphinx = sphinx, + visibility = visibility, + ) diff --git a/bazel/rules/score_module/score_module.bzl b/bazel/rules/score_module/score_module.bzl new file mode 100644 index 0000000..a949d48 --- /dev/null +++ b/bazel/rules/score_module/score_module.bzl @@ -0,0 +1,13 @@ +load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_docs") +load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") +load( + "//bazel/rules/score_module/private:score_component.bzl", + _score_component = "score_component", +) +load( + "//bazel/rules/score_module/private:sphinx_module.bzl", + _sphinx_module = "sphinx_module", +) + +sphinx_module = _sphinx_module +score_component = _score_component diff --git a/bazel/rules/score_module/src/sphinx_html_merge.py b/bazel/rules/score_module/src/sphinx_html_merge.py new file mode 100644 index 0000000..60dfaa4 --- /dev/null +++ b/bazel/rules/score_module/src/sphinx_html_merge.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Merge multiple Sphinx HTML output directories. + +This script merges Sphinx HTML documentation from multiple modules into a single +output directory. It copies the main module's HTML as-is, and then copies each +dependency module's HTML into a subdirectory, excluding nested module directories +to avoid duplication. + +Usage: + sphinx_html_merge.py --output OUTPUT_DIR --main MAIN_HTML_DIR [--dep NAME:PATH ...] +""" + +import argparse +import os +import re +import shutil +import sys +from pathlib import Path + + +# Standard Sphinx directories that should be copied +# Note: _static and _sphinx_design_static are excluded for dependencies to avoid duplication +SPHINX_DIRS = {"_sources", ".doctrees"} + + +def copy_html_files(src_dir, dst_dir, exclude_module_dirs=None, sibling_modules=None): + """Copy HTML and related files from src to dst, with optional link fixing. + + Args: + src_dir: Source HTML directory + dst_dir: Destination directory + exclude_module_dirs: Set of module directory names to skip (to avoid copying nested modules). + If None, copy everything. + sibling_modules: Set of sibling module names for fixing links in HTML files. + If None, no link fixing is performed. + """ + src_path = Path(src_dir) + dst_path = Path(dst_dir) + + if not src_path.exists(): + print(f"Warning: Source directory does not exist: {src_dir}", file=sys.stderr) + return + + dst_path.mkdir(parents=True, exist_ok=True) + + if exclude_module_dirs is None: + exclude_module_dirs = set() + + # Prepare regex patterns for link fixing if needed + module_pattern = None + static_pattern = None + if sibling_modules: + module_pattern = re.compile( + r'((?:href|src)=")(' + + "|".join(re.escape(mod) for mod in sibling_modules) + + r")/", + re.IGNORECASE, + ) + static_pattern = re.compile( + r'((?:href|src)=")(\.\./)*(_static|_sphinx_design_static)/', re.IGNORECASE + ) + + def process_file(src_file, dst_file, relative_path): + """Read, optionally modify, and write a file.""" + if src_file.suffix == ".html" and sibling_modules: + # Read, modify, and write HTML files + try: + content = src_file.read_text(encoding="utf-8") + + # Replace module_name/ with ../module_name/ + modified_content = module_pattern.sub(r"\1../\2/", content) + + # Calculate depth for static file references + depth = len(relative_path.parents) - 1 + parent_prefix = "../" * (depth + 1) + + def replace_static(match): + return f"{match.group(1)}{parent_prefix}{match.group(3)}/" + + modified_content = static_pattern.sub(replace_static, modified_content) + + # Write modified content + dst_file.parent.mkdir(parents=True, exist_ok=True) + dst_file.write_text(modified_content, encoding="utf-8") + except Exception as e: + print(f"Warning: Failed to process {src_file}: {e}", file=sys.stderr) + # Fallback to regular copy on error + shutil.copy2(src_file, dst_file) + else: + # Regular copy for non-HTML files + dst_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_file, dst_file) + + def copy_tree(src, dst, rel_path): + """Recursively copy directory tree with processing.""" + for item in src.iterdir(): + rel_item = rel_path / item.name + dst_item = dst / item.name + + if item.is_file(): + process_file(item, dst_item, rel_item) + elif item.is_dir(): + # Skip excluded directories + if item.name in exclude_module_dirs: + continue + # Skip static dirs from dependencies + if ( + item.name in ("_static", "_sphinx_design_static") + and exclude_module_dirs + ): + continue + + dst_item.mkdir(parents=True, exist_ok=True) + copy_tree(item, dst_item, rel_item) + + # Start copying from root + copy_tree(src_path, dst_path, Path(".")) + + +def merge_html_dirs(output_dir, main_html_dir, dependencies): + """Merge HTML directories. + + Args: + output_dir: Target output directory + main_html_dir: Main module's HTML directory to copy as-is + dependencies: List of (name, path) tuples for dependency modules + """ + output_path = Path(output_dir) + + # First, copy the main HTML directory + print(f"Copying main HTML from {main_html_dir} to {output_dir}") + copy_html_files(main_html_dir, output_dir) + + # Collect all dependency names for link fixing and exclusion + dep_names = [name for name, _ in dependencies] + + # Then copy each dependency into a subdirectory with link fixing + for dep_name, dep_html_dir in dependencies: + dep_output = output_path / dep_name + print(f"Copying dependency {dep_name} from {dep_html_dir} to {dep_output}") + # Exclude other module directories to avoid nested modules + # Remove current module from the list to get actual siblings to exclude + sibling_modules = set(n for n in dep_names if n != dep_name) + copy_html_files( + dep_html_dir, + dep_output, + exclude_module_dirs=sibling_modules, + sibling_modules=sibling_modules, + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Merge Sphinx HTML documentation directories" + ) + parser.add_argument( + "--output", required=True, help="Output directory for merged HTML" + ) + parser.add_argument("--main", required=True, help="Main HTML directory to copy") + parser.add_argument( + "--dep", + action="append", + default=[], + metavar="NAME:PATH", + help="Dependency HTML directory in format NAME:PATH", + ) + + args = parser.parse_args() + + # Parse dependencies + dependencies = [] + for dep_spec in args.dep: + if ":" not in dep_spec: + print( + f"Error: Invalid dependency format '{dep_spec}', expected NAME:PATH", + file=sys.stderr, + ) + return 1 + + name, path = dep_spec.split(":", 1) + dependencies.append((name, path)) + + # Merge the HTML directories + merge_html_dirs(args.output, args.main, dependencies) + + print(f"Successfully merged HTML into {args.output}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bazel/rules/score_module/src/sphinx_wrapper.py b/bazel/rules/score_module/src/sphinx_wrapper.py new file mode 100644 index 0000000..d00e0b2 --- /dev/null +++ b/bazel/rules/score_module/src/sphinx_wrapper.py @@ -0,0 +1,231 @@ +""" +Wrapper script for running Sphinx builds in Bazel environments. + +This script provides a command-line interface to Sphinx documentation builds, +handling argument parsing, environment configuration, and build execution. +It's designed to be used as part of Bazel build rules for Score modules. +""" + +import argparse +import logging +import os +import sys +import time +from pathlib import Path +from typing import List, Optional + +from sphinx.cmd.build import main as sphinx_main + +# Constants +DEFAULT_PORT = 8000 +DEFAULT_GITHUB_VERSION = "main" +DEFAULT_SOURCE_DIR = "." + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(levelname)s: %(message)s", +) +logger = logging.getLogger(__name__) + + +def get_env(name: str, required: bool = True) -> Optional[str]: + """ + Get an environment variable value. + + Args: + name: The name of the environment variable + required: Whether the variable is required (raises error if not set) + + Returns: + The value of the environment variable, or None if not required and not set + + Raises: + ValueError: If the variable is required but not set + """ + val = os.environ.get(name) + logger.debug(f"Environment variable {name} = {val}") + if val is None and required: + raise ValueError(f"Required environment variable {name} is not set") + return val + + +def validate_arguments(args: argparse.Namespace) -> None: + """ + Validate required command-line arguments. + + Args: + args: Parsed command-line arguments + + Raises: + ValueError: If required arguments are missing or invalid + """ + if not args.index_file: + raise ValueError("--index_file is required") + if not args.output_dir: + raise ValueError("--output_dir is required") + if not args.builder: + raise ValueError("--builder is required") + + # Validate that index file exists if it's a real path + index_path = Path(args.index_file) + if not index_path.exists(): + raise ValueError(f"Index file does not exist: {args.index_file}") + + +def build_sphinx_arguments(args: argparse.Namespace) -> List[str]: + """ + Build the argument list for Sphinx. + + Args: + args: Parsed command-line arguments + + Returns: + List of arguments to pass to Sphinx + """ + source_dir = ( + str(Path(args.index_file).parent) if args.index_file else DEFAULT_SOURCE_DIR + ) + config_dir = str(Path(args.config).parent) if args.config else source_dir + + base_arguments = [ + source_dir, # source dir + args.output_dir, # output dir + "-c", + config_dir, # config directory + # "-W", # treat warning as errors - disabled for modular builds + "--keep-going", # do not abort after one error + "-T", # show details in case of errors in extensions + "--jobs", + "auto", + ] + + # Configure sphinx build with GitHub user and repo from CLI + if args.github_user and args.github_repo: + base_arguments.extend( + [ + f"-A=github_user={args.github_user}", + f"-A=github_repo={args.github_repo}", + f"-A=github_version={DEFAULT_GITHUB_VERSION}", + ] + ) + + # Add doc_path if SOURCE_DIRECTORY environment variable is set + source_directory = get_env("SOURCE_DIRECTORY", required=False) + if source_directory: + base_arguments.append(f"-A=doc_path='{source_directory}'") + + base_arguments.extend(["-b", args.builder]) + + return base_arguments + + +def run_sphinx_build(sphinx_args: List[str], builder: str) -> int: + """ + Execute the Sphinx build and measure duration. + + Args: + sphinx_args: Arguments to pass to Sphinx + builder: The builder type (for logging purposes) + + Returns: + The exit code from Sphinx build + """ + logger.info(f"Starting Sphinx build with builder: {builder}") + logger.debug(f"Sphinx arguments: {sphinx_args}") + + start_time = time.perf_counter() + + try: + exit_code = sphinx_main(sphinx_args) + except Exception as e: + logger.error(f"Sphinx build failed with exception: {e}") + return 1 + + end_time = time.perf_counter() + duration = end_time - start_time + + if exit_code == 0: + logger.info(f"docs ({builder}) finished successfully in {duration:.1f} seconds") + else: + logger.error( + f"docs ({builder}) failed with exit code {exit_code} after {duration:.1f} seconds" + ) + + return exit_code + + +def parse_arguments() -> argparse.Namespace: + """ + Parse command-line arguments. + + Returns: + Parsed command-line arguments + """ + parser = argparse.ArgumentParser( + description="Wrapper for Sphinx documentation builds in Bazel environments" + ) + + # Required arguments + parser.add_argument( + "--index_file", + required=True, + help="Path to the index file (e.g., index.rst)", + ) + parser.add_argument( + "--output_dir", + required=True, + help="Build output directory", + ) + parser.add_argument( + "--builder", + required=True, + help="Sphinx builder to use (e.g., html, needs, json)", + ) + + # Optional arguments + parser.add_argument( + "--config", + help="Path to config file (conf.py)", + ) + parser.add_argument( + "--github_user", + help="GitHub username to embed in the Sphinx build", + ) + parser.add_argument( + "--github_repo", + help="GitHub repository to embed in the Sphinx build", + ) + parser.add_argument( + "--port", + type=int, + default=DEFAULT_PORT, + help=f"Port to use for live preview (default: {DEFAULT_PORT}). Use 0 for auto-detection.", + ) + + return parser.parse_args() + + +def main() -> int: + """ + Main entry point for the Sphinx wrapper script. + + Returns: + Exit code (0 for success, non-zero for failure) + """ + try: + args = parse_arguments() + validate_arguments(args) + sphinx_args = build_sphinx_arguments(args) + exit_code = run_sphinx_build(sphinx_args, args.builder) + return exit_code + except ValueError as e: + logger.error(f"Validation error: {e}") + return 1 + except Exception as e: + logger.error(f"Unexpected error: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bazel/rules/score_module/templates/conf.template.py b/bazel/rules/score_module/templates/conf.template.py new file mode 100644 index 0000000..6f0c188 --- /dev/null +++ b/bazel/rules/score_module/templates/conf.template.py @@ -0,0 +1,192 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Generic Sphinx configuration template for SCORE modules. + +This file is auto-generated from a template and should not be edited directly. +Template variables like {PROJECT_NAME} are replaced during Bazel build. +""" + +import json +import os +from pathlib import Path +from typing import Any, Dict, List + +# Project configuration - {PROJECT_NAME} will be replaced by the module name during build +project = "{PROJECT_NAME}" +author = "S-CORE" +version = "1.0" +release = "1.0.0" +project_url = ( + "https://github.com/eclipse-score" # Required by score_metamodel extension +) + +# Sphinx extensions - comprehensive list for SCORE modules +extensions = [ + "sphinx_needs", + "sphinx_design", + "myst_parser", + "sphinxcontrib.plantuml", + "score_plantuml", + "score_metamodel", + "score_draw_uml_funcs", + "score_source_code_linker", + "score_layout", +] + +# MyST parser extensions +myst_enable_extensions = ["colon_fence"] + +# Exclude patterns for Bazel builds +exclude_patterns = [ + "bazel-*", + ".venv*", +] + +# Enable markdown rendering +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +# Enable numref for cross-references +numfig = True + +# HTML theme +# html_theme = "pydata_sphinx_theme" + + +# Configuration constants +NEEDS_EXTERNAL_FILE = "needs_external_needs.json" +BAZEL_OUT_DIR = "bazel-out" + + +def find_workspace_root() -> Path: + """ + Find the Bazel workspace root by looking for the bazel-out directory. + + Returns: + Path to the workspace root directory + """ + current = Path.cwd() + + # Traverse up the directory tree looking for bazel-out + while current != current.parent: + if (current / BAZEL_OUT_DIR).exists(): + return current + current = current.parent + + # If we reach the root without finding it, return current directory + return Path.cwd() + + +def load_external_needs() -> List[Dict[str, Any]]: + """ + Load external needs configuration from JSON file. + + This function reads the needs_external_needs.json file if it exists and + resolves relative paths to absolute paths based on the workspace root. + + Returns: + List of external needs configurations with resolved paths + """ + needs_file = Path(NEEDS_EXTERNAL_FILE) + + if not needs_file.exists(): + print(f"INFO: {NEEDS_EXTERNAL_FILE} not found - no external dependencies") + return [] + + print(f"INFO: Loading external needs from {NEEDS_EXTERNAL_FILE}") + + try: + with needs_file.open("r", encoding="utf-8") as file: + needs_dict = json.load(file) + except json.JSONDecodeError as e: + print(f"ERROR: Failed to parse {NEEDS_EXTERNAL_FILE}: {e}") + return [] + except Exception as e: + print(f"ERROR: Failed to read {NEEDS_EXTERNAL_FILE}: {e}") + return [] + + workspace_root = find_workspace_root() + print(f"INFO: Workspace root: {workspace_root}") + + external_needs = [] + for key, config in needs_dict.items(): + if "json_path" not in config: + print( + f"WARNING: External needs config for '{key}' missing 'json_path', skipping" + ) + continue + + # Resolve relative path to absolute path + # Bazel provides relative paths like: bazel-out/k8-fastbuild/bin/.../needs.json + # We need absolute paths: .../execroot/_main/bazel-out/... + json_path = workspace_root / config["json_path"] + config["json_path"] = str(json_path) + + print(f"INFO: Added external needs config for '{key}':") + print(f" json_path: {config['json_path']}") + print(f" id_prefix: {config.get('id_prefix', 'none')}") + print(f" version: {config.get('version', 'none')}") + + external_needs.append(config) + + return external_needs + + +def verify_config(app: Any, config: Any) -> None: + """ + Verify that configuration was properly loaded. + + This is called during Sphinx's config-inited event to ensure + external needs configuration is correctly set up. + + Args: + app: Sphinx application object + config: Sphinx configuration object + """ + print("=" * 80) + print("INFO: Verifying Sphinx configuration") + print(f" Project: {config.project}") + print(f" External needs count: {len(config.needs_external_needs)}") + print("=" * 80) + + +def setup(app: Any) -> Dict[str, Any]: + """ + Sphinx setup hook to register event listeners. + + Args: + app: Sphinx application object + + Returns: + Extension metadata dictionary + """ + app.connect("config-inited", verify_config) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } + + +# Initialize external needs configuration +print("=" * 80) +print(f"INFO: Sphinx configuration loaded for project: {project}") +print(f"INFO: Current working directory: {Path.cwd()}") + +# Load external needs configuration +needs_external_needs = load_external_needs() diff --git a/bazel/rules/score_module/templates/seooc_index.template.rst b/bazel/rules/score_module/templates/seooc_index.template.rst new file mode 100644 index 0000000..e4ae5c3 --- /dev/null +++ b/bazel/rules/score_module/templates/seooc_index.template.rst @@ -0,0 +1,26 @@ +.. ******************************************************************************* +.. 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 +.. ******************************************************************************* + +{title} +{underline} + +{description} + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + {architectural_design} + {component_requirements} + {assumptions_of_use} + {dependability_analysis} diff --git a/bazel/rules/score_module/test/BUILD b/bazel/rules/score_module/test/BUILD new file mode 100644 index 0000000..b155955 --- /dev/null +++ b/bazel/rules/score_module/test/BUILD @@ -0,0 +1,145 @@ +# ******************************************************************************* +# 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("//bazel/rules/score_module:score_module.bzl", "score_component", "sphinx_module") +load( + ":html_generation_test.bzl", + "html_merging_test", + "module_dependencies_test", + "needs_transitive_test", + "sphinx_module_test_suite", +) +load( + ":seooc_test.bzl", + "seooc_artifacts_copied_test", + "seooc_index_generation_test", + "seooc_needs_provider_test", + "seooc_sphinx_module_generated_test", +) + +package(default_visibility = ["//visibility:public"]) + +# ============================================================================ +# Test Fixtures - Module Definitions +# ============================================================================ + +# Test 1: Multi-Module Aggregation +# Dependency graph: module_a_lib -> module_b_lib -> module_c_lib +# module_a_lib -> module_c_lib (also direct) +sphinx_module( + name = "module_c_lib", + srcs = glob(["fixtures/module_c/*.rst"]), + index = "fixtures/module_c/index.rst", + sphinx = "//bazel/rules/score_module:score_build", +) + +sphinx_module( + name = "module_b_lib", + srcs = glob(["fixtures/module_b/*.rst"]), + index = "fixtures/module_b/index.rst", + sphinx = "//bazel/rules/score_module:score_build", + deps = [":module_c_lib"], +) + +sphinx_module( + name = "module_a_lib", + srcs = glob(["fixtures/module_a/*.rst"]), + index = "fixtures/module_a/index.rst", + sphinx = "//bazel/rules/score_module:score_build", + deps = [ + ":module_b_lib", + ":module_c_lib", + ], +) + +# Test 2: SEooC (Safety Element out of Context) Module +# Tests the score_component macro with S-CORE process artifacts +score_component( + name = "seooc_test_lib", + architectural_design = ["fixtures/seooc_test/architectural_design.rst"], + assumptions_of_use = ["fixtures/seooc_test/assumptions_of_use.rst"], + component_requirements = ["fixtures/seooc_test/component_requirements.rst"], + dependability_analysis = ["fixtures/seooc_test/dependability_analysis.rst"], + description = "Test SEooC module demonstrating S-CORE process compliance structure.", + deps = [ + ":module_c_lib", + ], +) + +# ============================================================================ +# Test Instantiations - HTML Generation Tests +# ============================================================================ + +# Needs Generation Tests +needs_transitive_test( + name = "needs_transitive_test", + target_under_test = ":module_b_lib_needs", +) + +# Dependency Tests +module_dependencies_test( + name = "module_dependencies_test", + target_under_test = ":module_a_lib", +) + +html_merging_test( + name = "html_merging_test", + target_under_test = ":module_a_lib", +) + +# ============================================================================ +# SEooC-Specific Tests +# ============================================================================ + +# Test that all artifacts are copied +seooc_artifacts_copied_test( + name = "seooc_tests_artifacts_copied", + target_under_test = ":seooc_test_lib_seooc_index", +) + +# Test that sphinx_module is generated with correct providers +seooc_sphinx_module_generated_test( + name = "seooc_tests_sphinx_module_generated", + target_under_test = ":seooc_test_lib", +) + +# Test that needs provider exists for cross-referencing +seooc_needs_provider_test( + name = "seooc_tests_needs_provider", + target_under_test = ":seooc_test_lib_needs", +) + +# ============================================================================ +# Test Suites +# ============================================================================ + +# Main test suite combining all sphinx_module tests +sphinx_module_test_suite(name = "sphinx_module_tests") + +# SEooC-focused test suite +test_suite( + name = "seooc_tests", + tests = [ + ":seooc_tests_artifacts_copied", + ":seooc_tests_needs_provider", + ":seooc_tests_sphinx_module_generated", + ], +) + +# Combined test suite for all tests +test_suite( + name = "all_tests", + tests = [ + ":seooc_tests", + ":sphinx_module_tests", + ], +) diff --git a/bazel/rules/score_module/test/fixtures/module_a/index.rst b/bazel/rules/score_module/test/fixtures/module_a/index.rst new file mode 100644 index 0000000..573ad4b --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/module_a/index.rst @@ -0,0 +1,31 @@ +Module A Documentation +====================== + +This is the documentation for Module A. + +.. document:: Documentation for Module A + :id: doc__module_fixtures_module_a + :status: valid + :safety: ASIL_B + :security: NO + :realizes: wp__component_arch + +Overview +-------- + +Module A is a simple module that depends on Module C. + +Features +-------- + +.. needlist:: + :tags: module_a + +Cross-Module References +----------------------- + +General reference to Module C :external+module_c_lib:doc:`index`. + +Need reference to Module C :need:`doc__module_fixtures_module_c`. + +Need reference to Module B :need:`doc__module_fixtures_module_b`. diff --git a/bazel/rules/score_module/test/fixtures/module_b/index.rst b/bazel/rules/score_module/test/fixtures/module_b/index.rst new file mode 100644 index 0000000..3155c10 --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/module_b/index.rst @@ -0,0 +1,37 @@ +Module B Documentation +====================== + +This is the documentation for Module B. + +.. document:: Documentation for Module B + :id: doc__module_fixtures_module_b + :status: valid + :safety: ASIL_B + :security: NO + :realizes: + +Overview +-------- + +Module B depends on both Module A and Module C. + +Features +-------- + +.. needlist:: + :tags: module_b + +Cross-Module References +----------------------- + +This module references: + +* :external+module_a_lib:doc:`index` from Module A +* :external+module_c_lib:doc:`index` from Module C +* Need reference to Module C :need:`doc__module_fixtures_module_c` +* Need reference to Module C :need:`doc__module_fixtures_module_d` + +Dependencies +------------ + +Module B integrates functionality from both dependent modules. diff --git a/bazel/rules/score_module/test/fixtures/module_c/index.rst b/bazel/rules/score_module/test/fixtures/module_c/index.rst new file mode 100644 index 0000000..b73ae61 --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/module_c/index.rst @@ -0,0 +1,29 @@ +Module C Documentation +====================== + +This is the documentation for Module C. + +.. document:: Documentation for Module C + :id: doc__module_fixtures_module_c + :status: valid + :safety: ASIL_B + :security: NO + :realizes: + + +Overview +-------- + +Module C is a base module with no dependencies. +Local need link: :need:`doc__module_fixtures_module_c` + +Features +-------- + +.. needlist:: + :tags: module_c + +Content +------- + +Module C provides foundational functionality used by other modules. diff --git a/bazel/rules/score_module/test/fixtures/seooc_test/architectural_design.rst b/bazel/rules/score_module/test/fixtures/seooc_test/architectural_design.rst new file mode 100644 index 0000000..02e96f7 --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/seooc_test/architectural_design.rst @@ -0,0 +1,174 @@ +Architectural Design +==================== + +This document describes the architectural design of the test SEooC module. + +Software Architecture Overview +------------------------------- + +The system consists of the following software components: + +.. comp_arc_sta:: Input Processing Module + :id: comp_arc_sta__seooc_test__input_processing_module + :status: valid + :tags: architecture, component, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__input_data_processing, comp_req__seooc_test__can_message_reception + + Responsible for receiving and validating input data from CAN interface. + + **Inputs**: Raw CAN messages + + **Outputs**: Validated data structures + + **Safety Mechanisms**: CRC validation, sequence counter check + +.. comp_arc_sta:: Data Processing Engine + :id: comp_arc_sta__seooc_test__data_processing_engine + :status: valid + :tags: architecture, component, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__output_accuracy, comp_req__seooc_test__redundant_calculation + + Core processing component that performs calculations on validated data. + + **Inputs**: Validated data from Input Processing Module + + **Outputs**: Processed results + + **Safety Mechanisms**: Dual-channel redundant calculation + +.. comp_arc_sta:: Output Handler + :id: comp_arc_sta__seooc_test__output_handler + :status: valid + :tags: architecture, component, seooc_test + :safety: QM + :security: NO + :fulfils: comp_req__seooc_test__can_message_transmission + + Formats and transmits output data via CAN interface. + + **Inputs**: Processed results from Data Processing Engine + + **Outputs**: CAN messages + + **Safety Mechanisms**: Message sequence numbering, alive counter + +.. comp_arc_sta:: Fault Detection and Handling + :id: comp_arc_sta__seooc_test__fault_detection_handling + :status: valid + :tags: architecture, component, safety, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__fault_detection, comp_req__seooc_test__safe_state_transition + + Monitors system health and handles fault conditions. + + **Inputs**: Status from all components + + **Outputs**: System state, error flags + + **Safety Mechanisms**: Watchdog timer, plausibility checks + +Component Interfaces +--------------------- + +Interface: CAN Communication +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. real_arc_int:: CAN RX Interface + :id: real_arc_int__seooc_test__can_rx + :status: valid + :tags: interface, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__can_message_reception + :language: cpp + + * **Protocol**: CAN 2.0B + * **Baud Rate**: 500 kbps + * **Message ID Range**: 0x100-0x1FF + * **DLC**: 8 bytes + +.. real_arc_int:: CAN TX Interface + :id: real_arc_int__seooc_test__can_tx + :status: valid + :tags: interface, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__can_message_transmission + :language: cpp + + * **Protocol**: CAN 2.0B + * **Baud Rate**: 500 kbps + * **Message ID Range**: 0x200-0x2FF + * **DLC**: 8 bytes + +Design Decisions +---------------- + +.. comp_arc_dyn:: Use of Hardware Watchdog + :id: comp_arc_dyn__seooc_test__hw_watchdog + :status: valid + :tags: design-decision, safety, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__fault_detection + + The architecture includes a hardware watchdog timer to ensure system + reliability and meet safety requirements. + + **Rationale**: Hardware watchdog provides independent monitoring + of software execution and can detect timing violations. + + **Alternatives Considered**: Software-only monitoring (rejected due + to lower ASIL coverage) + +.. comp_arc_dyn:: Redundant Processing Paths + :id: comp_arc_dyn__seooc_test__redundancy + :status: valid + :tags: design-decision, safety, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__redundant_calculation + + Critical calculations are performed using redundant processing paths + to detect and prevent silent data corruption. + + **Rationale**: Meets ASIL-B requirements for detection of random + hardware faults during calculation. + + **Implementation**: Main path + shadow path with result comparison + +Memory Architecture +------------------- + +.. comp_arc_sta:: RAM Allocation + :id: comp_arc_sta__seooc_test__ram_allocation + :status: valid + :tags: resource, memory, seooc_test + :safety: QM + :security: NO + :fulfils: aou_req__seooc_test__memory_requirements + + * **Total RAM**: 512 KB + * **Stack**: 64 KB + * **Heap**: 128 KB + * **Static Data**: 256 KB + * **Reserved**: 64 KB + +.. comp_arc_sta:: Flash Allocation + :id: comp_arc_sta__seooc_test__flash_allocation + :status: valid + :tags: resource, memory, seooc_test + :safety: QM + :security: NO + :fulfils: aou_req__seooc_test__memory_requirements + + * **Total Flash**: 2 MB + * **Application Code**: 1.5 MB + * **Configuration Data**: 256 KB + * **Boot Loader**: 128 KB + * **Reserved**: 128 KB diff --git a/bazel/rules/score_module/test/fixtures/seooc_test/assumptions_of_use.rst b/bazel/rules/score_module/test/fixtures/seooc_test/assumptions_of_use.rst new file mode 100644 index 0000000..fae172c --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/seooc_test/assumptions_of_use.rst @@ -0,0 +1,80 @@ +Assumptions of Use +================== + +This document describes the assumptions of use for the test SEooC module. + +.. aou_req:: Operating Temperature Range + :id: aou_req__seooc_test__operating_temperature_range + :status: valid + :tags: environment, iso26262, seooc_test + :safety: ASIL_B + :security: NO + + The SEooC shall operate within temperature range -40°C to +85°C. + +.. aou_req:: Supply Voltage + :id: aou_req__seooc_test__supply_voltage + :status: valid + :tags: power, iso26262, seooc_test + :safety: ASIL_B + :security: NO + + The SEooC shall operate with supply voltage 12V ±10%. + + Maximum current consumption: 2.5A + +.. aou_req:: Processing Load + :id: aou_req__seooc_test__processing_load + :status: valid + :tags: performance, iso26262, seooc_test + :safety: ASIL_B + :security: NO + + The maximum processing load shall not exceed 80% to ensure + timing requirements are met. + +Environmental Assumptions +------------------------- + +.. aou_req:: Controlled Environment + :id: aou_req__seooc_test__controlled_environment + :status: valid + :tags: environment, seooc_test + :safety: ASIL_B + :security: NO + + The system operates in a controlled automotive environment + compliant with ISO 16750 standards. + +.. aou_req:: Maintenance + :id: aou_req__seooc_test__maintenance + :status: valid + :tags: maintenance, seooc_test + :safety: ASIL_B + :security: NO + + Regular maintenance is performed according to the maintenance + schedule defined in the integration manual. + +Integration Constraints +----------------------- + +.. aou_req:: CAN Bus Interface + :id: aou_req__seooc_test__can_bus_interface + :status: valid + :tags: interface, communication, seooc_test + :safety: ASIL_B + :security: NO + + The host system shall provide a CAN 2.0B compliant interface + for communication with the SEooC. + +.. aou_req:: Memory Requirements + :id: aou_req__seooc_test__memory_requirements + :status: valid + :tags: resource, seooc_test + :safety: ASIL_B + :security: NO + + The host system shall provide at least 512KB of RAM and + 2MB of flash memory for the SEooC. diff --git a/bazel/rules/score_module/test/fixtures/seooc_test/component_requirements.rst b/bazel/rules/score_module/test/fixtures/seooc_test/component_requirements.rst new file mode 100644 index 0000000..1d7f90c --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/seooc_test/component_requirements.rst @@ -0,0 +1,105 @@ +Component Requirements +====================== + +This document defines the functional and safety requirements. + +Functional Requirements +------------------------ + +.. comp_req:: Input Data Processing + :id: comp_req__seooc_test__input_data_processing + :status: valid + :tags: functional, performance, seooc_test + :safety: QM + :security: NO + :satisfies: aou_req__seooc_test__processing_load + + The system shall process input data within 100ms from reception. + + **Rationale**: Real-time processing required for control loop. + +.. comp_req:: Output Accuracy + :id: comp_req__seooc_test__output_accuracy + :status: valid + :tags: functional, quality, seooc_test + :safety: QM + :security: NO + + The system shall provide output with 99.9% accuracy under + nominal operating conditions. + +.. comp_req:: Data Logging + :id: comp_req__seooc_test__data_logging + :status: valid + :tags: functional, diagnostic, seooc_test + :safety: QM + :security: NO + + The system shall log all error events with timestamp and + error code to non-volatile memory. + +Safety Requirements +------------------- + +.. comp_req:: Fault Detection + :id: comp_req__seooc_test__fault_detection + :status: valid + :tags: safety, seooc_test + :safety: ASIL_B + :security: NO + :satisfies: aou_req__seooc_test__processing_load + + The system shall detect and handle fault conditions within 50ms. + + **ASIL Level**: ASIL-B + **Safety Mechanism**: Watchdog timer + plausibility checks + +.. comp_req:: Safe State Transition + :id: comp_req__seooc_test__safe_state_transition + :status: valid + :tags: safety, seooc_test + :safety: ASIL_B + :security: NO + + The system shall maintain safe state during power loss and + complete shutdown within 20ms. + + **ASIL Level**: ASIL-B + **Safe State**: All outputs disabled, error flag set + +.. comp_req:: Redundant Calculation + :id: comp_req__seooc_test__redundant_calculation + :status: valid + :tags: safety, seooc_test + :safety: ASIL_B + :security: NO + + Critical calculations shall be performed using redundant + processing paths with comparison. + + **ASIL Level**: ASIL-B + **Safety Mechanism**: Dual-channel processing + +Communication Requirements +--------------------------- + +.. comp_req:: CAN Message Transmission + :id: comp_req__seooc_test__can_message_transmission + :status: valid + :tags: functional, communication, seooc_test + :safety: QM + :security: NO + :satisfies: aou_req__seooc_test__can_bus_interface + + The system shall transmit status messages on CAN bus + every 100ms ±10ms. + +.. comp_req:: CAN Message Reception + :id: comp_req__seooc_test__can_message_reception + :status: valid + :tags: functional, communication, seooc_test + :safety: QM + :security: NO + :satisfies: aou_req__seooc_test__can_bus_interface + + The system shall process received CAN messages within 10ms. diff --git a/bazel/rules/score_module/test/fixtures/seooc_test/dependability_analysis.rst b/bazel/rules/score_module/test/fixtures/seooc_test/dependability_analysis.rst new file mode 100644 index 0000000..ea5b518 --- /dev/null +++ b/bazel/rules/score_module/test/fixtures/seooc_test/dependability_analysis.rst @@ -0,0 +1,292 @@ +Safety Analysis +=============== + +This document contains the safety analysis for the test SEooC module. + +Failure Mode and Effects Analysis (FMEA) +----------------------------------------- + +.. comp_saf_fmea:: Input Data Corruption + :id: comp_saf_fmea__seooc_test__input_data_corruption + :status: valid + :tags: fmea, safety, seooc_test + :violates: comp_arc_sta__seooc_test__input_processing_module + :fault_id: bit_flip + :failure_effect: Corrupted input data from CAN bus due to electromagnetic interference, transmission errors, or faulty sensor leading to incorrect processing results + :mitigated_by: comp_req__seooc_test__fault_detection + :sufficient: yes + + **Failure Mode**: Corrupted input data from CAN bus + + **Potential Causes**: + + * Electromagnetic interference + * Transmission errors + * Faulty sensor + + **Effects**: Incorrect processing results, potential unsafe output + + **Severity**: High (S9) + + **Occurrence**: Medium (O4) + + **Detection**: High (D2) + + **RPN**: 72 + + **Detection Method**: CRC checksum validation, sequence counter check + + **Mitigation**: Reject invalid data and enter safe state within 50ms + +.. comp_saf_fmea:: Processing Timeout + :id: comp_saf_fmea__seooc_test__processing_timeout + :status: valid + :tags: fmea, safety, seooc_test + :violates: comp_arc_sta__seooc_test__fault_detection_handling + :fault_id: timing_failure + :failure_effect: Processing exceeds time deadline due to software defect, CPU overload, or hardware fault causing system unresponsiveness + :mitigated_by: comp_req__seooc_test__fault_detection + :sufficient: yes + + **Failure Mode**: Processing exceeds time deadline + + **Potential Causes**: + + * Software defect (infinite loop) + * CPU overload + * Hardware fault + + **Effects**: System becomes unresponsive, watchdog reset + + **Severity**: Medium (S6) + + **Occurrence**: Low (O3) + + **Detection**: Very High (D1) + + **RPN**: 18 + + **Detection Method**: Hardware watchdog timer + + **Mitigation**: System reset and recovery to safe state + +.. comp_saf_fmea:: Calculation Error + :id: comp_saf_fmea__seooc_test__calculation_error + :status: valid + :tags: fmea, safety, seooc_test + :violates: comp_arc_sta__seooc_test__data_processing_engine + :fault_id: seu + :failure_effect: Incorrect calculation result due to single event upset, register corruption, or ALU malfunction + :mitigated_by: comp_req__seooc_test__redundant_calculation + :sufficient: yes + + **Failure Mode**: Incorrect calculation result due to random hardware fault + + **Potential Causes**: + + * Single event upset (SEU) + * Register corruption + * ALU malfunction + + **Effects**: Incorrect output values + + **Severity**: High (S8) + + **Occurrence**: Very Low (O2) + + **Detection**: High (D2) + + **RPN**: 32 + + **Detection Method**: Dual-channel redundant calculation with comparison + + **Mitigation**: Discard result and use previous valid value, set error flag + +Dependent Failure Analysis (DFA) +--------------------------------- + +.. comp_saf_dfa:: System Failure Top Event + :id: comp_saf_dfa__seooc_test__system_failure_top + :status: valid + :tags: dfa, safety, seooc_test + :violates: comp_arc_sta__seooc_test__data_processing_engine + :failure_id: common_cause + :failure_effect: System provides unsafe output due to common cause failures affecting multiple safety mechanisms simultaneously + :mitigated_by: aou_req__seooc_test__controlled_environment + :sufficient: yes + + **Top Event**: System provides unsafe output + + **Goal**: Probability < 1e-6 per hour (ASIL-B target) + +.. comp_saf_dfa:: Hardware Failure Branch + :id: comp_saf_dfa__seooc_test__hw_failure + :status: valid + :tags: dfa, safety, seooc_test + :violates: comp_arc_sta__seooc_test__data_processing_engine + :failure_id: hw_common_mode + :failure_effect: Hardware component failures due to common cause (overvoltage, overtemperature) affecting multiple components + :mitigated_by: aou_req__seooc_test__operating_temperature_range, aou_req__seooc_test__supply_voltage + :sufficient: yes + + **Event**: Hardware component failure + + **Sub-events**: + + * Microcontroller failure (λ = 5e-7) + * Power supply failure (λ = 3e-7) + * CAN transceiver failure (λ = 2e-7) + + **Combined Probability**: 1.0e-6 per hour + +.. comp_saf_dfa:: Software Failure Branch + :id: comp_saf_dfa__seooc_test__sw_failure + :status: valid + :tags: dfa, safety, seooc_test + :violates: comp_arc_sta__seooc_test__data_processing_engine + :failure_id: sw_systematic + :failure_effect: Software defect affecting both processing channels due to systematic fault in common code base + :mitigated_by: comp_req__seooc_test__redundant_calculation + :sufficient: yes + + **Event**: Software defect leads to unsafe output + + **Sub-events**: + + * Undetected software bug (λ = 8e-6, detection coverage 90%) + * Memory corruption (λ = 1e-7) + + **Combined Probability**: 9e-7 per hour (after detection coverage) + +.. comp_saf_dfa:: External Interference Branch + :id: comp_saf_dfa__seooc_test__ext_interference + :status: valid + :tags: dfa, safety, seooc_test + :violates: comp_arc_sta__seooc_test__input_processing_module + :failure_id: emi + :failure_effect: External interference causing simultaneous malfunction of multiple components + :mitigated_by: aou_req__seooc_test__controlled_environment + :sufficient: yes + + **Event**: External interference causes malfunction + + **Sub-events**: + + * EMI beyond specification (λ = 5e-8) + * Voltage transient (λ = 2e-8, mitigation 99%) + + **Combined Probability**: 5.2e-8 per hour (after mitigation) + +**Total System Failure Probability**: 1.95e-6 per hour + +**ASIL-B Target**: < 1e-5 per hour ✓ **PASSED** + +Safety Mechanisms +----------------- + +.. comp_arc_sta:: SM: Input Validation + :id: comp_arc_sta__seooc_test__sm_input_validation + :status: valid + :tags: safety-mechanism, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__fault_detection + + **Description**: All input data is validated before processing + + **Checks Performed**: + + * CRC-16 checksum validation + * Message sequence counter verification + * Data range plausibility checks + + **Diagnostic Coverage**: 95% + + **Reaction**: Reject invalid data, increment error counter, use last valid value + +.. comp_arc_sta:: SM: Watchdog Timer + :id: comp_arc_sta__seooc_test__sm_watchdog + :status: valid + :tags: safety-mechanism, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__fault_detection + + **Description**: Hardware watchdog monitors software execution + + **Configuration**: + + * Timeout: 150ms + * Window watchdog: 100-140ms trigger window + * Reset delay: 10ms + + **Diagnostic Coverage**: 99% + + **Reaction**: System reset, boot to safe state + +.. comp_arc_sta:: SM: Redundant Calculation + :id: comp_arc_sta__seooc_test__sm_redundant_calc + :status: valid + :tags: safety-mechanism, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__redundant_calculation + + **Description**: Critical calculations performed in dual channels + + **Implementation**: + + * Main calculation path + * Independent shadow path + * Result comparison with tolerance check + + **Diagnostic Coverage**: 98% + + **Reaction**: On mismatch, use previous valid value, set error flag + +Safety Validation Results +-------------------------- + +.. comp_arc_dyn:: Validation: FMEA Coverage + :id: comp_arc_dyn__seooc_test__val_fmea_coverage + :status: valid + :tags: validation, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__fault_detection + + **Result**: All identified failure modes have detection mechanisms + + **Coverage**: 100% of critical failure modes + + **Status**: ✓ PASSED + +.. comp_arc_dyn:: Validation: DFA Target Achievement + :id: comp_arc_dyn__seooc_test__val_dfa_target + :status: valid + :tags: validation, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__safe_state_transition + + **Result**: System failure probability 1.95e-6 per hour + + **Target**: < 1e-5 per hour (ASIL-B) + + **Margin**: 5.1x + + **Status**: ✓ PASSED + +.. comp_arc_dyn:: Validation: Safety Mechanism Effectiveness + :id: comp_arc_dyn__seooc_test__val_sm_effectiveness + :status: valid + :tags: validation, seooc_test + :safety: ASIL_B + :security: NO + :fulfils: comp_req__seooc_test__redundant_calculation + + **Result**: Combined diagnostic coverage 97.3% + + **Target**: > 90% (ASIL-B) + + **Status**: ✓ PASSED diff --git a/bazel/rules/score_module/test/html_generation_test.bzl b/bazel/rules/score_module/test/html_generation_test.bzl new file mode 100644 index 0000000..6aeb47e --- /dev/null +++ b/bazel/rules/score_module/test/html_generation_test.bzl @@ -0,0 +1,223 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Test rules for sphinx_module HTML generation and dependencies.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//bazel/rules/score_module/private:sphinx_module.bzl", "ScoreModuleInfo", "ScoreNeedsInfo") + +# ============================================================================ +# Provider Tests +# ============================================================================ + +def _providers_test_impl(ctx): + """Test that sphinx_module provides the correct providers.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Verify required providers + asserts.true( + env, + ScoreModuleInfo in target_under_test, + "Target should provide ScoreModuleInfo", + ) + + asserts.true( + env, + DefaultInfo in target_under_test, + "Target should provide DefaultInfo", + ) + + return analysistest.end(env) + +providers_test = analysistest.make(_providers_test_impl) + +# ============================================================================ +# HTML Generation Tests +# ============================================================================ + +def _basic_html_generation_test_impl(ctx): + """Test that a simple document generates HTML output.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Check that HTML directory exists + score_info = target_under_test[ScoreModuleInfo] + asserts.true( + env, + score_info.html_dir != None, + "Module should generate HTML directory", + ) + + return analysistest.end(env) + +basic_html_generation_test = analysistest.make(_basic_html_generation_test_impl) + +# ============================================================================ +# Needs.json Generation Tests +# ============================================================================ + +def _needs_generation_test_impl(ctx): + """Test that sphinx_module generates needs.json files.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Check for ScoreNeedsInfo provider on _needs target + # Note: This test requires the _needs suffix target + asserts.true( + env, + DefaultInfo in target_under_test, + "Needs target should provide DefaultInfo", + ) + + return analysistest.end(env) + +needs_generation_test = analysistest.make(_needs_generation_test_impl) + +def _needs_transitive_test_impl(ctx): + """Test that needs.json files are collected transitively.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Verify ScoreNeedsInfo provider + asserts.true( + env, + ScoreNeedsInfo in target_under_test, + "Needs target should provide ScoreNeedsInfo", + ) + + needs_info = target_under_test[ScoreNeedsInfo] + + # Check direct needs.json file + asserts.true( + env, + needs_info.needs_json_file != None, + "Should have direct needs.json file", + ) + + # Check transitive needs collection + asserts.true( + env, + needs_info.needs_json_files != None, + "Should have transitive needs.json files depset", + ) + + return analysistest.end(env) + +needs_transitive_test = analysistest.make(_needs_transitive_test_impl) + +# ============================================================================ +# Dependency and Integration Tests +# ============================================================================ + +def _module_dependencies_test_impl(ctx): + """Test that module dependencies are properly handled.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module with dependencies should still generate HTML + asserts.true( + env, + score_info.html_dir != None, + "Module with dependencies should generate HTML", + ) + + return analysistest.end(env) + +module_dependencies_test = analysistest.make(_module_dependencies_test_impl) + +def _html_merging_test_impl(ctx): + """Test that HTML from dependencies is merged correctly.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Verify merged HTML output exists + asserts.true( + env, + score_info.html_dir != None, + "Merged HTML should be generated", + ) + + return analysistest.end(env) + +html_merging_test = analysistest.make(_html_merging_test_impl) + +# ============================================================================ +# Config Generation Tests +# ============================================================================ + +def _auto_config_generation_test_impl(ctx): + """Test that conf.py is automatically generated when not provided.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module without explicit config should still generate HTML + asserts.true( + env, + score_info.html_dir != None, + "Module with auto-generated config should produce HTML", + ) + + return analysistest.end(env) + +auto_config_generation_test = analysistest.make(_auto_config_generation_test_impl) + +def _explicit_config_test_impl(ctx): + """Test that explicit conf.py is used when provided.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module with explicit config should generate HTML + asserts.true( + env, + score_info.html_dir != None, + "Module with explicit config should produce HTML", + ) + + return analysistest.end(env) + +explicit_config_test = analysistest.make(_explicit_config_test_impl) + +# ============================================================================ +# Test Suite +# ============================================================================ + +def sphinx_module_test_suite(name): + """Create a comprehensive test suite for sphinx_module. + + Tests cover: + - Needs.json generation and transitive collection + - Module dependencies and HTML merging + + Args: + name: Name of the test suite + """ + + native.test_suite( + name = name, + tests = [ + # Needs generation + ":needs_transitive_test", + + # Dependencies and integration + ":module_dependencies_test", + ":html_merging_test", + ], + ) diff --git a/bazel/rules/score_module/test/score_module_providers_test.bzl b/bazel/rules/score_module/test/score_module_providers_test.bzl new file mode 100644 index 0000000..6fbbc9c --- /dev/null +++ b/bazel/rules/score_module/test/score_module_providers_test.bzl @@ -0,0 +1,323 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Tests for sphinx_module providers and two-phase build system.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//bazel/rules/score_module/private:score_module.bzl", "ScoreModuleInfo", "ScoreNeedsInfo") + +# ============================================================================ +# ScoreModuleInfo Provider Tests +# ============================================================================ + +def _sphinx_module_info_fields_test_impl(ctx): + """Test that ScoreModuleInfo provides all required fields.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + asserts.true( + env, + ScoreModuleInfo in target_under_test, + "Target should provide ScoreModuleInfo", + ) + + score_info = target_under_test[ScoreModuleInfo] + + # Verify html_dir field + asserts.true( + env, + hasattr(score_info, "html_dir"), + "ScoreModuleInfo should have html_dir field", + ) + + asserts.true( + env, + score_info.html_dir != None, + "html_dir should not be None", + ) + + return analysistest.end(env) + +sphinx_module_info_fields_test = analysistest.make(_sphinx_module_info_fields_test_impl) + +# ============================================================================ +# ScoreNeedsInfo Provider Tests +# ============================================================================ + +def _score_needs_info_fields_test_impl(ctx): + """Test that ScoreNeedsInfo provides all required fields.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + asserts.true( + env, + ScoreNeedsInfo in target_under_test, + "Needs target should provide ScoreNeedsInfo", + ) + + needs_info = target_under_test[ScoreNeedsInfo] + + # Verify needs_json_file field (direct file) + asserts.true( + env, + hasattr(needs_info, "needs_json_file"), + "ScoreNeedsInfo should have needs_json_file field", + ) + + asserts.true( + env, + needs_info.needs_json_file != None, + "needs_json_file should not be None", + ) + + # Verify needs_json_files field (transitive depset) + asserts.true( + env, + hasattr(needs_info, "needs_json_files"), + "ScoreNeedsInfo should have needs_json_files field", + ) + + asserts.true( + env, + needs_info.needs_json_files != None, + "needs_json_files should not be None", + ) + + # Verify it's a depset + asserts.true( + env, + type(needs_info.needs_json_files) == type(depset([])), + "needs_json_files should be a depset", + ) + + return analysistest.end(env) + +score_needs_info_fields_test = analysistest.make(_score_needs_info_fields_test_impl) + +def _score_needs_transitive_collection_test_impl(ctx): + """Test that needs.json files are collected transitively.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + needs_info = target_under_test[ScoreNeedsInfo] + + # Get the list of transitive needs files + transitive_needs = needs_info.needs_json_files.to_list() + + # Should have at least the direct needs file + asserts.true( + env, + len(transitive_needs) >= 1, + "Should have at least the direct needs.json file", + ) + + # Direct file should be in the transitive set + direct_file = needs_info.needs_json_file + asserts.true( + env, + direct_file in transitive_needs, + "Direct needs.json file should be in transitive collection", + ) + + return analysistest.end(env) + +score_needs_transitive_collection_test = analysistest.make(_score_needs_transitive_collection_test_impl) + +def _score_needs_with_deps_test_impl(ctx): + """Test that needs.json files include dependencies.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + needs_info = target_under_test[ScoreNeedsInfo] + transitive_needs = needs_info.needs_json_files.to_list() + + # Module with dependencies should have multiple needs files + # (its own + dependencies) + asserts.true( + env, + len(transitive_needs) >= 1, + "Module with dependencies should collect transitive needs.json files", + ) + + return analysistest.end(env) + +score_needs_with_deps_test = analysistest.make(_score_needs_with_deps_test_impl) + +# ============================================================================ +# Two-Phase Build Tests +# ============================================================================ + +def _two_phase_needs_first_test_impl(ctx): + """Test that Phase 1 (needs generation) works independently.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Verify ScoreNeedsInfo provider + asserts.true( + env, + ScoreNeedsInfo in target_under_test, + "Phase 1 should provide ScoreNeedsInfo", + ) + + # Verify DefaultInfo with needs.json output + asserts.true( + env, + DefaultInfo in target_under_test, + "Phase 1 should provide DefaultInfo", + ) + + default_info = target_under_test[DefaultInfo] + files = default_info.files.to_list() + + # Should have at least one file (needs.json) + asserts.true( + env, + len(files) >= 1, + "Phase 1 should output needs.json file", + ) + + return analysistest.end(env) + +two_phase_needs_first_test = analysistest.make(_two_phase_needs_first_test_impl) + +def _two_phase_html_second_test_impl(ctx): + """Test that Phase 2 (HTML generation) works with needs from Phase 1.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Verify ScoreModuleInfo provider + asserts.true( + env, + ScoreModuleInfo in target_under_test, + "Phase 2 should provide ScoreModuleInfo", + ) + + score_info = target_under_test[ScoreModuleInfo] + + # Verify HTML output + asserts.true( + env, + score_info.html_dir != None, + "Phase 2 should generate HTML directory", + ) + + return analysistest.end(env) + +two_phase_html_second_test = analysistest.make(_two_phase_html_second_test_impl) + +# ============================================================================ +# Config Generation Tests +# ============================================================================ + +def _config_auto_generation_test_impl(ctx): + """Test that conf.py is auto-generated when not provided.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module without explicit config should still build + asserts.true( + env, + score_info.html_dir != None, + "Auto-generated config should allow HTML generation", + ) + + return analysistest.end(env) + +config_auto_generation_test = analysistest.make(_config_auto_generation_test_impl) + +def _config_explicit_usage_test_impl(ctx): + """Test that explicit conf.py is used when provided.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module with explicit config should build + asserts.true( + env, + score_info.html_dir != None, + "Explicit config should allow HTML generation", + ) + + return analysistest.end(env) + +config_explicit_usage_test = analysistest.make(_config_explicit_usage_test_impl) + +# ============================================================================ +# Dependency Handling Tests +# ============================================================================ + +def _deps_html_merging_test_impl(ctx): + """Test that HTML from dependencies is merged into output.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + score_info = target_under_test[ScoreModuleInfo] + + # Module with dependencies should generate merged HTML + asserts.true( + env, + score_info.html_dir != None, + "Module with dependencies should generate merged HTML", + ) + + return analysistest.end(env) + +deps_html_merging_test = analysistest.make(_deps_html_merging_test_impl) + +def _deps_needs_collection_test_impl(ctx): + """Test that needs from dependencies are collected.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + needs_info = target_under_test[ScoreNeedsInfo] + transitive_needs = needs_info.needs_json_files.to_list() + + # Should collect needs from dependencies + asserts.true( + env, + len(transitive_needs) >= 1, + "Should collect needs.json from dependencies", + ) + + return analysistest.end(env) + +deps_needs_collection_test = analysistest.make(_deps_needs_collection_test_impl) + +# ============================================================================ +# Test Suite +# ============================================================================ + +def sphinx_module_providers_test_suite(name): + """Create a test suite for sphinx_module providers and build phases. + + Tests cover: + - Transitive needs.json collection + - Dependency handling (HTML merging, needs collection) + + Args: + name: Name of the test suite + """ + + native.test_suite( + name = name, + tests = [ + # Provider tests + ":score_needs_with_deps_test", + + # Dependency tests + ":deps_html_merging_test", + ":deps_needs_collection_test", + ], + ) diff --git a/bazel/rules/score_module/test/seooc_test.bzl b/bazel/rules/score_module/test/seooc_test.bzl new file mode 100644 index 0000000..bc05ceb --- /dev/null +++ b/bazel/rules/score_module/test/seooc_test.bzl @@ -0,0 +1,136 @@ +""" +Test suite for score_component macro. + +Tests the SEooC (Safety Element out of Context) functionality including: +- Index generation with artifact references +- Integration with sphinx_module +- Sphinx-needs cross-referencing +- HTML output generation +""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//bazel/rules/score_module/private:sphinx_module.bzl", "ScoreModuleInfo", "ScoreNeedsInfo") + +def _seooc_index_generation_test_impl(ctx): + """Test that SEooC generates proper index.rst file.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Get the generated index file + files = target_under_test[DefaultInfo].files.to_list() + + # Find index.rst in the output files + index_file = None + for f in files: + if f.basename == "index.rst": + index_file = f + break + + # Assert index file exists + asserts.true( + env, + index_file != None, + "Expected index.rst to be generated by _generate_seooc_index rule", + ) + + return analysistest.end(env) + +seooc_index_generation_test = analysistest.make( + impl = _seooc_index_generation_test_impl, +) + +def _seooc_artifacts_copied_test_impl(ctx): + """Test that all SEooC artifacts are copied to output directory.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + files = target_under_test[DefaultInfo].files.to_list() + + # Expected artifact basenames + expected_artifacts = [ + "assumptions_of_use.rst", + "component_requirements.rst", + "architectural_design.rst", + "dependability_analysis.rst", + ] + + # Check each artifact exists + actual_basenames = [f.basename for f in files] + for artifact in expected_artifacts: + asserts.true( + env, + artifact in actual_basenames, + "Expected artifact '{}' to be in output files".format(artifact), + ) + + return analysistest.end(env) + +seooc_artifacts_copied_test = analysistest.make( + impl = _seooc_artifacts_copied_test_impl, +) + +def _seooc_sphinx_module_generated_test_impl(ctx): + """Test that SEooC generates sphinx_module with HTML output.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Check that ScoreModuleInfo provider exists + asserts.true( + env, + ScoreModuleInfo in target_under_test, + "Expected SEooC to provide ScoreModuleInfo from sphinx_module", + ) + + return analysistest.end(env) + +seooc_sphinx_module_generated_test = analysistest.make( + impl = _seooc_sphinx_module_generated_test_impl, +) + +def _seooc_needs_provider_test_impl(ctx): + """Test that SEooC generates needs provider for cross-referencing.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Check that ScoreNeedsInfo provider exists + asserts.true( + env, + ScoreNeedsInfo in target_under_test, + "Expected SEooC_needs to provide ScoreNeedsInfo", + ) + + return analysistest.end(env) + +seooc_needs_provider_test = analysistest.make( + impl = _seooc_needs_provider_test_impl, +) + +def _seooc_description_test_impl(ctx): + """Test that SEooC includes description in generated index.rst.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # Get the generated index file + files = target_under_test[DefaultInfo].files.to_list() + + # Find index.rst + index_file = None + for f in files: + if f.basename == "index.rst": + index_file = f + break + + # Note: We can't easily read file contents in analysis test, + # but we can verify the file exists. The description content + # would be validated through integration tests or manual inspection. + asserts.true( + env, + index_file != None, + "Expected index.rst to exist for description validation", + ) + + return analysistest.end(env) + +seooc_description_test = analysistest.make( + impl = _seooc_description_test_impl, +) diff --git a/cr_checker/tests/.bazelversion b/cr_checker/tests/.bazelversion new file mode 100644 index 0000000..2bf50aa --- /dev/null +++ b/cr_checker/tests/.bazelversion @@ -0,0 +1 @@ +8.3.0 diff --git a/starpls/integration_tests/.bazelversion b/starpls/integration_tests/.bazelversion new file mode 100644 index 0000000..2bf50aa --- /dev/null +++ b/starpls/integration_tests/.bazelversion @@ -0,0 +1 @@ +8.3.0