diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 03f7457..6b53f2f 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -6,6 +6,8 @@ load( exports_files([ "templates/conf.template.py", "templates/seooc_index.template.rst", + "templates/unit.template.rst", + "templates/component.template.rst", ]) # HTML merge tool diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 158b92b..36c584f 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -20,6 +20,7 @@ operating conditions and constraints for a Safety Element out of Context (SEooC) """ load("//bazel/rules/rules_score:providers.bzl", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:component_requirements.bzl", "ComponentRequirementsInfo") load("//bazel/rules/rules_score/private:feature_requirements.bzl", "FeatureRequirementsInfo") # ============================================================================ @@ -55,13 +56,13 @@ def _assumptions_of_use_impl(ctx): # Collect feature requirements providers feature_reqs = [] - for feat_req in ctx.attr.feature_requirement: + for feat_req in ctx.attr.feature_requirements: if FeatureRequirementsInfo in feat_req: feature_reqs.append(feat_req[FeatureRequirementsInfo]) # Collect transitive sphinx sources from feature requirements transitive = [srcs] - for feat_req in ctx.attr.feature_requirement: + for feat_req in ctx.attr.feature_requirements: if SphinxSourcesInfo in feat_req: transitive.append(feat_req[SphinxSourcesInfo].transitive_srcs) @@ -91,11 +92,16 @@ _assumptions_of_use = rule( mandatory = True, doc = "Source files containing Assumptions of Use specifications", ), - "feature_requirement": attr.label_list( + "feature_requirements": attr.label_list( providers = [FeatureRequirementsInfo], mandatory = False, doc = "List of feature_requirements targets that these Assumptions of Use trace to", ), + "component_requirements": attr.label_list( + providers = [ComponentRequirementsInfo], + mandatory = False, + doc = "List of feature_requirements targets that these Assumptions of Use trace to", + ), }, ) @@ -107,6 +113,7 @@ def assumptions_of_use( name, srcs, feature_requirement = [], + component_requirements = [], visibility = None): """Define Assumptions of Use following S-CORE process guidelines. @@ -141,6 +148,7 @@ def assumptions_of_use( _assumptions_of_use( name = name, srcs = srcs, - feature_requirement = feature_requirement, + feature_requirements = feature_requirement, + component_requirements = component_requirements, visibility = visibility, ) diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index 0966a03..4e41b9e 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -46,9 +46,6 @@ def _component_impl(ctx): requirements_depset = depset(transitive = requirements_files) - # Collect implementation targets - implementation_depset = depset(ctx.attr.implementation) - # Collect components and tests components_depset = depset(ctx.attr.components) tests_depset = depset(ctx.attr.tests) @@ -63,7 +60,6 @@ def _component_impl(ctx): ComponentInfo( name = ctx.label.name, requirements = requirements_depset, - implementation = implementation_depset, components = components_depset, tests = tests_depset, ), @@ -85,10 +81,6 @@ _component = rule( mandatory = True, doc = "Component requirements artifacts (typically component_requirements targets)", ), - "implementation": attr.label_list( - doc = "Implementation targets (libraries, binaries) that realize this component", - default = [], - ), "components": attr.label_list( mandatory = True, doc = "Unit targets that comprise this component", @@ -108,7 +100,6 @@ def component( name, units = None, tests = [], - implementation = [], requirements = None, components = None, testonly = True, @@ -152,7 +143,6 @@ def component( _component( name = name, requirements = requirements, - implementation = implementation, components = components, tests = tests, testonly = testonly, diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 05c7f36..0d31738 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -22,7 +22,7 @@ a comprehensive view of component reliability and safety. load("//bazel/rules/rules_score:providers.bzl", "SphinxSourcesInfo") load("//bazel/rules/rules_score/private:architectural_design.bzl", "ArchitecturalDesignInfo") -load("//bazel/rules/rules_score/private:safety_analysis.bzl", "SafetyAnalysisInfo") +load("//bazel/rules/rules_score/private:safety_analysis.bzl", "AnalysisInfo") # ============================================================================ # Provider Definition @@ -31,9 +31,8 @@ load("//bazel/rules/rules_score/private:safety_analysis.bzl", "SafetyAnalysisInf DependabilityAnalysisInfo = provider( doc = "Provider for dependability analysis artifacts", fields = { - "safety_analysis": "List of SafetyAnalysisInfo providers", - "dfa": "Depset of Dependent Failure Analysis documentation", - "fmea": "Depset of Failure Mode and Effects Analysis documentation", + "safety_analysis": "List of AnalysisInfo providers", + "security_analysis": "List of AnalysisInfo providers", "arch_design": "ArchitecturalDesignInfo provider for linked architectural design", "name": "Name of the dependability analysis target", }, @@ -55,16 +54,8 @@ def _dependability_analysis_impl(ctx): Returns: List of providers including DefaultInfo and DependabilityAnalysisInfo """ - dfa_files = depset(ctx.files.dfa) - fmea_files = depset(ctx.files.fmea) - - # Collect safety analysis providers - safety_analysis_infos = [] - safety_analysis_files = [] - for sa in ctx.attr.safety_analysis: - if SafetyAnalysisInfo in sa: - safety_analysis_infos.append(sa[SafetyAnalysisInfo]) - safety_analysis_files.append(sa.files) + security_analysis_files = depset(ctx.files.dfa) + safety_analysis_files = depset(ctx.files.fmea) # Get architectural design provider if available arch_design_info = None @@ -72,13 +63,11 @@ def _dependability_analysis_impl(ctx): arch_design_info = ctx.attr.arch_design[ArchitecturalDesignInfo] # Combine all files for DefaultInfo - all_files = depset( - transitive = [dfa_files, fmea_files] + safety_analysis_files, - ) + all_files = depset(transitive = [security_analysis_files, safety_analysis_files]) # Collect transitive sphinx sources from safety analysis and architectural design transitive = [all_files] - for sa in ctx.attr.safety_analysis: + for sa in ctx.attr.security_analysis: if SphinxSourcesInfo in sa: transitive.append(sa[SphinxSourcesInfo].transitive_srcs) if ctx.attr.arch_design and SphinxSourcesInfo in ctx.attr.arch_design: @@ -87,9 +76,8 @@ def _dependability_analysis_impl(ctx): return [ DefaultInfo(files = all_files), DependabilityAnalysisInfo( - safety_analysis = safety_analysis_infos, - dfa = dfa_files, - fmea = fmea_files, + safety_analysis = security_analysis_files, + security_analysis = security_analysis_files, arch_design = arch_design_info, name = ctx.label.name, ), @@ -107,8 +95,9 @@ _dependability_analysis = rule( implementation = _dependability_analysis_impl, doc = "Collects dependability analysis documents for S-CORE process compliance", attrs = { - "safety_analysis": attr.label_list( - providers = [SafetyAnalysisInfo], + "security_analysis": attr.label_list( + # TODO: change provider name + providers = [AnalysisInfo], mandatory = False, doc = "List of safety_analysis targets containing FMEA, FMEDA, FTA results", ), @@ -183,7 +172,10 @@ def dependability_analysis( """ _dependability_analysis( name = name, - safety_analysis = safety_analysis, + # TODO: this needs to be fixed. A security is not a safety_analysis. + # we leave it for now for compatibility reasons until there is alignment on the a + # attributes of a dependability analysis + security_analysis = safety_analysis, dfa = dfa, fmea = fmea, arch_design = arch_design, diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index f193cc4..da368f9 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -22,10 +22,67 @@ assumptions of use, requirements, design, and safety analysis. load( "//bazel/rules/rules_score:providers.bzl", + "ComponentInfo", "SphinxSourcesInfo", + "UnitInfo", ) load("//bazel/rules/rules_score/private:sphinx_module.bzl", "sphinx_module") +# ============================================================================ +# Template Constants +# ============================================================================ + +_UNIT_DESIGN_SECTION_TEMPLATE = """Unit Design +----------- + +.. toctree:: + :maxdepth: 2 + +{design_refs}""" + +_IMPLEMENTATION_SECTION_TEMPLATE = """Implementation +-------------- + +This {entity_type} is implemented by the following targets: + +{implementation_list}""" + +_TESTS_SECTION_TEMPLATE = """Tests +----- + +This {entity_type} is verified by the following test targets: + +{test_list}""" + +_COMPONENT_REQUIREMENTS_SECTION_TEMPLATE = """Component Requirements +---------------------- + +.. toctree:: + :maxdepth: 2 + +{requirements_refs}""" + +_COMPONENT_UNITS_SECTION_TEMPLATE = """Units +----- + +This component is composed of the following units: + +{unit_links}""" + +_UNIT_TEMPLATE = """ + +Unit: {unit_name} +{underline} + +{design_section}{implementation_section}{tests_section}""" + +_COMPONENT_TEMPLATE = """ + +Component: {component_name} +{underline} + +{requirements_section}{units_section}{implementation_section}{tests_section}""" + # ============================================================================ # Helper Functions for Documentation Generation # ============================================================================ @@ -252,6 +309,242 @@ def _process_deps(ctx): def _get_component_names(components): return [c.label.name for c in components] +def _collect_units_recursive(components, visited_units = None): + """Iteratively collect all units from components, handling nested components. + + Uses a stack-based approach to avoid Starlark recursion limitations. + + Args: + components: List of component targets + visited_units: Dict of unit names already visited (for deduplication) + + Returns: + Dict mapping unit names to unit targets + """ + if visited_units == None: + visited_units = {} + + # Process components iteratively using a work queue approach + # Since Starlark doesn't support while loops, we use a for loop with a large enough range + # and track our own index + to_process = [] + components + + for _ in range(1000): # Max depth to prevent infinite loops + if not to_process: + break + comp_target = to_process.pop(0) + + # Check if this is a component with ComponentInfo + if ComponentInfo in comp_target: + comp_info = comp_target[ComponentInfo] + + # Process nested components + nested_components = comp_info.components.to_list() + for nested in nested_components: + # Check if nested item is a unit or component + if UnitInfo in nested: + unit_name = nested.label.name + if unit_name not in visited_units: + visited_units[unit_name] = nested + elif ComponentInfo in nested: + # Add nested component to queue for processing + to_process.append(nested) + + # Check if this is directly a unit + elif UnitInfo in comp_target: + unit_name = comp_target.label.name + if unit_name not in visited_units: + visited_units[unit_name] = comp_target + + return visited_units + +def _generate_unit_doc(ctx, unit_target, unit_name): + """Generate RST documentation for a single unit. + + Args: + ctx: Rule context + unit_target: The unit target + unit_name: Name of the unit + + Returns: + Tuple of (rst_file, list_of_output_files) + """ + unit_info = unit_target[UnitInfo] + + # Create RST file for this unit + unit_rst = ctx.actions.declare_file(ctx.label.name + "/units/" + unit_name + ".rst") + + # Collect design files - unit_design depset contains File objects + design_files = [] + design_refs = [] + if unit_info.unit_design: + doc_files = _filter_doc_files(unit_info.unit_design.to_list()) + + if doc_files: + # Find common directory + common_dir = _find_common_directory(doc_files) + + for f in doc_files: + relative_path = _compute_relative_path(f, common_dir) + output_file = _create_artifact_symlink( + ctx, + "units/" + unit_name + "_design", + f, + relative_path, + ) + design_files.append(output_file) + + if _is_document_file(f): + doc_ref = ("units/" + unit_name + "_design/" + relative_path) \ + .replace(".rst", "") \ + .replace(".md", "") + design_refs.append(" " + doc_ref) + + # Collect implementation target names + impl_names = [] + if unit_info.implementation: + for impl in unit_info.implementation.to_list(): + impl_names.append(impl.label) + + # Collect test target names + test_names = [] + if unit_info.tests: + for test in unit_info.tests.to_list(): + test_names.append(test.label) + + # Generate RST content using template + underline = "=" * (len("Unit: " + unit_name)) + + # Generate sections from template constants + design_section = "" + if design_refs: + design_section = "\n" + _UNIT_DESIGN_SECTION_TEMPLATE.format( + design_refs = "\n".join(design_refs), + ) + "\n" + + implementation_section = "" + if impl_names: + impl_list = "\n".join(["- ``" + str(impl) + "``" for impl in impl_names]) + implementation_section = "\n" + _IMPLEMENTATION_SECTION_TEMPLATE.format( + entity_type = "unit", + implementation_list = impl_list, + ) + "\n" + + tests_section = "" + if test_names: + test_list = "\n".join(["- ``" + str(test) + "``" for test in test_names]) + tests_section = "\n" + _TESTS_SECTION_TEMPLATE.format( + entity_type = "unit", + test_list = test_list, + ) + "\n" + + # Generate unit RST content from template constant + unit_content = _UNIT_TEMPLATE.format( + unit_name = unit_name, + underline = underline, + design_section = design_section, + implementation_section = implementation_section, + tests_section = tests_section, + ) + + ctx.actions.write( + output = unit_rst, + content = unit_content, + ) + + return (unit_rst, design_files) + +def _generate_component_doc(ctx, comp_target, comp_name, unit_names): + """Generate RST documentation for a single component. + + Args: + ctx: Rule context + comp_target: The component target + comp_name: Name of the component + unit_names: List of unit names that belong to this component + + Returns: + Tuple of (rst_file, list_of_output_files) + """ + comp_info = comp_target[ComponentInfo] + + # Create RST file for this component + comp_rst = ctx.actions.declare_file(ctx.label.name + "/components/" + comp_name + ".rst") + + # Collect requirements files - requirements depset contains File objects + req_files = [] + req_refs = [] + if comp_info.requirements: + doc_files = _filter_doc_files(comp_info.requirements.to_list()) + + if doc_files: + # Find common directory + common_dir = _find_common_directory(doc_files) + + for f in doc_files: + relative_path = _compute_relative_path(f, common_dir) + output_file = _create_artifact_symlink( + ctx, + "components/" + comp_name + "_requirements", + f, + relative_path, + ) + req_files.append(output_file) + + if _is_document_file(f): + doc_ref = ("components/" + comp_name + "_requirements/" + relative_path) \ + .replace(".rst", "") \ + .replace(".md", "") + req_refs.append(" " + doc_ref) + + # Collect test target names + test_names = [] + if comp_info.tests: + for test in comp_info.tests.to_list(): + test_names.append(test.label) + + # Generate RST content using template + underline = "=" * (len("Component: " + comp_name)) + + # Generate sections from template constants + requirements_section = "" + if req_refs: + requirements_section = "\n" + _COMPONENT_REQUIREMENTS_SECTION_TEMPLATE.format( + requirements_refs = "\n".join(req_refs), + ) + "\n" + + units_section = "" + if unit_names: + unit_links = "\n".join(["- :doc:`../units/" + unit_name + "`" for unit_name in unit_names]) + units_section = "\n" + _COMPONENT_UNITS_SECTION_TEMPLATE.format( + unit_links = unit_links, + ) + "\n" + + tests_section = "" + if test_names: + test_list = "\n".join(["- ``" + str(test) + "``" for test in test_names]) + tests_section = "\n" + _TESTS_SECTION_TEMPLATE.format( + entity_type = "component", + test_list = test_list, + ) + "\n" + + # Generate component RST content from template constant + component_content = _COMPONENT_TEMPLATE.format( + component_name = comp_name, + underline = underline, + requirements_section = requirements_section, + units_section = units_section, + implementation_section = "", + tests_section = tests_section, + ) + + ctx.actions.write( + output = comp_rst, + content = component_content, + ) + + return (comp_rst, req_files) + # ============================================================================ # Index Generation Rule Implementation # ============================================================================ @@ -291,13 +584,45 @@ def _dependable_element_index_impl(ctx): output_files.extend(files) artifacts_by_type[artifact_name] = refs + # Collect all units recursively from components + all_units = _collect_units_recursive(ctx.attr.components) + + # Generate documentation for each unit + unit_refs = [] + for unit_name, unit_target in all_units.items(): + unit_rst, unit_files = _generate_unit_doc(ctx, unit_target, unit_name) + output_files.append(unit_rst) + output_files.extend(unit_files) + unit_refs.append(" units/" + unit_name) + + # Generate documentation for each component + component_refs = [] + for comp_target in ctx.attr.components: + if ComponentInfo in comp_target: + comp_info = comp_target[ComponentInfo] + comp_name = comp_info.name + + # Collect units that belong to this component + comp_unit_names = [] + for nested in comp_info.components.to_list(): + if UnitInfo in nested: + comp_unit_names.append(nested.label.name) + elif ComponentInfo in nested: + # For nested components, collect their units recursively + nested_units = _collect_units_recursive([nested]) + comp_unit_names.extend(nested_units.keys()) + + comp_rst, comp_files = _generate_component_doc(ctx, comp_target, comp_name, comp_unit_names) + output_files.append(comp_rst) + output_files.extend(comp_files) + component_refs.append(" components/" + comp_name) + # Process dependencies (submodules) deps_links = _process_deps(ctx) # Generate index file from template title = ctx.attr.module_name underline = "=" * len(title) - component_names = _get_component_names(ctx.attr.components) # Collect list of components ctx.actions.expand_template( template = ctx.file.template, @@ -306,7 +631,8 @@ def _dependable_element_index_impl(ctx): "{title}": title, "{underline}": underline, "{description}": ctx.attr.description, - "{components}": "\n- ".join(component_names), + "{units}": "\n".join(unit_refs) if unit_refs else " (none)", + "{components}": "\n".join(component_refs) if component_refs else " (none)", "{assumptions_of_use}": "\n ".join(artifacts_by_type["assumptions_of_use"]), "{component_requirements}": "\n ".join(artifacts_by_type["requirements"]), "{architectural_design}": "\n ".join(artifacts_by_type["architectural_design"]), @@ -352,6 +678,10 @@ _dependable_element_index = rule( default = [], doc = "Safety checklists targets or files.", ), + "tests": attr.label_list( + default = [], + doc = "Integration tests for the dependable element.", + ), "checklists": attr.label_list( default = [], doc = "Safety checklists targets or files.", @@ -441,6 +771,7 @@ def dependable_element( architectural_design = architectural_design, dependability_analysis = dependability_analysis, checklists = checklists, + tests = tests, deps = deps, testonly = testonly, visibility = ["//visibility:private"], diff --git a/bazel/rules/rules_score/private/safety_analysis.bzl b/bazel/rules/rules_score/private/safety_analysis.bzl index 89d4ab0..9614e25 100644 --- a/bazel/rules/rules_score/private/safety_analysis.bzl +++ b/bazel/rules/rules_score/private/safety_analysis.bzl @@ -26,7 +26,7 @@ load("//bazel/rules/rules_score/private:architectural_design.bzl", "Architectura # Provider Definition # ============================================================================ -SafetyAnalysisInfo = provider( +AnalysisInfo = provider( doc = "Provider for safety analysis artifacts", fields = { "controlmeasures": "Depset of control measures documentation or requirements", @@ -41,7 +41,7 @@ SafetyAnalysisInfo = provider( # Private Rule Implementation # ============================================================================ -def _safety_analysis_impl(ctx): +def _analysis_impl(ctx): """Implementation for safety_analysis rule. Collects safety analysis artifacts including control measures, failure modes, @@ -51,7 +51,7 @@ def _safety_analysis_impl(ctx): ctx: Rule context Returns: - List of providers including DefaultInfo and SafetyAnalysisInfo + List of providers including DefaultInfo and AnalysisInfo """ controlmeasures = depset(ctx.files.controlmeasures) failuremodes = depset(ctx.files.failuremodes) @@ -74,7 +74,7 @@ def _safety_analysis_impl(ctx): return [ DefaultInfo(files = all_files), - SafetyAnalysisInfo( + AnalysisInfo( controlmeasures = controlmeasures, failuremodes = failuremodes, fta = fta, @@ -91,8 +91,8 @@ def _safety_analysis_impl(ctx): # Rule Definition # ============================================================================ -_safety_analysis = rule( - implementation = _safety_analysis_impl, +_analysis = rule( + implementation = _analysis_impl, doc = "Collects safety analysis documents for S-CORE process compliance", attrs = { "controlmeasures": attr.label_list( @@ -152,7 +152,7 @@ def safety_analysis( visibility: Bazel visibility specification for the generated targets. Generated Targets: - : Main safety analysis target providing SafetyAnalysisInfo + : Main safety analysis target providing AnalysisInfo Example: ```starlark @@ -165,7 +165,7 @@ def safety_analysis( ) ``` """ - _safety_analysis( + _analysis( name = name, controlmeasures = controlmeasures, failuremodes = failuremodes, diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 1331f6e..e038df2 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -51,7 +51,6 @@ ComponentInfo = provider( fields = { "name": "Name of the component target", "requirements": "Depset of component requirements artifacts", - "implementation": "Depset of implementation targets (libraries, binaries)", "components": "Depset of unit targets that comprise this component", "tests": "Depset of component-level integration test targets", }, diff --git a/bazel/rules/rules_score/templates/seooc_index.template.rst b/bazel/rules/rules_score/templates/seooc_index.template.rst index c833b7b..5def2dc 100644 --- a/bazel/rules/rules_score/templates/seooc_index.template.rst +++ b/bazel/rules/rules_score/templates/seooc_index.template.rst @@ -16,6 +16,15 @@ Dependable element: {title} {description} +Architectural Design +-------------------- + +.. toctree:: + :maxdepth: 2 + + {architectural_design} + + Assumptions of Use ------------------ @@ -27,15 +36,22 @@ Assumptions of Use Components ---------- +.. toctree:: + :maxdepth: 1 + {components} -Architectural Design --------------------- + +Units +----- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + +{units} + + - {architectural_design} Dependability Analysis ---------------------- diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index f81b2cf..e1690cd 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -190,14 +190,24 @@ unit( ], ) +unit( + name = "test_binary_unit", + testonly = True, + tests = [":test_unit_tests"], + unit_design = [":arch_design"], + implementation = [":test_component_binary"], +) + # Test Component component( name = "test_component", testonly = True, - components = [":test_unit"], + components = [ + ":test_unit", + "test_binary_unit", + ], requirements = [":comp_req"], tests = [], # Empty for testing - implementation = [":test_component_binary"], ) # Test Dependable Element @@ -213,6 +223,68 @@ dependable_element( tests = [], # Empty for testing ) +# ============================================================================ +# Test Fixtures - Nested Components for Recursive Testing +# ============================================================================ + +# Additional mock implementations +cc_library( + name = "mock_lib3", + srcs = ["fixtures/mock_lib1.cc"], # Reuse same source for testing +) + +cc_test( + name = "test_unit2_tests", + testonly = True, + srcs = ["fixtures/test_unit_test.cc"], + tags = ["manual"], + deps = [":mock_lib3"], +) + +# Second unit that will be shared between components +unit( + name = "test_unit2", + testonly = True, + tests = [":test_unit2_tests"], + unit_design = [":arch_design"], + implementation = [":mock_lib3"], +) + +# Nested component containing unit2 +component( + name = "test_nested_component", + testonly = True, + components = [":test_unit2"], + requirements = [":comp_req"], + tests = [], +) + +# Parent component containing nested component and shared unit +component( + name = "test_parent_component", + testonly = True, + components = [ + ":test_nested_component", + ":test_unit2", # Same unit appears here and in nested component + ":test_unit", # Different unit + ], + requirements = [":comp_req"], + tests = [], +) + +# Dependable element with nested components to test recursive collection +dependable_element( + name = "test_dependable_element_nested", + testonly = True, + architectural_design = [":arch_design"], + assumptions_of_use = [":aous"], + components = [":test_parent_component"], + dependability_analysis = [":dependability_analysis_target"], + description = "Test dependable element with nested components for testing recursive unit collection and deduplication", + requirements = [":comp_req"], + tests = [], +) + # ============================================================================ # Test Instantiations - HTML Generation Tests # ============================================================================