diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index c67c552d..7386e46e 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -144,7 +144,7 @@ sphinx_docs_library( strip_prefix = "validation/ai_checker/", ) -# Validation specifications — two files, same source dir, no rename needed. +# Validation specifications — same source dir, no rename needed. sphinx_docs_library( name = "validation_specs", srcs = ["//validation/core:specifications"], diff --git a/bazel/rules/rules_score/docs/index.rst b/bazel/rules/rules_score/docs/index.rst index ddff5ac6..1cb717d4 100644 --- a/bazel/rules/rules_score/docs/index.rst +++ b/bazel/rules/rules_score/docs/index.rst @@ -38,7 +38,9 @@ safety analysis to the top-level SEooC assembly. :caption: Validation tool_reference/specs/bazel_component + tool_reference/specs/component_internal_api tool_reference/specs/component_sequence + tool_reference/specs/sequence_internal_api .. toctree:: :maxdepth: 2 diff --git a/validation/core/BUILD b/validation/core/BUILD index 395bf590..3d17159e 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -23,7 +23,9 @@ filegroup( name = "specifications", srcs = [ "docs/specifications/bazel_component.md", + "docs/specifications/component_internal_api.md", "docs/specifications/component_sequence.md", + "docs/specifications/sequence_internal_api.md", ], visibility = ["//visibility:public"], ) @@ -50,9 +52,17 @@ rust_library( "src/results/diagnostics.rs", "src/results/mod.rs", "src/validators/bazel_component_validator.rs", + "src/validators/component_internal_api_validator.rs", "src/validators/component_sequence_validator.rs", "src/validators/mod.rs", + "src/validators/sequence_internal_api_validator.rs", + "src/validators/shared/diagram_analysis.rs", + "src/validators/shared/helpers.rs", + "src/validators/shared/mod.rs", + "src/validators/test/component_internal_api_validator_test.rs", "src/validators/test/component_sequence_validator_test.rs", + "src/validators/test/fixtures.rs", + "src/validators/test/sequence_internal_api_validator_test.rs", ], crate_root = "src/lib.rs", visibility = ["//visibility:public"], diff --git a/validation/core/README.md b/validation/core/README.md index 36088d77..e1247b8b 100644 --- a/validation/core/README.md +++ b/validation/core/README.md @@ -29,16 +29,15 @@ The current implementation supports these validation flows: 1. `BazelComponent`: compares the indexed Bazel build graph with the indexed PlantUML component-diagram structure. - -2. `ComponentSequence`: checks that component-diagram unit aliases, shared +2. `ComponentInternalApi`: checks that every component-diagram interface is + declared by the Internal API diagram. +3. `ComponentSequence`: checks that component-diagram unit aliases, shared interface relations, and sequence-diagram function-call connections stay in - sync. When internal API diagrams are provided, it also checks that each - sequence function name is declared on a shared interface referenced by both - participating units. - - Internal API diagrams are handled separately from regular class diagrams. - If no `--internal-api-fbs` inputs are provided, `ComponentSequence` still runs - the alias and interface-connection checks and skips method-level validation. + sync. +4. `SequenceInternalApi`: checks that Internal API methods are exercised by + sequence interactions. When component input is also available, it uses that + component context to check sequence function names against related shared + interfaces. The CLI dispatches to the selected validation profile. Each profile owns its input schema, reads the models it needs, and runs the validators that are diff --git a/validation/core/docs/assets/validation_core_flow.puml b/validation/core/docs/assets/validation_core_flow.puml index 4b9b277a..20f12a3e 100644 --- a/validation/core/docs/assets/validation_core_flow.puml +++ b/validation/core/docs/assets/validation_core_flow.puml @@ -18,10 +18,12 @@ participant "CLI" as cli participant "BazelReader" as bazel_reader participant "ComponentDiagramReader" as component_reader participant "SequenceDiagramReader" as sequence_reader -participant "ClassDiagramReader" as class_reader +participant "ClassDiagramReader\n(class + internal API)" as class_reader participant "validate_bazel_component()" as bazel_validator participant "validate_component_sequence()" as sequence_validator participant "validate_component_class()" as class_validator +participant "validate_component_internal_api()" as component_internal_api_validator +participant "validate_sequence_internal_api()" as sequence_internal_api_validator participant "Errors" as errors participant "Report writer" as report_writer @@ -43,10 +45,16 @@ alt architectural-design profile cli -> cli: to_sequence_diagram_index(&mut errors) end - opt internal_api or public_api provided (planned) - cli -> class_reader: read(internal_api + public_api) + opt internal_api provided + cli -> class_reader: read(internal_api) class_reader --> cli: ClassDiagramInputs - cli -> cli: ClassDiagramIndex::build_index(&mut errors) + cli -> cli: InternalApiIndex::build_index(&mut errors) + end + + opt public_api provided (planned) + cli -> class_reader: read(public_api) + class_reader --> cli: ClassDiagramInputs + cli -> cli: PublicApiIndex::build_index(&mut errors) end opt component + class available (planned) @@ -61,6 +69,18 @@ alt architectural-design profile cli -> errors: merge_errors(...) end + opt component + internal API available + cli -> component_internal_api_validator: validate_component_internal_api(..., Errors::default()) + component_internal_api_validator --> cli: Errors + cli -> errors: merge_errors(...) + end + + opt sequence + internal API available + cli -> sequence_internal_api_validator: validate_sequence_internal_api(..., optional component, Errors::default()) + sequence_internal_api_validator --> cli: Errors + cli -> errors: merge_errors(...) + end + else dependable-element profile cli -> cli: read DependableElementInputs diff --git a/validation/core/docs/assets/validation_core_overview.puml b/validation/core/docs/assets/validation_core_overview.puml index 4b298337..450af6f3 100644 --- a/validation/core/docs/assets/validation_core_overview.puml +++ b/validation/core/docs/assets/validation_core_overview.puml @@ -46,14 +46,17 @@ package "validation/core" { component "BazelArchitecture\n(SEooC / component / unit sets)" as MBazel component "ComponentDiagramArchitecture\n(entities by stereotype)" as MComp component "SequenceDiagramIndex\n(used participant set)" as MSeq - component "ClassDiagramIndex\n(enclosing namespace set)" as MClass + component "ClassDiagramIndex (planned)\n(enclosing namespace set)" as MClass + component "InternalApiIndex\n(interface methods)" as MInternalApi } ' Validators: compare domain models and return accumulated findings package "Validator Layer" as ValidatorLayer { component "validate_bazel_component()" as VBC - component "validate_component_class()" as VCC + component "validate_component_class() (planned)" as VCC + component "validate_component_internal_api()" as VCIA component "validate_component_sequence()" as VCS + component "validate_sequence_internal_api()" as VSIA } ' Reporting: collect findings and write validation reports @@ -75,19 +78,27 @@ BR --> MBazel : converts CDR --> MComp : converts SDR --> MSeq : converts ClDR --> MClass : converts +ClDR --> MInternalApi : converts ' Validator inputs: consume only the domain models required by each rule MBazel --> VBC MComp --> VBC MComp --> VCC +MComp --> VCIA MComp --> VCS +MComp ..> VSIA : optional context MClass --> VCC MSeq --> VCS +MSeq --> VSIA +MInternalApi --> VCIA +MInternalApi --> VSIA ' Findings: merge reader and validator findings into one report VBC --> Err : findings -VCS --> Err : findings VCC --> Err : findings +VCIA --> Err : findings +VCS --> Err : findings +VSIA --> Err : findings ReaderLayer --> Err : parse/index findings Err --> ReportWriter : final status ReportWriter --> Report diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index 93dc6222..b92c3777 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -98,21 +98,50 @@ section "Tool Requirements" { satisfied_by = Verifier } - ToolQualification.ToolRequirement ComponentSequenceMethodNameConsistency { - description = '''When an internal API diagram is provided, the - validator shall report an error when a function used in a - sequence interaction is not declared in any shared interface - of the participating units as defined in the component - diagram.''' + } + + section "Sequence Internal API Validator" { + + ToolQualification.ToolRequirement ComponentSequenceInternalApiMethodNameConsistency { + description = '''When component, sequence, and internal API + diagrams are provided, the validator shall report an error + when a function used in a sequence interaction is not declared + in any shared interface of the participating units as defined + in the component diagram.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + ToolQualification.ToolRequirement ComponentSequenceInternalApiConsumerProviderRoleConsistency { + description = '''When component, sequence, and internal API + diagrams are provided, the validator shall report an error + when a cross-unit sequence interaction does not match + consumer/provider roles derived from the shared interfaces in + the component diagram: the caller shall require and the callee + shall provide an interface on which the called method is + declared.''' derived_from = [UseCases.Validate_Architecture_Specification_Documents] satisfied_by = Verifier } - ToolQualification.ToolRequirement ComponentSequenceInterfaceCoverage { - description = '''When an internal API diagram is provided, the - validator shall report an error when a function declared in a - validated interface is never called in any sequence - interaction. Self-calls count as valid usage.''' + ToolQualification.ToolRequirement ComponentSequenceInternalApiInterfaceCoverage { + description = '''When component, sequence, and internal API + diagrams are provided, the validator shall report an error + when a function declared in a validated interface is never + called in any sequence interaction. Self-calls count as valid + usage.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + } + + section "Component Internal API Validator" { + + ToolQualification.ToolRequirement ComponentInternalApiInterfaceDeclarationConsistency { + description = '''The validator shall report an error when an + interface declared in the component diagram is not declared as + an interface in the internal API diagram.''' derived_from = [UseCases.Validate_Architecture_Specification_Documents] satisfied_by = Verifier } diff --git a/validation/core/docs/specifications/component_internal_api.md b/validation/core/docs/specifications/component_internal_api.md new file mode 100644 index 00000000..de0fb7ca --- /dev/null +++ b/validation/core/docs/specifications/component_internal_api.md @@ -0,0 +1,63 @@ + + +# Component Internal API Specification + +## Purpose + +This validator enforces consistency between two diagram types: + +- **Component diagrams** +- **Internal API diagrams** + +It shall make sure that every interface declared by the component design is +also declared by the internal API design. + +## What is Validated + +All comparisons are case-sensitive. + +### Interface Declaration Consistency + +Every interface declared in the component diagram must resolve to an interface +declared in the internal API diagram. +*(Requirement: {requirement:downstream-ref}`Tools.ComponentInternalApiInterfaceDeclarationConsistency`)* + +```text +' component diagram +component "Unit 1" as unit_1 <> +interface "IData" as IData +unit_1 -( IData + +' internal_api diagram +interface "IData" as IData <> { + {abstract} GetData(): Data* +} +``` + +The component interface is matched against the internal API interface ID. The +match is exact and case-sensitive. This check applies even when a component +interface is not referenced by a unit relation. + +## Failure Cases + +| Failure case | Validation rule | +|---|---| +| Missing internal API interface | Interface Declaration Consistency | + +## Debug Output + +The validator emits debug output containing: + +- component interfaces checked against the internal API +- internal API interfaces available for component interfaces diff --git a/validation/core/docs/specifications/component_sequence.md b/validation/core/docs/specifications/component_sequence.md index dc6000ff..4ab39190 100644 --- a/validation/core/docs/specifications/component_sequence.md +++ b/validation/core/docs/specifications/component_sequence.md @@ -15,19 +15,16 @@ ## Purpose -This validator enforces consistency across entities in three diagram types: +This validator enforces consistency across entities in two diagram types: - **Component diagrams** - **Sequence diagrams** -- **Internal API diagrams** It shall make sure that Architectural Elements are consistently named and related to each other. ## What is Validated -All comparisons are case-sensitive. Alias and interface-connection checks -always run. Method-name and interface-coverage checks run only when an -internal API diagram is provided. +All comparisons are case-sensitive. ### Alias Consistency @@ -50,9 +47,8 @@ participant "Unit 2" as unit_2 Every pair of units connected through an interface in the component diagram must have at least one corresponding function-call interaction in the sequence diagrams, and every cross-unit function call in a sequence diagram must -correspond to an interface connection in the component diagram. The caller -shall be the consumer of the shared interface and the callee shall be the -provider. Self-calls are excluded from this check. +correspond to an interface connection in the component diagram. Self-calls are +excluded from this check. *(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInterfaceConnectionConsistency`)* ```text @@ -69,52 +65,6 @@ participant "Unit 2" as unit_2 unit_1 -> unit_2 : GetData() ``` -### Method-Name Consistency - -Every function used in a sequence interaction, including self-calls, must be -declared in a shared interface of the participating units as defined in the -component diagram. -*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceMethodNameConsistency`)* - -```text -' component diagram -component "Unit 1" as unit_1 <> -component "Unit 2" as unit_2 <> -interface "IData" as IData -unit_1 -( IData -unit_2 )- IData - -' sequence diagram -participant "Unit 1" as unit_1 -participant "Unit 2" as unit_2 -unit_1 -> unit_2 : GetData() - -' internal_api diagram -interface "IData" as IData <> { - {abstract} GetData(): Data* -} -``` - -### Interface Coverage - -Every function declared in a validated interface must be called in at least one -sequence interaction. Self-calls count as valid usage. -*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInterfaceCoverage`)* - -```text -' internal_api diagram -interface "IData" as IData <> { - {abstract} GetData(): Data* - {abstract} SetData(d: Data*): void -} - -' sequence diagram -participant "Unit 1" as unit_1 -participant "Unit 2" as unit_2 -unit_1 -> unit_2 : GetData() -unit_1 -> unit_2 : SetData(d) -``` - ## Failure Cases | Failure case | Validation rule | @@ -123,10 +73,6 @@ unit_1 -> unit_2 : SetData(d) | Unexpected sequence participant | Alias Consistency | | Missing sequence interaction for interface-connected units | Interface-Connection Consistency | | Missing interface connection for sequence-connected units | Interface-Connection Consistency | -| Invalid consumer/provider roles | Interface-Connection Consistency | -| Missing internal API interface | Method-Name Consistency | -| Method not declared in related interface | Method-Name Consistency | -| Interface function not exercised | Interface Coverage | ## Debug Output @@ -137,5 +83,3 @@ The validator emits debug output containing: - observed sequence calls (`caller -> callee : method`) - unit interface targets derived from the component diagram - interface-connected unit pairs derived from the component diagram -- internal API interfaces found and checked for method validation, when - internal API input is present diff --git a/validation/core/docs/specifications/sequence_internal_api.md b/validation/core/docs/specifications/sequence_internal_api.md new file mode 100644 index 00000000..0f5751e7 --- /dev/null +++ b/validation/core/docs/specifications/sequence_internal_api.md @@ -0,0 +1,131 @@ + + +# Sequence Internal API Specification + +## Purpose + +This validator enforces consistency between sequence diagrams and Internal API +diagrams: + +- **Sequence diagrams** +- **Internal API diagrams** + +It checks Internal API method coverage with sequence plus Internal API inputs. +When a **Component diagram** is also provided, the validator uses it as optional +context to check sequence method names against the related shared interfaces of +the participating units. + +## What is Validated + +All comparisons are case-sensitive. + +Method-name consistency and consumer/provider roles consistency are checked only +when component context is available. Without component context, the validator +does not run a weak global method-name existence check. + +### Method-Name Consistency + +Every function used in a sequence interaction must be declared in the related +Internal API interface context. + +For cross-unit calls, the method must be declared on a shared interface of the +participating units as defined in the component diagram. For self-calls, the +method must be declared on one of the available interfaces. +*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInternalApiMethodNameConsistency`)* + +```text +' component diagram +component "Unit 1" as unit_1 <> +component "Unit 2" as unit_2 <> +interface "IData" as IData +unit_1 -( IData +unit_2 )- IData + +' sequence diagram +participant "Unit 1" as unit_1 +participant "Unit 2" as unit_2 +unit_1 -> unit_2 : GetData() + +' internal_api diagram +interface "IData" as IData <> { + {abstract} GetData(): Data* +} +``` + +### Consumer/Provider Role Consistency + +When component context is available, cross-unit sequence calls must align with +consumer/provider roles derived from the component diagram for shared +interfaces. + +The caller shall require and the callee shall provide at least one shared +interface on which the called method is declared. Self-calls are excluded from +this check. +*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInternalApiConsumerProviderRoleConsistency`)* + +```text +' component diagram +component "Unit 1" as unit_1 <> +component "Unit 2" as unit_2 <> +interface "IData" as IData +unit_1 -( IData +unit_2 )- IData + +' sequence diagram +participant "Unit 1" as unit_1 +participant "Unit 2" as unit_2 +unit_1 -> unit_2 : GetData() + +' internal_api diagram +interface "IData" as IData <> { + {abstract} GetData(): Data* +} +``` + +### Interface Coverage + +Every function declared in an Internal API interface must be called in at least +one sequence interaction. Self-calls count as valid usage. +*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInternalApiInterfaceCoverage`)* + +```text +' internal_api diagram +interface "IData" as IData <> { + {abstract} GetData(): Data* + {abstract} SetData(d: Data*): void +} + +' sequence diagram +participant "Unit 1" as unit_1 +participant "Unit 2" as unit_2 +unit_1 -> unit_2 : GetData() +unit_1 -> unit_2 : SetData(d) +``` + +## Failure Cases + +| Failure case | Validation rule | +|---|---| +| Method not declared in related interface | Method-Name Consistency | +| Invalid consumer/provider roles | Consumer/Provider Role Consistency | +| Internal API interface function not exercised | Interface Coverage | + +## Debug Output + +The validator emits debug output containing: + +- observed sequence calls (`caller -> callee : method`) +- unit interface targets derived from the component diagram, when component + context is available +- Internal API interfaces available for sequence validation diff --git a/validation/core/integration_test/BUILD b/validation/core/integration_test/BUILD index 672347e9..f301388f 100644 --- a/validation/core/integration_test/BUILD +++ b/validation/core/integration_test/BUILD @@ -42,19 +42,33 @@ filegroup( filegroup( name = "component_sequence_test_data", srcs = [ - "//validation/core/integration_test/component_sequence/negative_interface_function_not_exercised:case_data", - "//validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction:case_data", - "//validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api:case_data", "//validation/core/integration_test/component_sequence/negative_missing_interface_connection_for_sequence_connected_units:case_data", - "//validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface:case_data", "//validation/core/integration_test/component_sequence/negative_missing_participant:case_data", "//validation/core/integration_test/component_sequence/negative_missing_sequence_interaction_for_interface_connected_units:case_data", "//validation/core/integration_test/component_sequence/negative_missing_unit_interface_relation:case_data", "//validation/core/integration_test/component_sequence/negative_mixed_mismatch:case_data", "//validation/core/integration_test/component_sequence/negative_orphan_participant:case_data", "//validation/core/integration_test/component_sequence/positive_exact_match:case_data", - "//validation/core/integration_test/component_sequence/positive_internal_api_method_match:case_data", - "//validation/core/integration_test/component_sequence/positive_self_call_method_match:case_data", + ], +) + +filegroup( + name = "component_internal_api_test_data", + srcs = [ + "//validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api:case_data", + "//validation/core/integration_test/component_internal_api/positive_interface_match:case_data", + ], +) + +filegroup( + name = "sequence_internal_api_test_data", + srcs = [ + "//validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised:case_data", + "//validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction:case_data", + "//validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface:case_data", + "//validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface:case_data", + "//validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match:case_data", + "//validation/core/integration_test/sequence_internal_api/positive_self_call_method_match:case_data", ], ) @@ -121,3 +135,35 @@ rust_test( "@crates//:serde_json", ], ) + +rust_test( + name = "component_internal_api_integration_test", + srcs = ["src/component_internal_api_suite.rs"], + crate_root = "src/component_internal_api_suite.rs", + data = [ + ":component_internal_api_test_data", + "//validation/core:validation_cli", + ], + deps = [ + ":test_framework", + "//validation/core:validation", + "@crates//:serde", + "@crates//:serde_json", + ], +) + +rust_test( + name = "sequence_internal_api_integration_test", + srcs = ["src/sequence_internal_api_suite.rs"], + crate_root = "src/sequence_internal_api_suite.rs", + data = [ + ":sequence_internal_api_test_data", + "//validation/core:validation_cli", + ], + deps = [ + ":test_framework", + "//validation/core:validation", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/validation/core/integration_test/README.md b/validation/core/integration_test/README.md index a4f6aa7c..8a15ba2d 100644 --- a/validation/core/integration_test/README.md +++ b/validation/core/integration_test/README.md @@ -23,12 +23,17 @@ binary → validation CLI — and asserts the outcome against a JSON fixture. integration_test/ ├── BUILD # test binaries + aggregated filegroups ├── puml_fixture.bzl # Starlark rule: provider → category dirs +├── component_sequence/ # ComponentSequence cases +├── component_internal_api/ # ComponentInternalApi cases +├── sequence_internal_api/ # SequenceInternalApi cases ├── src/ │ ├── lib.rs # re-exports from test_framework │ ├── test_framework.rs # shared helpers (CLI runner, assertions) │ ├── bazel_component_suite.rs # tests for BazelComponent validator │ ├── component_class_suite.rs # tests for ComponentClass validator -│ └── component_sequence_suite.rs # tests for ComponentSequence validator +│ ├── component_sequence_suite.rs # tests for ComponentSequence validator +│ ├── component_internal_api_suite.rs # tests for ComponentInternalApi validator +│ └── sequence_internal_api_suite.rs # tests for SequenceInternalApi validator ``` ## How it works @@ -69,9 +74,9 @@ A `filegroup` named `case_data` then bundles the `fbs` target together with the static fixture files (`architecture.json`, `expected.json`), making the whole case available as a single Bazel dependency. -For `ComponentSequence` cases that exercise method-level validation, the suite -also reads `internal_api/*.fbs.bin` and forwards those files to the CLI as -`--internal-api-fbs`. +For `ComponentInternalApi` and `SequenceInternalApi` suites, cases include +`internal_api/*.fbs.bin`, and the suite forwards those files to the CLI as +the `internal_api_diagrams` input bundle field. ### Layer 3 — CLI invocation (Rust test binary) @@ -124,12 +129,6 @@ bazel_component_integration_test PASS / FAIL ``` -`ComponentSequence` method-validation cases follow the same flow for -`internal_api_diagram.puml`: the `architectural_design` rule produces -`internal_api/*.fbs.bin`, `provider_fbs_fixture_bundle` materializes those -files under `fbs/internal_api/`, and the suite passes them to `validation_cli` -with `--internal-api-fbs`. - ## Test case anatomy Each test case is a self-contained directory. The exact files required depend on @@ -159,10 +158,30 @@ the validator under test. ``` / -├── BUILD # architectural_design (static + dynamic, plus optional internal_api) + provider_fbs_fixture_bundle + case_data +├── BUILD # architectural_design + provider_fbs_fixture_bundle + case_data +├── component_diagram.puml +├── sequence_diagram.puml +└── expected.json +``` + +### ComponentInternalApi cases + +``` +/ +├── BUILD # architectural_design + provider_fbs_fixture_bundle + case_data +├── component_diagram.puml +├── internal_api_diagram.puml +└── expected.json +``` + +### SequenceInternalApi cases + +``` +/ +├── BUILD # architectural_design + provider_fbs_fixture_bundle + case_data ├── component_diagram.puml ├── sequence_diagram.puml -├── internal_api_diagram.puml # optional; include when the case exercises method-level validation +├── internal_api_diagram.puml └── expected.json ``` @@ -198,6 +217,8 @@ Run a single suite: bazel test //validation/core/integration_test:bazel_component_integration_test bazel test //validation/core/integration_test:component_class_integration_test bazel test //validation/core/integration_test:component_sequence_integration_test +bazel test //validation/core/integration_test:component_internal_api_integration_test +bazel test //validation/core/integration_test:sequence_internal_api_integration_test ``` ## Adding a new test case @@ -217,8 +238,9 @@ bazel test //validation/core/integration_test:component_sequence_integration_tes suite. 5. Add the new `case_data` target to the matching filegroup in - [`BUILD`](BUILD) (`bazel_component_test_data`, - `component_class_test_data`, or `component_sequence_test_data`). + [`BUILD`](BUILD) (`bazel_component_test_data`, + `component_class_test_data`, `component_sequence_test_data`, + `component_internal_api_test_data`, or `sequence_internal_api_test_data`). 6. Add a `#[test]` function in the matching suite file under `src/`. diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/BUILD b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/BUILD new file mode 100644 index 00000000..b1a89f2e --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2026 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/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + internal_api = ["internal_api_diagram.puml"], + maturity = "development", + static = ["component_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.json", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/component_diagram.puml b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/component_diagram.puml rename to validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/component_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/expected.json b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/expected.json new file mode 100644 index 00000000..fcc1f30a --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/expected.json @@ -0,0 +1,7 @@ +{ + "should_pass": false, + "error_contains": [ + "Internal API consistency failure: Missing internal API interface", + "Missing interfaces : \"package_a.InternalInterface\"" + ] +} diff --git a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/internal_api_diagram.puml b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/internal_api_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/internal_api_diagram.puml rename to validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/internal_api_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_method_missing_from_internal_api/expected.json b/validation/core/integration_test/component_internal_api/negative_method_missing_from_internal_api/expected.json new file mode 100644 index 00000000..fcc1f30a --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_method_missing_from_internal_api/expected.json @@ -0,0 +1,7 @@ +{ + "should_pass": false, + "error_contains": [ + "Internal API consistency failure: Missing internal API interface", + "Missing interfaces : \"package_a.InternalInterface\"" + ] +} diff --git a/validation/core/integration_test/component_internal_api/positive_interface_match/BUILD b/validation/core/integration_test/component_internal_api/positive_interface_match/BUILD new file mode 100644 index 00000000..b7fa7290 --- /dev/null +++ b/validation/core/integration_test/component_internal_api/positive_interface_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 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/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + internal_api = ["internal_api_diagram.puml"], + static = ["component_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.json", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/component_diagram.puml b/validation/core/integration_test/component_internal_api/positive_interface_match/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/component_diagram.puml rename to validation/core/integration_test/component_internal_api/positive_interface_match/component_diagram.puml diff --git a/validation/core/integration_test/component_sequence/positive_internal_api_method_match/expected.json b/validation/core/integration_test/component_internal_api/positive_interface_match/expected.json similarity index 100% rename from validation/core/integration_test/component_sequence/positive_internal_api_method_match/expected.json rename to validation/core/integration_test/component_internal_api/positive_interface_match/expected.json diff --git a/validation/core/integration_test/component_sequence/positive_internal_api_method_match/internal_api_diagram.puml b/validation/core/integration_test/component_internal_api/positive_interface_match/internal_api_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_internal_api_method_match/internal_api_diagram.puml rename to validation/core/integration_test/component_internal_api/positive_interface_match/internal_api_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/expected.json b/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/expected.json deleted file mode 100644 index aa105900..00000000 --- a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/expected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "should_pass": false, - "error_contains": [ - "Method consistency failure: Missing internal API interface", - "\"package_a.InternalInterface\"" - ] -} diff --git a/validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/BUILD b/validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/BUILD similarity index 100% rename from validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/BUILD rename to validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/BUILD diff --git a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/component_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/component_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/expected.json b/validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/expected.json similarity index 100% rename from validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/expected.json rename to validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/expected.json diff --git a/validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/internal_api_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/internal_api_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/internal_api_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_interface_function_not_exercised/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_interface_function_not_exercised/sequence_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/BUILD b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/BUILD similarity index 100% rename from validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/BUILD rename to validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/BUILD diff --git a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/component_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/component_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/expected.json b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/expected.json similarity index 100% rename from validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/expected.json rename to validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/expected.json diff --git a/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/internal_api_diagram.puml new file mode 100644 index 00000000..30ff4338 --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/internal_api_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 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 +' ******************************************************************************* + +@startuml + +package package_a { + interface "InternalInterface" as InternalInterface { + + SendSignal() + } +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/sequence_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/BUILD b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/BUILD similarity index 100% rename from validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/BUILD rename to validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/BUILD diff --git a/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/component_diagram.puml new file mode 100644 index 00000000..ec6490a8 --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/component_diagram.puml @@ -0,0 +1,30 @@ +' ******************************************************************************* +' Copyright (c) 2026 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 +' ******************************************************************************* + +@startuml component_diagram + +package "Package A" as package_a { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + + interface "SharedInterface" as SharedInterface + interface "CallerOnlyInterface" as CallerOnlyInterface + + unit_1 -( SharedInterface + unit_2 )- SharedInterface + unit_1 -( CallerOnlyInterface +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/expected.json b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/expected.json similarity index 74% rename from validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/expected.json rename to validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/expected.json index 148acbd0..a9ee21be 100644 --- a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/expected.json +++ b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/expected.json @@ -3,7 +3,6 @@ "error_contains": [ "Method consistency failure", "sequence function name was not found in the related interface methods", - "\"GetData\"", - "\"package_a.InternalInterface\"" + "\"GetData\"" ] } diff --git a/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/internal_api_diagram.puml new file mode 100644 index 00000000..d2d9fe4f --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/internal_api_diagram.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' Copyright (c) 2026 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 +' ******************************************************************************* + +@startuml + +package package_a { + interface "SharedInterface" as SharedInterface { + + OtherMethod() + } + + interface "CallerOnlyInterface" as CallerOnlyInterface { + + GetData() + } +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/sequence_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/BUILD b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/BUILD similarity index 95% rename from validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/BUILD rename to validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/BUILD index 68419fff..02461e67 100644 --- a/validation/core/integration_test/component_sequence/negative_invalid_consumer_provider_direction/BUILD +++ b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/BUILD @@ -17,6 +17,7 @@ load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixtur architectural_design( name = "design", dynamic = ["sequence_diagram.puml"], + internal_api = ["internal_api_diagram.puml"], maturity = "development", static = ["component_diagram.puml"], visibility = ["//visibility:private"], diff --git a/validation/core/integration_test/component_sequence/positive_internal_api_method_match/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_internal_api_method_match/component_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/component_diagram.puml diff --git a/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/expected.json b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/expected.json new file mode 100644 index 00000000..737cce5d --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/expected.json @@ -0,0 +1,9 @@ +{ + "should_pass": false, + "error_contains": [ + "Method consistency failure", + "sequence function name was not found in available interface methods", + "\"GetData\"", + "\"package_a.InternalInterface\"" + ] +} diff --git a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/internal_api_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/internal_api_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/internal_api_diagram.puml diff --git a/validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/negative_missing_method_in_related_interface/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/sequence_diagram.puml diff --git a/validation/core/integration_test/component_sequence/positive_internal_api_method_match/BUILD b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/BUILD similarity index 100% rename from validation/core/integration_test/component_sequence/positive_internal_api_method_match/BUILD rename to validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/BUILD diff --git a/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/component_diagram.puml new file mode 100644 index 00000000..c87543a4 --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/component_diagram.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' Copyright (c) 2026 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 +' ******************************************************************************* + +@startuml component_diagram + +package "Package A" as package_a { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + + interface "InternalInterface" as InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/positive_self_call_method_match/expected.json b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/expected.json similarity index 100% rename from validation/core/integration_test/component_sequence/positive_self_call_method_match/expected.json rename to validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/expected.json diff --git a/validation/core/integration_test/component_sequence/positive_self_call_method_match/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/internal_api_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_self_call_method_match/internal_api_diagram.puml rename to validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/internal_api_diagram.puml diff --git a/validation/core/integration_test/component_sequence/positive_internal_api_method_match/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_internal_api_method_match/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/sequence_diagram.puml diff --git a/validation/core/integration_test/component_sequence/positive_self_call_method_match/BUILD b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/BUILD similarity index 100% rename from validation/core/integration_test/component_sequence/positive_self_call_method_match/BUILD rename to validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/BUILD diff --git a/validation/core/integration_test/component_sequence/positive_self_call_method_match/component_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_self_call_method_match/component_diagram.puml rename to validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/component_diagram.puml diff --git a/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/expected.json b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/expected.json new file mode 100644 index 00000000..208a55e1 --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/expected.json @@ -0,0 +1,4 @@ +{ + "should_pass": true, + "error_contains": [] +} diff --git a/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/internal_api_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/internal_api_diagram.puml new file mode 100644 index 00000000..d1cf74ec --- /dev/null +++ b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/internal_api_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 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 +' ******************************************************************************* + +@startuml + +package package_a { + interface "InternalInterface" as InternalInterface { + + GetData() + } +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/positive_self_call_method_match/sequence_diagram.puml b/validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/sequence_diagram.puml similarity index 100% rename from validation/core/integration_test/component_sequence/positive_self_call_method_match/sequence_diagram.puml rename to validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/sequence_diagram.puml diff --git a/validation/core/integration_test/src/component_internal_api_suite.rs b/validation/core/integration_test/src/component_internal_api_suite.rs new file mode 100644 index 00000000..47ef38d6 --- /dev/null +++ b/validation/core/integration_test/src/component_internal_api_suite.rs @@ -0,0 +1,60 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use test_framework::{ + assert_cli_result, collect_case_fbs_files, load_expected_fixture, run_validation_profile, + CliRunResult, +}; + +const SUITE_DIR: &str = "component_internal_api"; + +fn run_case_from_cli( + case_dir: &str, + component_fbs_paths: &[String], + internal_api_fbs_paths: &[String], +) -> CliRunResult { + run_validation_profile( + &format!("component_internal_api_{case_dir}"), + "architectural-design", + serde_json::json!({ + "component_diagrams": component_fbs_paths, + "internal_api_diagrams": internal_api_fbs_paths, + }), + ) +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let internal_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "internal_api"); + + let result = if !component_fbs_paths.is_empty() && !internal_api_fbs_paths.is_empty() { + run_case_from_cli(case_dir, &component_fbs_paths, &internal_api_fbs_paths) + } else { + panic!( + "missing generated FBS fixtures for {case_dir}: expected component/*.fbs.bin and internal_api/*.fbs.bin", + ); + }; + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn negative_interface_missing_from_internal_api_suite_case() { + assert_case("negative_interface_missing_from_internal_api"); +} + +#[test] +fn positive_interface_match_suite_case() { + assert_case("positive_interface_match"); +} diff --git a/validation/core/integration_test/src/component_sequence_suite.rs b/validation/core/integration_test/src/component_sequence_suite.rs index ea07a254..6676b265 100644 --- a/validation/core/integration_test/src/component_sequence_suite.rs +++ b/validation/core/integration_test/src/component_sequence_suite.rs @@ -22,7 +22,6 @@ fn run_case_from_cli( case_dir: &str, component_fbs_paths: &[String], sequence_fbs_paths: &[String], - internal_api_fbs_paths: &[String], ) -> CliRunResult { run_validation_profile( &format!("component_sequence_{case_dir}"), @@ -30,7 +29,6 @@ fn run_case_from_cli( serde_json::json!({ "component_diagrams": component_fbs_paths, "sequence_diagrams": sequence_fbs_paths, - "internal_api_diagrams": internal_api_fbs_paths, }), ) } @@ -38,16 +36,10 @@ fn run_case_from_cli( fn assert_case(case_dir: &str) { let expected = load_expected_fixture(SUITE_DIR, case_dir); let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); - let internal_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "internal_api"); let sequence_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "sequence"); let result = if !component_fbs_paths.is_empty() && !sequence_fbs_paths.is_empty() { - run_case_from_cli( - case_dir, - &component_fbs_paths, - &sequence_fbs_paths, - &internal_api_fbs_paths, - ) + run_case_from_cli(case_dir, &component_fbs_paths, &sequence_fbs_paths) } else { panic!( "missing generated FBS fixtures for {case_dir}: expected at least one component/*.fbs.bin and sequence/*.fbs.bin", @@ -62,31 +54,11 @@ fn positive_exact_match_suite_case() { assert_case("positive_exact_match"); } -#[test] -fn negative_invalid_consumer_provider_direction_suite_case() { - assert_case("negative_invalid_consumer_provider_direction"); -} - -#[test] -fn negative_interface_function_not_exercised_suite_case() { - assert_case("negative_interface_function_not_exercised"); -} - #[test] fn negative_missing_participant_suite_case() { assert_case("negative_missing_participant"); } -#[test] -fn negative_method_missing_from_internal_api_suite_case() { - assert_case("negative_method_missing_from_internal_api"); -} - -#[test] -fn negative_missing_method_in_related_interface_suite_case() { - assert_case("negative_missing_method_in_related_interface"); -} - #[test] fn negative_missing_interface_connection_for_sequence_connected_units_suite_case() { assert_case("negative_missing_interface_connection_for_sequence_connected_units"); @@ -111,13 +83,3 @@ fn negative_orphan_participant_suite_case() { fn negative_mixed_mismatch_suite_case() { assert_case("negative_mixed_mismatch"); } - -#[test] -fn positive_internal_api_method_match_suite_case() { - assert_case("positive_internal_api_method_match"); -} - -#[test] -fn positive_self_call_method_match_suite_case() { - assert_case("positive_self_call_method_match"); -} diff --git a/validation/core/integration_test/src/sequence_internal_api_suite.rs b/validation/core/integration_test/src/sequence_internal_api_suite.rs new file mode 100644 index 00000000..097bc86f --- /dev/null +++ b/validation/core/integration_test/src/sequence_internal_api_suite.rs @@ -0,0 +1,88 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use test_framework::{ + assert_cli_result, collect_case_fbs_files, load_expected_fixture, run_validation_profile, + CliRunResult, +}; + +const SUITE_DIR: &str = "sequence_internal_api"; + +fn run_case_from_cli( + case_dir: &str, + component_fbs_paths: &[String], + sequence_fbs_paths: &[String], + internal_api_fbs_paths: &[String], +) -> CliRunResult { + run_validation_profile( + &format!("sequence_internal_api_{case_dir}"), + "architectural-design", + serde_json::json!({ + "component_diagrams": component_fbs_paths, + "sequence_diagrams": sequence_fbs_paths, + "internal_api_diagrams": internal_api_fbs_paths, + }), + ) +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let internal_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "internal_api"); + let sequence_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "sequence"); + + let result = if !sequence_fbs_paths.is_empty() && !internal_api_fbs_paths.is_empty() { + run_case_from_cli( + case_dir, + &component_fbs_paths, + &sequence_fbs_paths, + &internal_api_fbs_paths, + ) + } else { + panic!( + "missing generated FBS fixtures for {case_dir}: expected sequence/*.fbs.bin and internal_api/*.fbs.bin", + ); + }; + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn negative_interface_function_not_exercised_suite_case() { + assert_case("negative_interface_function_not_exercised"); +} + +#[test] +fn negative_invalid_consumer_provider_direction_suite_case() { + assert_case("negative_invalid_consumer_provider_direction"); +} + +#[test] +fn negative_missing_method_in_related_interface_suite_case() { + assert_case("negative_missing_method_in_related_interface"); +} + +#[test] +fn negative_method_available_but_not_on_related_interface_suite_case() { + assert_case("negative_method_available_but_not_on_related_interface"); +} + +#[test] +fn positive_internal_api_method_match_suite_case() { + assert_case("positive_internal_api_method_match"); +} + +#[test] +fn positive_self_call_method_match_suite_case() { + assert_case("positive_self_call_method_match"); +} diff --git a/validation/core/src/models/bazel_models.rs b/validation/core/src/models/bazel_models.rs index 071ad243..61723cb9 100644 --- a/validation/core/src/models/bazel_models.rs +++ b/validation/core/src/models/bazel_models.rs @@ -142,3 +142,42 @@ pub struct BazelArchitecture { /// Nested units (`<>`), keyed with the enclosing component alias. pub unit_set: BTreeMap, } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_arch(entries: Vec<(&str, Vec<&str>, Vec<&str>)>) -> BazelInput { + let mut components = BTreeMap::new(); + for (label, units, nested) in entries { + components.insert( + label.to_string(), + BazelInputEntry { + units: units.into_iter().map(str::to_string).collect(), + components: nested.into_iter().map(str::to_string).collect(), + }, + ); + } + BazelInput { components } + } + + #[test] + fn reports_duplicate_dependable_element_key() { + let arch = make_arch(vec![ + ("@//pkg1:comp_a", vec![], vec![]), + ("@//pkg2:comp_a", vec![], vec![]), + ]); + + let mut setup_result = ValidationResult::default(); + let _architecture = arch.to_bazel_architecture(&mut setup_result); + + assert!( + setup_result + .failures + .iter() + .any(|message| message.contains("Duplicate dependable element key")), + "Expected duplicate dependable element key error, got: {:?}", + setup_result.failures + ); + } +} diff --git a/validation/core/src/models/component_diagram_models.rs b/validation/core/src/models/component_diagram_models.rs index 3664c2df..6151f6b3 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -15,57 +15,43 @@ use std::collections::BTreeMap; use super::EntityKey; use crate::ValidationResult; +pub use component_diagram::{ + ComponentRelationType, ComponentType, EndpointRole, LogicComponent, LogicRelation, +}; -/// Supported component-diagram entity kinds needed for validation. -#[derive(Clone, PartialEq)] -pub enum ComponentDiagramElementType { - Component, - Package, - Interface, -} +/// Validation-specific helpers for component metamodel entities. +pub trait LogicComponentExt { + /// Canonical match key: alias (lowercased) when present, otherwise raw id. + fn match_key(&self) -> String; -/// One relation attached to a component-diagram entity. -#[derive(Clone)] -pub struct ComponentDiagramRelation { - pub target: String, - #[allow(dead_code)] - pub annotation: Option, - #[allow(dead_code)] - pub relation_type: Option, - pub source_role: Option, -} + fn is_component(&self) -> bool; + + fn is_unit(&self) -> bool; -/// A single component-level entity parsed from a PlantUML `.fbs.bin` file. -#[derive(Clone)] -pub struct ComponentDiagramInput { - pub id: String, - pub alias: Option, - pub parent_id: Option, - pub element_type: ComponentDiagramElementType, - pub stereotype: Option, - pub relations: Vec, + fn is_interface(&self) -> bool; + + /// Returns `true` for `<>` package entities (dependable elements). + fn is_seooc_package(&self) -> bool; } -impl ComponentDiagramInput { - /// Canonical match key: alias (lowercased) when present, otherwise raw id. - pub fn match_key(&self) -> String { +impl LogicComponentExt for LogicComponent { + fn match_key(&self) -> String { self.alias.as_deref().unwrap_or(&self.id).to_lowercase() } - pub fn is_component(&self) -> bool { + fn is_component(&self) -> bool { self.stereotype.as_deref() == Some("component") } - pub fn is_unit(&self) -> bool { + fn is_unit(&self) -> bool { self.stereotype.as_deref() == Some("unit") } - pub fn is_interface(&self) -> bool { - self.element_type == ComponentDiagramElementType::Interface + fn is_interface(&self) -> bool { + self.element_type == ComponentType::Interface } - /// Returns `true` for `<>` package entities (dependable elements). - pub fn is_seooc_package(&self) -> bool { + fn is_seooc_package(&self) -> bool { self.stereotype.as_deref() == Some("SEooC") } } @@ -75,7 +61,7 @@ impl ComponentDiagramInput { /// Symmetric peer of [`BazelInput`]: produced by [`ComponentDiagramReader`] and /// consumed by [`to_diagram_architecture`](ComponentDiagramInputs::to_diagram_architecture). pub struct ComponentDiagramInputs { - pub entities: Vec, + pub entities: Vec, } impl ComponentDiagramInputs { @@ -93,12 +79,12 @@ impl ComponentDiagramInputs { /// Built via [`ComponentDiagramInputs::to_diagram_architecture`]. pub struct ComponentDiagramArchitecture { /// `<>` package entities, keyed with `parent = None`. - pub seooc_set: BTreeMap, + pub seooc_set: BTreeMap, /// `<>` entities, keyed with `parent = Some(..)`. - pub comp_set: BTreeMap, - pub unit_set: BTreeMap, + pub comp_set: BTreeMap, + pub unit_set: BTreeMap, /// Full raw entity list, kept for debug output. - pub entities: Vec, + pub entities: Vec, pub filtered_seooc_count: usize, pub filtered_component_count: usize, pub filtered_unit_count: usize, @@ -111,10 +97,10 @@ impl ComponentDiagramArchitecture { /// `<>` go into `comp_set`; /// `<>` go into `unit_set`. /// Duplicates (same [`EntityKey`]) are reported via `result`. - fn from_entities(entities: &[ComponentDiagramInput], result: &mut ValidationResult) -> Self { + fn from_entities(entities: &[LogicComponent], result: &mut ValidationResult) -> Self { // Index by raw id for parent resolution; PlantUML nesting uses id, // not alias. - let mut id_index: BTreeMap = BTreeMap::new(); + let mut id_index: BTreeMap = BTreeMap::new(); for entity in entities { let key = entity.id.to_lowercase(); if let Some(prev) = id_index.insert(key.clone(), entity) { @@ -127,15 +113,15 @@ impl ComponentDiagramArchitecture { } } - let seoocs: Vec<&ComponentDiagramInput> = entities + let seoocs: Vec<&LogicComponent> = entities .iter() .filter(|entity| entity.is_seooc_package()) .collect(); - let components: Vec<&ComponentDiagramInput> = entities + let components: Vec<&LogicComponent> = entities .iter() .filter(|entity| entity.is_component()) .collect(); - let units: Vec<&ComponentDiagramInput> = + let units: Vec<&LogicComponent> = entities.iter().filter(|entity| entity.is_unit()).collect(); let filtered_seooc_count = seoocs.len(); @@ -158,10 +144,10 @@ impl ComponentDiagramArchitecture { } fn build_set( - items: &[&ComponentDiagramInput], - id_index: &BTreeMap, + items: &[&LogicComponent], + id_index: &BTreeMap, result: &mut ValidationResult, - ) -> BTreeMap { + ) -> BTreeMap { let mut set = BTreeMap::new(); for entity in items { let alias = entity.match_key(); @@ -199,12 +185,12 @@ impl ComponentDiagramArchitecture { mod tests { use super::*; - fn relation(target: &str) -> ComponentDiagramRelation { - ComponentDiagramRelation { + fn relation(target: &str) -> LogicRelation { + LogicRelation { target: target.to_string(), annotation: None, - relation_type: Some("None".to_string()), - source_role: Some("None".to_string()), + relation_type: ComponentRelationType::Association, + source_role: EndpointRole::None, } } @@ -212,12 +198,13 @@ mod tests { id: &str, alias: Option<&str>, parent_id: Option<&str>, - element_type: ComponentDiagramElementType, + element_type: ComponentType, stereotype: Option<&str>, - relations: Vec, - ) -> ComponentDiagramInput { - ComponentDiagramInput { + relations: Vec, + ) -> LogicComponent { + LogicComponent { id: id.to_string(), + name: alias.map(str::to_string), alias: alias.map(str::to_string), parent_id: parent_id.map(str::to_string), element_type, @@ -234,7 +221,7 @@ mod tests { "safety_software_seooc_example", Some("safety_software_seooc_example"), None, - ComponentDiagramElementType::Package, + ComponentType::Package, Some("SEooC"), Vec::new(), ), @@ -242,7 +229,7 @@ mod tests { "safety_software_seooc_example.component_example", Some("component_example"), Some("safety_software_seooc_example"), - ComponentDiagramElementType::Component, + ComponentType::Component, Some("component"), Vec::new(), ), @@ -250,7 +237,7 @@ mod tests { "safety_software_seooc_example.InternalInterface", Some("InternalInterface"), Some("safety_software_seooc_example"), - ComponentDiagramElementType::Interface, + ComponentType::Interface, None, Vec::new(), ), @@ -258,7 +245,7 @@ mod tests { "safety_software_seooc_example.component_example.unit_1", Some("unit_1"), Some("safety_software_seooc_example.component_example"), - ComponentDiagramElementType::Component, + ComponentType::Component, Some("unit"), vec![relation("safety_software_seooc_example.InternalInterface")], ), @@ -288,4 +275,76 @@ mod tests { "safety_software_seooc_example.InternalInterface" ); } + + #[test] + fn reports_duplicate_entity_id() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity( + "MyDE", + Some("my_de"), + None, + ComponentType::Package, + Some("SEooC"), + Vec::new(), + ), + entity( + "myDE", + Some("other_alias"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + ), + ], + }; + + let mut set_result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut set_result); + + assert!( + set_result + .failures + .iter() + .any(|message| message.contains("Duplicate entity ID")), + "Expected duplicate ID error, got: {:?}", + set_result.failures + ); + } + + #[test] + fn reports_unresolved_parent_id() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity( + "MyDE", + Some("my_de"), + None, + ComponentType::Package, + Some("SEooC"), + Vec::new(), + ), + entity( + "CompA", + Some("comp_a"), + Some("NonExistent"), + ComponentType::Component, + Some("component"), + Vec::new(), + ), + ], + }; + + let mut set_result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut set_result); + + assert!( + set_result + .failures + .iter() + .any(|message| message.contains("Unresolved parent_id")), + "Expected unresolved parent error, got: {:?}", + set_result.failures + ); + } } diff --git a/validation/core/src/models/mod.rs b/validation/core/src/models/mod.rs index 583d824d..a3ceac5a 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -26,9 +26,9 @@ pub use bazel_models::BazelInputEntry; pub use bazel_models::{BazelArchitecture, BazelInput}; pub use class_diagram_models::{ClassDiagramInputs, InternalApiIndex, InternalApiInterface}; pub use component_diagram_models::{ - ComponentDiagramArchitecture, ComponentDiagramElementType, ComponentDiagramInput, - ComponentDiagramInputs, ComponentDiagramRelation, + ComponentDiagramArchitecture, ComponentDiagramInputs, ComponentRelationType, ComponentType, + EndpointRole, LogicComponent, LogicComponentExt, LogicRelation, }; pub use sequence_diagram_models::{ - ObservedSequenceCall, SequenceDiagramIndex, SequenceDiagramInput, SequenceDiagramInputs, + ObservedSequenceCall, SequenceDiagramIndex, SequenceDiagramInputs, }; diff --git a/validation/core/src/models/sequence_diagram_models.rs b/validation/core/src/models/sequence_diagram_models.rs index 0a36d9f0..9e2321ca 100644 --- a/validation/core/src/models/sequence_diagram_models.rs +++ b/validation/core/src/models/sequence_diagram_models.rs @@ -19,18 +19,9 @@ use sequence_logic::{Event, SequenceNode, SequenceTree}; use crate::ValidationResult; -/// One parsed sequence diagram from a FlatBuffer file. -pub struct SequenceDiagramInput { - pub tree: SequenceTree, - #[allow(dead_code)] - pub source_files: Vec, - #[allow(dead_code)] - pub version: Option, -} - /// Collection of sequence diagrams loaded from one or more FlatBuffer files. pub struct SequenceDiagramInputs { - pub diagrams: Vec, + pub diagrams: Vec, } /// One function-call interaction observed in a sequence diagram. @@ -54,12 +45,12 @@ pub struct SequenceDiagramIndex { } impl SequenceDiagramIndex { - fn from_diagrams(diagrams: &[SequenceDiagramInput], result: &mut ValidationResult) -> Self { + fn from_diagrams(diagrams: &[SequenceTree], result: &mut ValidationResult) -> Self { let mut used_participants = BTreeSet::new(); let mut observed_calls = Vec::new(); for diagram in diagrams { - for node in &diagram.tree.root_interactions { + for node in &diagram.root_interactions { collect_sequence_data(node, &mut used_participants, &mut observed_calls, result); } } @@ -201,21 +192,17 @@ mod tests { #[test] fn sequence_index_collects_calls_and_used_participants_recursively() { let inputs = SequenceDiagramInputs { - diagrams: vec![SequenceDiagramInput { - tree: SequenceTree { - name: Some("seq".to_string()), - root_interactions: vec![interaction( - "unit_1", - "unit_2", - "GetData()", - vec![ - ret("unit_1", "unit_2"), - interaction("unit_2", "unit_3", "Forward()", Vec::new()), - ], - )], - }, - source_files: Vec::new(), - version: None, + diagrams: vec![SequenceTree { + name: Some("seq".to_string()), + root_interactions: vec![interaction( + "unit_1", + "unit_2", + "GetData()", + vec![ + ret("unit_1", "unit_2"), + interaction("unit_2", "unit_3", "Forward()", Vec::new()), + ], + )], }], }; @@ -243,13 +230,9 @@ mod tests { #[test] fn sequence_index_reports_interaction_with_missing_required_endpoints() { let inputs = SequenceDiagramInputs { - diagrams: vec![SequenceDiagramInput { - tree: SequenceTree { - name: Some("seq".to_string()), - root_interactions: vec![interaction("", "unit_2", "GetData()", Vec::new())], - }, - source_files: Vec::new(), - version: None, + diagrams: vec![SequenceTree { + name: Some("seq".to_string()), + root_interactions: vec![interaction("", "unit_2", "GetData()", Vec::new())], }], }; @@ -266,13 +249,9 @@ mod tests { #[test] fn sequence_index_reports_interaction_with_missing_callee() { let inputs = SequenceDiagramInputs { - diagrams: vec![SequenceDiagramInput { - tree: SequenceTree { - name: Some("seq".to_string()), - root_interactions: vec![interaction("unit_1", "", "GetData()", Vec::new())], - }, - source_files: Vec::new(), - version: None, + diagrams: vec![SequenceTree { + name: Some("seq".to_string()), + root_interactions: vec![interaction("unit_1", "", "GetData()", Vec::new())], }], }; diff --git a/validation/core/src/profiles/architectural_design.rs b/validation/core/src/profiles/architectural_design.rs index 046554e2..3275e677 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -16,12 +16,16 @@ use crate::models::{ SequenceDiagramIndex, SequenceDiagramInputs, }; use crate::readers::{ClassDiagramReader, ComponentDiagramReader, SequenceDiagramReader}; -use crate::validators::validate_component_sequence; +use crate::validators::{ + validate_component_internal_api, validate_component_sequence, validate_sequence_internal_api, +}; use crate::ValidationResult; use serde::Deserialize; use super::profile::{merge_results, read_and_convert, ProfileRun}; +type ProfileValidator<'a> = Box Option + 'a>; + #[derive(Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct ArchitecturalDesignInputs { @@ -31,6 +35,31 @@ pub struct ArchitecturalDesignInputs { public_api_diagrams: Vec, } +fn registered_validators<'a>( + component: &'a Option, + sequence: &'a Option, + internal_api: &'a Option, +) -> Vec> { + vec![ + Box::new(move || { + let (component, sequence) = (component.as_ref()?, sequence.as_ref()?); + Some(validate_component_sequence(component, sequence)) + }), + Box::new(move || { + let (component, internal_api) = (component.as_ref()?, internal_api.as_ref()?); + Some(validate_component_internal_api(component, internal_api)) + }), + Box::new(move || { + let (sequence, internal_api) = (sequence.as_ref()?, internal_api.as_ref()?); + Some(validate_sequence_internal_api( + sequence, + internal_api, + component.as_ref(), + )) + }), + ] +} + pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { let mut result = ValidationResult::default(); let component = read_and_convert::( @@ -49,13 +78,14 @@ pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { |raw: ClassDiagramInputs, errs| InternalApiIndex::build_index(&raw, errs), )?; + let validators = registered_validators(&component, &sequence, &internal_api); + let mut ran_validator = false; - if let (Some(component), Some(sequence)) = (component.as_ref(), sequence.as_ref()) { - merge_results( - &mut result, - validate_component_sequence(component, sequence, internal_api.as_ref()), - ); - ran_validator = true; + for validator in validators { + if let Some(validator_result) = validator() { + merge_results(&mut result, validator_result); + ran_validator = true; + } } Ok(ProfileRun { diff --git a/validation/core/src/readers/component_diagram_reader.rs b/validation/core/src/readers/component_diagram_reader.rs index 1f47acff..be94d93a 100644 --- a/validation/core/src/readers/component_diagram_reader.rs +++ b/validation/core/src/readers/component_diagram_reader.rs @@ -19,26 +19,46 @@ use std::fs; use component_fbs::component as fb_component; use crate::models::{ - ComponentDiagramElementType, ComponentDiagramInput, ComponentDiagramInputs, - ComponentDiagramRelation, + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, + LogicRelation, }; use crate::readers::Reader; pub struct ComponentDiagramReader; -fn map_element_type(value: fb_component::ComponentType) -> Option { +fn map_element_type(value: fb_component::ComponentType) -> Option { match value { - fb_component::ComponentType::Component => Some(ComponentDiagramElementType::Component), - fb_component::ComponentType::Package => Some(ComponentDiagramElementType::Package), - fb_component::ComponentType::Interface => Some(ComponentDiagramElementType::Interface), + fb_component::ComponentType::Component => Some(ComponentType::Component), + fb_component::ComponentType::Package => Some(ComponentType::Package), + fb_component::ComponentType::Interface => Some(ComponentType::Interface), _ => None, } } +fn map_relation_type(value: fb_component::ComponentRelationType) -> ComponentRelationType { + match value { + fb_component::ComponentRelationType::Association => ComponentRelationType::Association, + fb_component::ComponentRelationType::Dependency => ComponentRelationType::Dependency, + fb_component::ComponentRelationType::InterfaceBinding => { + ComponentRelationType::InterfaceBinding + } + _ => ComponentRelationType::Association, + } +} + +fn map_endpoint_role(value: fb_component::EndpointRole) -> EndpointRole { + match value { + fb_component::EndpointRole::None => EndpointRole::None, + fb_component::EndpointRole::Provided => EndpointRole::Provided, + fb_component::EndpointRole::Required => EndpointRole::Required, + _ => EndpointRole::None, + } +} + fn read_relations( component: &fb_component::LogicComponent<'_>, context: &str, -) -> Result, String> { +) -> Result, String> { component .relations() .map(|relations| { @@ -49,17 +69,11 @@ fn read_relations( .target() .ok_or_else(|| format!("Component relation missing target in {context}"))?; - Ok(ComponentDiagramRelation { + Ok(LogicRelation { target: target.to_string(), annotation: relation.annotation().map(|value| value.to_string()), - relation_type: relation - .relation_type() - .variant_name() - .map(|value| value.to_string()), - source_role: relation - .source_role() - .variant_name() - .map(|value| value.to_string()), + relation_type: map_relation_type(relation.relation_type()), + source_role: map_endpoint_role(relation.source_role()), }) }) .collect::, String>>() @@ -104,8 +118,9 @@ impl ComponentDiagramReader { if let Some(element_type) = map_element_type(comp.comp_type()) { let context = format!("{path}:component:{}", comp.id().unwrap_or_default()); - out.push(ComponentDiagramInput { + out.push(LogicComponent { id: comp.id().unwrap_or_default().to_string(), + name: comp.name().map(|s| s.to_string()), alias: comp.alias().map(|s| s.to_string()), parent_id: comp.parent_id().map(|s| s.to_string()), element_type, diff --git a/validation/core/src/readers/sequence_diagram_reader.rs b/validation/core/src/readers/sequence_diagram_reader.rs index 344d33aa..082519b0 100644 --- a/validation/core/src/readers/sequence_diagram_reader.rs +++ b/validation/core/src/readers/sequence_diagram_reader.rs @@ -20,7 +20,7 @@ use sequence_logic::{ Condition, ConditionType, Event, Interaction, Return, SequenceNode, SequenceTree, }; -use crate::models::{SequenceDiagramInput, SequenceDiagramInputs}; +use crate::models::SequenceDiagramInputs; use crate::readers::Reader; pub struct SequenceDiagramReader; @@ -52,18 +52,9 @@ impl Reader for SequenceDiagramReader { Vec::new() }; - let source_files = diagram - .source_files() - .map(|values| values.iter().map(|f| f.to_string()).collect::>()) - .unwrap_or_default(); - - diagrams.push(SequenceDiagramInput { - tree: SequenceTree { - name: diagram.name().map(|s| s.to_string()), - root_interactions, - }, - source_files, - version: diagram.version().map(|s| s.to_string()), + diagrams.push(SequenceTree { + name: diagram.name().map(|s| s.to_string()), + root_interactions, }); } diff --git a/validation/core/src/validators/bazel_component_validator.rs b/validation/core/src/validators/bazel_component_validator.rs index 491745e9..ca30562c 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -227,8 +227,7 @@ fn append_debug_log( mod tests { use super::*; use crate::models::{ - BazelInput, BazelInputEntry, ComponentDiagramElementType, ComponentDiagramInput, - ComponentDiagramInputs, + BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentType, LogicComponent, }; use std::collections::BTreeMap; @@ -251,15 +250,16 @@ mod tests { alias: Option<&str>, parent_id: Option<&str>, stereotype: Option<&str>, - ) -> ComponentDiagramInput { + ) -> LogicComponent { let element_type = if stereotype == Some("SEooC") { - ComponentDiagramElementType::Package + ComponentType::Package } else { - ComponentDiagramElementType::Component + ComponentType::Component }; - ComponentDiagramInput { + LogicComponent { id: id.to_string(), + name: alias.map(|s| s.to_string()), alias: alias.map(|s| s.to_string()), parent_id: parent_id.map(|s| s.to_string()), element_type, @@ -268,7 +268,7 @@ mod tests { } } - fn diagram(entities: Vec) -> ComponentDiagramInputs { + fn diagram(entities: Vec) -> ComponentDiagramInputs { ComponentDiagramInputs { entities } } @@ -385,22 +385,6 @@ mod tests { assert!(errs.failures.iter().any(|m| m.contains("Missing unit"))); } - #[test] - fn test_duplicate_bazel_key_detected() { - let arch = make_arch(vec![ - ("@//pkg1:comp_a", vec![], vec![]), - ("@//pkg2:comp_a", vec![], vec![]), - ]); - let diagram = diagram(vec![entity("CompA", Some("comp_a"), None, Some("SEooC"))]); - let errs = run_arch_validation(&arch, &diagram); - assert!(!errs.is_empty()); - assert!( - errs.failures.iter().any(|m| m.contains("Duplicate")), - "Expected duplicate error, got: {:?}", - errs.failures - ); - } - #[test] fn test_same_short_name_different_packages_one_child() { let arch = make_arch(vec![ @@ -548,43 +532,4 @@ mod tests { let errs = run_arch_validation(&arch, &diagram); assert!(errs.is_empty(), "Expected pass, got: {:?}", errs.failures); } - - #[test] - fn test_duplicate_diagram_id_detected() { - let arch = make_arch(vec![("my_de", vec![], vec![])]); - let diagram = diagram(vec![ - entity("MyDE", Some("my_de"), None, Some("SEooC")), - entity("myDE", Some("other_alias"), None, Some("component")), - ]); - let errs = run_arch_validation(&arch, &diagram); - assert!( - errs.failures - .iter() - .any(|m| m.contains("Duplicate entity ID")), - "Expected duplicate ID error, got: {:?}", - errs.failures - ); - } - - #[test] - fn test_orphaned_parent_id_detected() { - let arch = make_arch(vec![("my_de", vec![], vec![])]); - let diagram = diagram(vec![ - entity("MyDE", Some("my_de"), None, Some("SEooC")), - entity( - "CompA", - Some("comp_a"), - Some("NonExistent"), - Some("component"), - ), - ]); - let errs = run_arch_validation(&arch, &diagram); - assert!( - errs.failures - .iter() - .any(|m| m.contains("Unresolved parent_id")), - "Expected unresolved parent error, got: {:?}", - errs.failures - ); - } } diff --git a/validation/core/src/validators/component_internal_api_validator.rs b/validation/core/src/validators/component_internal_api_validator.rs new file mode 100644 index 00000000..5e10ef55 --- /dev/null +++ b/validation/core/src/validators/component_internal_api_validator.rs @@ -0,0 +1,122 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Validation: compare component-diagram interfaces with interfaces declared +//! by the internal API diagram. + +use std::collections::BTreeSet; + +use super::shared::format_name_list; +use crate::models::{ComponentDiagramArchitecture, InternalApiIndex, LogicComponentExt}; +use crate::{Diagnostics, ValidationResult}; + +/// Run component-vs-internal-API interface reference validation. +pub fn validate_component_internal_api( + component_diagram: &ComponentDiagramArchitecture, + internal_api_diagram: &InternalApiIndex, +) -> ValidationResult { + ComponentInternalApiValidator::new(component_diagram, internal_api_diagram).run() +} + +struct ComponentInternalApiValidator { + component_interface_ids: BTreeSet, + internal_api_interface_ids: BTreeSet, + result: ValidationResult, +} + +impl ComponentInternalApiValidator { + fn new( + component_diagram: &ComponentDiagramArchitecture, + internal_api_diagram: &InternalApiIndex, + ) -> Self { + Self { + component_interface_ids: collect_component_interface_ids(component_diagram), + internal_api_interface_ids: collect_internal_api_interface_ids(internal_api_diagram), + result: ValidationResult::default(), + } + } + + fn run(mut self) -> ValidationResult { + append_debug_log( + &mut self.result.diagnostics, + &self.component_interface_ids, + &self.internal_api_interface_ids, + ); + self.check_component_interfaces_declared_by_internal_api(); + self.result + } + + fn check_component_interfaces_declared_by_internal_api(&mut self) { + let missing_interfaces: BTreeSet = self + .component_interface_ids + .difference(&self.internal_api_interface_ids) + .cloned() + .collect(); + + if !missing_interfaces.is_empty() { + self.result + .add_failure(format_missing_internal_api_interface_error( + &missing_interfaces, + )); + } + } +} + +fn append_debug_log( + diagnostics: &mut Diagnostics, + component_interface_ids: &BTreeSet, + internal_api_interface_ids: &BTreeSet, +) { + diagnostics.debug(|| "Component interfaces checked against internal API:".to_string()); + for interface_id in component_interface_ids { + diagnostics.debug(|| format!(" {interface_id}")); + } + + diagnostics.debug(|| "Internal API interfaces available for component interfaces:".to_string()); + for interface_id in internal_api_interface_ids { + diagnostics.debug(|| format!(" {interface_id}")); + } +} + +fn collect_component_interface_ids( + component_diagram: &ComponentDiagramArchitecture, +) -> BTreeSet { + component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface()) + .map(|entity| entity.id.clone()) + .collect() +} + +fn collect_internal_api_interface_ids(internal_api_diagram: &InternalApiIndex) -> BTreeSet { + internal_api_diagram + .interfaces() + .map(|interface| interface.id.clone()) + .collect() +} + +fn format_missing_internal_api_interface_error( + missing_internal_api_interfaces: &BTreeSet, +) -> String { + format!( + "Internal API consistency failure: Missing internal API interface:\n\ + Missing interfaces : {missing_interfaces}\n\ + Action : Add each component interface to the internal API diagram or remove it from the component diagram", + missing_interfaces = format_name_list(missing_internal_api_interfaces), + ) +} + +#[cfg(test)] +#[path = "test/component_internal_api_validator_test.rs"] +mod tests; diff --git a/validation/core/src/validators/component_sequence_validator.rs b/validation/core/src/validators/component_sequence_validator.rs index 31739d09..8b0baf7e 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -16,48 +16,31 @@ use std::collections::{BTreeMap, BTreeSet}; -use crate::models::{ - ComponentDiagramArchitecture, InternalApiIndex, InternalApiInterface, SequenceDiagramIndex, +use super::shared::{ + build_observed_call_contexts, build_unit_bindings, format_name_list, intersect_interfaces, + SequenceCallContext, UnitBindings, }; +use crate::models::{ComponentDiagramArchitecture, SequenceDiagramIndex}; use crate::{Diagnostics, ValidationResult}; /// Run component-vs-sequence naming validation. pub fn validate_component_sequence( component_diagram: &ComponentDiagramArchitecture, sequence_diagram: &SequenceDiagramIndex, - internal_api_diagram: Option<&InternalApiIndex>, ) -> ValidationResult { - ComponentSequenceValidator::new(component_diagram, sequence_diagram, internal_api_diagram).run() + ComponentSequenceValidator::new(component_diagram, sequence_diagram).run() } type ConnectedUnitPairs = BTreeMap<(String, String), BTreeSet>; -type InternalApiInterfacesById<'a> = BTreeMap; struct ComponentSequenceValidator<'a> { observed_participants: &'a BTreeSet, observed_call_contexts: Vec>, connected_unit_pairs: ConnectedUnitPairs, - unit_bindings: BTreeMap, - all_interfaces: BTreeSet, - internal_api_interfaces_by_id: Option>, + unit_bindings: UnitBindings, result: ValidationResult, } -#[derive(Clone, Default)] -struct UnitInterfaces { - all_interfaces: BTreeSet, - required_interfaces: BTreeSet, - provided_interfaces: BTreeSet, -} - -struct SequenceCallContext<'a> { - caller_unit: &'a str, - callee_unit: &'a str, - method: &'a str, - caller_interfaces: BTreeSet, - callee_interfaces: BTreeSet, -} - impl SequenceCallContext<'_> { fn normalized_left_unit(&self) -> &str { if self.caller_unit <= self.callee_unit { @@ -90,21 +73,14 @@ impl SequenceCallContext<'_> { &self.callee_interfaces } } - - fn has_shared_interfaces(&self) -> bool { - !self.caller_interfaces.is_disjoint(&self.callee_interfaces) - } } impl<'a> ComponentSequenceValidator<'a> { fn new( component_diagram: &ComponentDiagramArchitecture, sequence_diagram: &'a SequenceDiagramIndex, - internal_api_diagram: Option<&'a InternalApiIndex>, ) -> Self { let unit_bindings = build_unit_bindings(component_diagram); - let all_interfaces = - build_all_interfaces(component_diagram, &unit_bindings, internal_api_diagram); let observed_call_contexts = build_observed_call_contexts(sequence_diagram.observed_calls(), &unit_bindings); @@ -113,10 +89,6 @@ impl<'a> ComponentSequenceValidator<'a> { observed_call_contexts, connected_unit_pairs: build_connected_unit_pairs(&unit_bindings), unit_bindings, - all_interfaces, - internal_api_interfaces_by_id: build_internal_api_interfaces_by_id( - internal_api_diagram, - ), result: ValidationResult::default(), } } @@ -127,8 +99,6 @@ impl<'a> ComponentSequenceValidator<'a> { self.observed_participants, &self.observed_call_contexts, &self.unit_bindings, - &self.all_interfaces, - self.internal_api_interfaces_by_id.as_ref(), &self.connected_unit_pairs, ); self.check_consistency(); @@ -139,9 +109,6 @@ impl<'a> ComponentSequenceValidator<'a> { self.check_participant_aliases(); self.check_interface_connected_units_have_sequence_calls(); self.check_sequence_calls_have_interface_connections(); - self.check_sequence_call_interface_roles(); - self.check_sequence_call_method_consistency(); - self.check_interface_method_coverage(); } fn check_participant_aliases(&mut self) { @@ -184,7 +151,7 @@ impl<'a> ComponentSequenceValidator<'a> { Shared interfaces : {shared_interfaces}\n\ Action : Add a function-call connection between these units in a sequence diagram", unit_pair = format_unit_pair(left_unit, right_unit), - shared_interfaces = format_interface_names(interfaces), + shared_interfaces = format_name_list(interfaces), )); } } @@ -230,230 +197,18 @@ impl<'a> ComponentSequenceValidator<'a> { ), left_unit = call_context.normalized_left_unit(), right_unit = call_context.normalized_right_unit(), - left_interfaces = format_interface_names(left_interfaces), - right_interfaces = format_interface_names(right_interfaces), - )); - } - } - - fn check_sequence_call_interface_roles(&mut self) { - let mut seen_interactions = BTreeSet::new(); - - for call_context in &self.observed_call_contexts { - if extract_method_name(call_context.method).is_empty() { - continue; - } - - if !self.unit_bindings.contains_key(call_context.caller_unit) - || !self.unit_bindings.contains_key(call_context.callee_unit) - { - continue; - } - - if call_context.caller_unit == call_context.callee_unit { - continue; - } - - if !seen_interactions.insert(( - call_context.caller_unit.to_string(), - call_context.callee_unit.to_string(), - )) { - continue; - } - - let caller_bindings = - unit_bindings_for_alias(&self.unit_bindings, call_context.caller_unit); - - if !call_context.has_shared_interfaces() { - continue; - } - - let callee_bindings = - unit_bindings_for_alias(&self.unit_bindings, call_context.callee_unit); - let directional_interfaces = intersect_interfaces( - &caller_bindings.required_interfaces, - &callee_bindings.provided_interfaces, - ); - - if !directional_interfaces.is_empty() { - continue; - } - - self.result - .add_failure(format_sequence_role_consistency_error( - call_context, - &caller_bindings.required_interfaces, - &callee_bindings.provided_interfaces, - )); - } - } - - fn check_sequence_call_method_consistency(&mut self) { - let Some(internal_api_interfaces_by_id) = self.internal_api_interfaces_by_id.as_ref() - else { - return; - }; - - let missing_internal_api_interfaces_by_unit = - self.collect_missing_internal_api_interfaces_by_unit(internal_api_interfaces_by_id); - for (unit_alias, missing_interfaces) in &missing_internal_api_interfaces_by_unit { - self.result - .add_failure(format_missing_internal_api_interface_error( - unit_alias, - missing_interfaces, - )); - } - - let mut seen_calls = BTreeSet::new(); - - for call_context in &self.observed_call_contexts { - let is_self_call = call_context.caller_unit == call_context.callee_unit; - - let method_name = extract_method_name(call_context.method); - if method_name.is_empty() { - continue; - } - - let call_key = ( - call_context.caller_unit.to_string(), - call_context.callee_unit.to_string(), - method_name.to_string(), - ); - if !seen_calls.insert(call_key) { - continue; - } - - if is_self_call { - let matching_interfaces = matching_interfaces_with_method( - internal_api_interfaces_by_id, - &self.all_interfaces, - method_name, - ); - - if matching_interfaces.is_empty() { - self.result.add_failure(format_sequence_method_consistency_error( - call_context, - method_name, - "sequence self-call function name was not found in available interface methods", - "Declare this method on one of the available interfaces in the internal API diagram", - )); - } - - continue; - } - - if !call_context.has_shared_interfaces() { - // The structural interface check above already reported that this - // cross-unit call has no usable shared interface relation. - continue; - } - - if missing_internal_api_interfaces_by_unit.contains_key(call_context.caller_unit) - || missing_internal_api_interfaces_by_unit.contains_key(call_context.callee_unit) - { - continue; - } - - let caller_matching_interfaces = matching_interfaces_with_method( - internal_api_interfaces_by_id, - &call_context.caller_interfaces, - method_name, - ); - let callee_matching_interfaces = matching_interfaces_with_method( - internal_api_interfaces_by_id, - &call_context.callee_interfaces, - method_name, - ); - let shared_matching_interfaces = - intersect_interfaces(&caller_matching_interfaces, &callee_matching_interfaces); - - if !shared_matching_interfaces.is_empty() { - continue; - } - - self.result.add_failure(format_sequence_method_consistency_error( - call_context, - method_name, - "sequence function name was not found in the related interface methods", - "Declare this method on a shared interface referenced by both participating units in the internal API diagram", - )); - } - } - - fn check_interface_method_coverage(&mut self) { - let Some(internal_api_interfaces_by_id) = self.internal_api_interfaces_by_id.as_ref() - else { - return; - }; - - let exercised_method_names = self.collect_exercised_method_names(); - - for interface in internal_api_interfaces_by_id.values().copied() { - let missing_methods: BTreeSet = interface - .method_names - .difference(&exercised_method_names) - .cloned() - .collect(); - - if missing_methods.is_empty() { - continue; - } - - self.result.add_failure(format!( - "Coverage consistency failure: internal API interface functions are not exercised in sequence diagrams:\n\ - Interface id : \"{interface_id}\"\n\ - Missing functions : {missing_functions}\n\ - Action : Add sequence interactions that call each missing function", - interface_id = interface.id, - missing_functions = format_name_list(&missing_methods), + left_interfaces = format_name_list(left_interfaces), + right_interfaces = format_name_list(right_interfaces), )); } } - - fn collect_missing_internal_api_interfaces_by_unit( - &self, - internal_api_interfaces_by_id: &InternalApiInterfacesById<'a>, - ) -> BTreeMap> { - self.unit_bindings - .iter() - .filter_map(|(unit_alias, bindings)| { - let missing_interfaces = missing_internal_api_interfaces( - internal_api_interfaces_by_id, - &bindings.all_interfaces, - ); - - if missing_interfaces.is_empty() { - None - } else { - Some((unit_alias.clone(), missing_interfaces)) - } - }) - .collect() - } - - fn collect_exercised_method_names(&self) -> BTreeSet { - let mut exercised_method_names = BTreeSet::new(); - - for call_context in &self.observed_call_contexts { - let method_name = extract_method_name(call_context.method); - if method_name.is_empty() { - continue; - } - - exercised_method_names.insert(method_name.to_string()); - } - - exercised_method_names - } } fn append_debug_log( diagnostics: &mut Diagnostics, observed_participants: &BTreeSet, observed_call_contexts: &[SequenceCallContext<'_>], - unit_bindings: &BTreeMap, - all_interfaces: &BTreeSet, - internal_api_interfaces_by_id: Option<&BTreeMap>, + unit_bindings: &UnitBindings, connected_unit_pairs: &BTreeMap<(String, String), BTreeSet>, ) { diagnostics.debug(|| "Expected unit aliases from component diagrams:".to_string()); @@ -481,38 +236,19 @@ fn append_debug_log( diagnostics.debug(|| { format!( " {unit_alias} -> {}", - format_interface_names(&bindings.all_interfaces) + format_name_list(&bindings.all_interfaces) ) }); } - diagnostics.debug(|| { - format!( - "All interfaces for self-call validation: {}", - format_interface_names(all_interfaces) - ) - }); - - if let Some(internal_api_interfaces_by_id) = internal_api_interfaces_by_id { - diagnostics.debug(|| "Internal API interfaces checked for method validation:".to_string()); - for interface_id in internal_api_interfaces_by_id.keys() { - diagnostics.debug(|| format!(" {interface_id}")); - } - } - diagnostics.debug(|| "Interface-connected unit pairs from component diagrams:".to_string()); for ((left, right), interfaces) in connected_unit_pairs { - diagnostics.debug(|| { - format!( - " {left} <-> {right} via {}", - format_interface_names(interfaces) - ) - }); + diagnostics.debug(|| format!(" {left} <-> {right} via {}", format_name_list(interfaces))); } } fn build_connected_unit_pairs( - unit_bindings: &BTreeMap, + unit_bindings: &UnitBindings, ) -> BTreeMap<(String, String), BTreeSet> { let mut connected_unit_pairs = BTreeMap::new(); let aliases: Vec<&String> = unit_bindings.keys().collect(); @@ -521,11 +257,16 @@ fn build_connected_unit_pairs( for other_index in (index + 1)..aliases.len() { let left_alias = aliases[index]; let right_alias = aliases[other_index]; - let shared_interfaces: BTreeSet = unit_bindings[left_alias] - .all_interfaces - .intersection(&unit_bindings[right_alias].all_interfaces) - .cloned() - .collect(); + let left_bindings = &unit_bindings[left_alias]; + let right_bindings = &unit_bindings[right_alias]; + let mut shared_interfaces = intersect_interfaces( + &left_bindings.required_interfaces, + &right_bindings.provided_interfaces, + ); + shared_interfaces.extend(intersect_interfaces( + &right_bindings.required_interfaces, + &left_bindings.provided_interfaces, + )); if shared_interfaces.is_empty() { continue; @@ -539,287 +280,10 @@ fn build_connected_unit_pairs( connected_unit_pairs } -fn build_unit_bindings( - component_diagram: &ComponentDiagramArchitecture, -) -> BTreeMap { - let mut unit_bindings = BTreeMap::new(); - - for entity in component_diagram - .entities - .iter() - .filter(|entity| entity.is_unit()) - { - let Some(alias) = entity.alias.clone() else { - continue; - }; - - let mut bindings = UnitInterfaces::default(); - - for relation in &entity.relations { - let Some(interface_id) = component_diagram - .entities - .iter() - .find(|candidate| candidate.is_interface() && candidate.id == relation.target) - .map(|candidate| candidate.id.clone()) - else { - continue; - }; - - bindings.all_interfaces.insert(interface_id.clone()); - - match relation.source_role.as_deref() { - Some("Required") => { - bindings.required_interfaces.insert(interface_id); - } - Some("Provided") => { - bindings.provided_interfaces.insert(interface_id); - } - _ => {} - } - } - - unit_bindings.insert(alias, bindings); - } - - unit_bindings -} - -fn build_all_interfaces( - component_diagram: &ComponentDiagramArchitecture, - unit_bindings: &BTreeMap, - internal_api_diagram: Option<&InternalApiIndex>, -) -> BTreeSet { - let mut interface_ids: BTreeSet = component_diagram - .entities - .iter() - .filter(|entity| entity.is_interface()) - .map(|entity| entity.id.clone()) - .collect(); - - if let Some(internal_api_diagram) = internal_api_diagram { - interface_ids.extend( - internal_api_diagram - .interfaces() - .map(|interface| interface.id.clone()), - ); - } - - for bindings in unit_bindings.values() { - interface_ids.extend(bindings.all_interfaces.iter().cloned()); - } - - interface_ids -} - -fn all_interfaces_for_alias( - unit_bindings: &BTreeMap, - alias: &str, -) -> BTreeSet { - unit_bindings - .get(alias) - .map(|bindings| bindings.all_interfaces.clone()) - .unwrap_or_default() -} - -fn unit_bindings_for_alias( - unit_bindings: &BTreeMap, - alias: &str, -) -> UnitInterfaces { - unit_bindings.get(alias).cloned().unwrap_or_default() -} - -fn build_observed_call_contexts<'a>( - observed_calls: &'a [crate::models::ObservedSequenceCall], - unit_bindings: &BTreeMap, -) -> Vec> { - observed_calls - .iter() - .map(|call| { - let caller_interfaces = all_interfaces_for_alias(unit_bindings, &call.caller); - let callee_interfaces = all_interfaces_for_alias(unit_bindings, &call.callee); - - SequenceCallContext { - caller_unit: call.caller.as_str(), - callee_unit: call.callee.as_str(), - method: call.method.as_str(), - caller_interfaces, - callee_interfaces, - } - }) - .collect() -} - -fn intersect_interfaces( - left_interfaces: &BTreeSet, - right_interfaces: &BTreeSet, -) -> BTreeSet { - left_interfaces - .intersection(right_interfaces) - .cloned() - .collect() -} - -fn matching_interfaces_with_method( - internal_api_interfaces_by_id: &BTreeMap, - interface_ids: &BTreeSet, - method_name: &str, -) -> BTreeSet { - interface_ids - .iter() - .filter(|interface_id| { - matching_internal_api_interface_ids(internal_api_interfaces_by_id, interface_id) - .into_iter() - .filter_map(|matched_interface_id| { - internal_api_interfaces_by_id - .get(&matched_interface_id) - .copied() - }) - .any(|interface| interface.method_names.contains(method_name)) - }) - .cloned() - .collect() -} - -fn build_internal_api_interfaces_by_id( - internal_api_diagram: Option<&InternalApiIndex>, -) -> Option> { - let mut interfaces_by_id = BTreeMap::new(); - - let internal_api_diagram = internal_api_diagram?; - - for interface in internal_api_diagram.interfaces() { - interfaces_by_id.insert(interface.id.clone(), interface); - } - - Some(interfaces_by_id) -} - -fn missing_internal_api_interfaces( - internal_api_interfaces_by_id: &BTreeMap, - interface_ids: &BTreeSet, -) -> BTreeSet { - interface_ids - .iter() - .filter(|interface_id| { - !has_matching_internal_api_reference(internal_api_interfaces_by_id, interface_id) - }) - .cloned() - .collect() -} - -fn matching_internal_api_interface_ids( - internal_api_interfaces_by_id: &BTreeMap, - reference: &str, -) -> BTreeSet { - let mut interface_ids = BTreeSet::new(); - - if internal_api_interfaces_by_id.contains_key(reference) { - interface_ids.insert(reference.to_string()); - } - - interface_ids -} - -fn has_matching_internal_api_reference( - internal_api_interfaces_by_id: &BTreeMap, - reference: &str, -) -> bool { - internal_api_interfaces_by_id.contains_key(reference) -} - -fn format_sequence_role_consistency_error( - call_context: &SequenceCallContext<'_>, - caller_required_interfaces: &BTreeSet, - callee_provided_interfaces: &BTreeSet, -) -> String { - let sequence_call = format_sequence_call( - call_context.caller_unit, - call_context.callee_unit, - call_context.method, - ); - let shared_interfaces = intersect_interfaces( - &call_context.caller_interfaces, - &call_context.callee_interfaces, - ); - - let expected_interfaces = if shared_interfaces.is_empty() { - intersect_interfaces(caller_required_interfaces, callee_provided_interfaces) - } else { - shared_interfaces - }; - - format!( - "Interface consistency failure: sequence interaction does not match consumer/provider roles in the component diagram:\n\ - Sequence call : {sequence_call}\n\ - Expected caller role: \"{caller_unit}\" should require shared interface(s) {expected_interfaces}\n\ - Expected callee role: \"{callee_unit}\" should provide shared interface(s) {expected_interfaces}\n\ - Action : Reverse the sequence call or align the required/provided interface bindings in the component diagram", - caller_unit = call_context.caller_unit, - callee_unit = call_context.callee_unit, - expected_interfaces = format_interface_names(&expected_interfaces), - ) -} - -fn format_sequence_method_consistency_error( - call_context: &SequenceCallContext<'_>, - method_name: &str, - description: &str, - action: &str, -) -> String { - let sequence_call = format_sequence_call( - call_context.caller_unit, - call_context.callee_unit, - method_name, - ); - - format!( - "Method consistency failure: {description}:\n\ - Sequence call : {sequence_call}\n\ - Action : {action}", - ) -} - -fn format_sequence_call(caller_unit: &str, callee_unit: &str, method_name: &str) -> String { - format!("\"{caller_unit}\" -> \"{callee_unit}\" : \"{method_name}\"") -} - fn format_unit_pair(left_unit: &str, right_unit: &str) -> String { format!("\"{left_unit}\" <-> \"{right_unit}\"") } -fn format_missing_internal_api_interface_error( - unit_alias: &str, - missing_internal_api_interfaces: &BTreeSet, -) -> String { - format!( - "Method consistency failure: Missing internal API interface:\n\ - Unit : \"{unit_alias}\"\n\ - Missing interfaces : {missing_interfaces}\n\ - Action : Add the referenced interfaces to the internal API diagram or fix the component diagram references", - missing_interfaces = format_interface_names(missing_internal_api_interfaces), - ) -} - -fn format_interface_names(interfaces: &BTreeSet) -> String { - format_name_list(interfaces) -} - -fn format_name_list(names: &BTreeSet) -> String { - if names.is_empty() { - return "".to_string(); - } - - names - .iter() - .map(|name| format!("\"{name}\"")) - .collect::>() - .join(", ") -} - -fn extract_method_name(method: &str) -> &str { - method.split('(').next().unwrap_or(method).trim() -} - #[cfg(test)] #[path = "test/component_sequence_validator_test.rs"] mod tests; diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 94cf983d..be1c55ee 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -14,7 +14,16 @@ //! Validator entrypoints for architecture checks. mod bazel_component_validator; +mod component_internal_api_validator; mod component_sequence_validator; +mod sequence_internal_api_validator; +mod shared; + +#[cfg(test)] +#[path = "test/fixtures.rs"] +pub(crate) mod fixtures; pub use bazel_component_validator::validate_bazel_component; +pub use component_internal_api_validator::validate_component_internal_api; pub use component_sequence_validator::validate_component_sequence; +pub use sequence_internal_api_validator::validate_sequence_internal_api; diff --git a/validation/core/src/validators/sequence_internal_api_validator.rs b/validation/core/src/validators/sequence_internal_api_validator.rs new file mode 100644 index 00000000..96e72600 --- /dev/null +++ b/validation/core/src/validators/sequence_internal_api_validator.rs @@ -0,0 +1,513 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Validation: compare sequence-diagram usage with methods declared by +//! internal API interfaces. Method names are checked against related shared +//! interfaces only when component context is available. + +use std::collections::{BTreeMap, BTreeSet}; + +use super::shared::{ + build_observed_call_contexts, build_unit_bindings, extract_method_name, format_name_list, + format_sequence_call, intersect_interfaces, SequenceCallContext, UnitBindings, UnitInterfaces, +}; +use crate::models::{ + ComponentDiagramArchitecture, InternalApiIndex, InternalApiInterface, LogicComponentExt, + SequenceDiagramIndex, +}; +use crate::{Diagnostics, ValidationResult}; + +/// Run sequence-vs-internal-API method and coverage validation. +pub fn validate_sequence_internal_api( + sequence_diagram: &SequenceDiagramIndex, + internal_api_diagram: &InternalApiIndex, + component_diagram: Option<&ComponentDiagramArchitecture>, +) -> ValidationResult { + SequenceInternalApiValidator::new(sequence_diagram, internal_api_diagram, component_diagram) + .run() +} + +struct SequenceInternalApiValidator<'a> { + sequence_diagram: &'a SequenceDiagramIndex, + internal_api_interfaces_by_id: BTreeMap, + component_context: Option>, + result: ValidationResult, +} + +struct ComponentContext<'a> { + observed_call_contexts: Vec>, + unit_bindings: UnitBindings, + all_interfaces: BTreeSet, +} + +impl<'a> SequenceInternalApiValidator<'a> { + fn new( + sequence_diagram: &'a SequenceDiagramIndex, + internal_api_diagram: &'a InternalApiIndex, + component_diagram: Option<&ComponentDiagramArchitecture>, + ) -> Self { + let internal_api_interfaces_by_id = + build_internal_api_interfaces_by_id(internal_api_diagram); + let component_context = component_diagram.map(|component_diagram| { + build_component_context(component_diagram, sequence_diagram, internal_api_diagram) + }); + + Self { + sequence_diagram, + internal_api_interfaces_by_id, + component_context, + result: ValidationResult::default(), + } + } + + fn run(mut self) -> ValidationResult { + append_debug_log( + &mut self.result.diagnostics, + &self.component_context, + self.sequence_diagram, + &self.internal_api_interfaces_by_id, + ); + self.check_sequence_call_method_consistency_with_component_context(); + self.check_interface_method_coverage(); + self.result + } + + fn check_sequence_call_method_consistency_with_component_context(&mut self) { + let Some(component_context) = self.component_context.as_ref() else { + return; + }; + let units_with_missing_internal_api_interfaces = + collect_units_with_missing_internal_api_interfaces( + &component_context.unit_bindings, + &self.internal_api_interfaces_by_id, + ); + + let mut seen_calls = BTreeSet::new(); + let mut consistency_errors = Vec::new(); + + for call_context in &component_context.observed_call_contexts { + let is_self_call = call_context.caller_unit == call_context.callee_unit; + + let method_name = extract_method_name(call_context.method); + if method_name.is_empty() { + continue; + } + + let call_key = ( + call_context.caller_unit.to_string(), + call_context.callee_unit.to_string(), + method_name.to_string(), + ); + if !seen_calls.insert(call_key) { + // Repeated calls exercise the same method relation once. + continue; + } + + if let Some(error) = self.check_method_exists_in_internal_api( + component_context, + call_context, + method_name, + ) { + consistency_errors.push(error); + continue; + } + + if is_self_call { + // Self-calls are not checked for cross-unit role consistency. + continue; + } + + if let Some(error) = self.check_cross_unit_call_consistency( + component_context, + &units_with_missing_internal_api_interfaces, + call_context, + method_name, + ) { + consistency_errors.push(error); + } + } + + for error in consistency_errors { + self.result.add_failure(error); + } + } + + fn check_method_exists_in_internal_api( + &self, + component_context: &ComponentContext<'_>, + call_context: &SequenceCallContext<'_>, + method_name: &str, + ) -> Option { + let matching_interfaces = matching_interfaces_with_method( + &self.internal_api_interfaces_by_id, + &component_context.all_interfaces, + method_name, + ); + + if matching_interfaces.is_empty() { + return Some(format_sequence_method_consistency_error( + call_context, + method_name, + "sequence function name was not found in available interface methods", + "Declare this method on one of the available interfaces in the internal API diagram", + )); + } + + None + } + + fn check_cross_unit_call_consistency( + &self, + component_context: &ComponentContext<'_>, + units_with_missing_internal_api_interfaces: &BTreeSet, + call_context: &SequenceCallContext<'_>, + method_name: &str, + ) -> Option { + if !call_context.has_shared_interfaces() { + // The structural component-sequence validator reports missing shared interface relations. + return None; + } + + if units_with_missing_internal_api_interfaces.contains(call_context.caller_unit) + || units_with_missing_internal_api_interfaces.contains(call_context.callee_unit) + { + // The component-internal-api validator reports missing interface declarations first. + return None; + } + + let caller_matching_interfaces = matching_interfaces_with_method( + &self.internal_api_interfaces_by_id, + &call_context.caller_interfaces, + method_name, + ); + let callee_matching_interfaces = matching_interfaces_with_method( + &self.internal_api_interfaces_by_id, + &call_context.callee_interfaces, + method_name, + ); + + let shared_method_interfaces = + intersect_interfaces(&caller_matching_interfaces, &callee_matching_interfaces); + + if shared_method_interfaces.is_empty() { + return Some(format_sequence_method_consistency_error( + call_context, + method_name, + "sequence function name was not found in the related interface methods", + "Declare this method on a shared interface referenced by both participating units in the internal API diagram", + )); + } + + self.check_cross_unit_call_role_consistency( + component_context, + call_context, + method_name, + &shared_method_interfaces, + ) + } + + fn check_cross_unit_call_role_consistency( + &self, + component_context: &ComponentContext<'_>, + call_context: &SequenceCallContext<'_>, + method_name: &str, + shared_method_interfaces: &BTreeSet, + ) -> Option { + let caller_bindings = component_context + .unit_bindings + .get(call_context.caller_unit)?; + let callee_bindings = component_context + .unit_bindings + .get(call_context.callee_unit)?; + + let caller_method_role_interfaces = + intersect_interfaces(shared_method_interfaces, &role_interfaces(caller_bindings)); + + let callee_method_role_interfaces = + intersect_interfaces(shared_method_interfaces, &role_interfaces(callee_bindings)); + + if caller_method_role_interfaces.is_empty() + || callee_method_role_interfaces.is_empty() + || intersect_interfaces( + &caller_method_role_interfaces, + &callee_method_role_interfaces, + ) + .is_empty() + { + log::warn!("sequence call between units \"{}\" and \"{}\" for method \"{}\" has shared interface(s) {:?} but no matching required/provided roles in the component diagram", + call_context.caller_unit, call_context.callee_unit, method_name, shared_method_interfaces); + return None; + } + + let caller_method_required_interfaces = intersect_interfaces( + shared_method_interfaces, + &caller_bindings.required_interfaces, + ); + + let callee_method_provided_interfaces = intersect_interfaces( + shared_method_interfaces, + &callee_bindings.provided_interfaces, + ); + + let directional_method_interfaces = intersect_interfaces( + &caller_method_required_interfaces, + &callee_method_provided_interfaces, + ); + + if directional_method_interfaces.is_empty() { + return Some(format_sequence_role_consistency_error( + call_context, + method_name, + shared_method_interfaces, + )); + } + + None + } + + fn check_interface_method_coverage(&mut self) { + let exercised_method_names = collect_exercised_method_names(self.sequence_diagram); + + for interface in self.internal_api_interfaces_by_id.values().copied() { + let missing_methods: BTreeSet = interface + .method_names + .difference(&exercised_method_names) + .cloned() + .collect(); + + if missing_methods.is_empty() { + continue; + } + + self.result + .add_failure(format_interface_method_coverage_error( + interface, + &missing_methods, + )); + } + } +} + +fn append_debug_log( + diagnostics: &mut Diagnostics, + component_context: &Option>, + sequence_diagram: &SequenceDiagramIndex, + internal_api_interfaces_by_id: &BTreeMap, +) { + if let Some(component_context) = component_context.as_ref() { + diagnostics.debug(|| { + "Sequence calls checked against related internal API interfaces:".to_string() + }); + for call_context in &component_context.observed_call_contexts { + diagnostics.debug(|| { + format!( + " {} -> {} : {}", + call_context.caller_unit, call_context.callee_unit, call_context.method + ) + }); + } + + diagnostics.debug(|| "Unit interface targets from component diagrams:".to_string()); + for (unit_alias, bindings) in &component_context.unit_bindings { + diagnostics.debug(|| { + format!( + " {unit_alias} -> {}", + format_name_list(&bindings.all_interfaces) + ) + }); + } + + diagnostics.debug(|| { + format!( + "All interfaces for self-call validation:\n {}", + format_name_list(&component_context.all_interfaces) + ) + }); + } else { + diagnostics.debug(|| { + "Sequence method-name consistency skipped because component context is unavailable:" + .to_string() + }); + for call in sequence_diagram.observed_calls() { + diagnostics.debug(|| format!(" {} -> {} : {}", call.caller, call.callee, call.method)); + } + } + + diagnostics.debug(|| "Internal API interfaces available for sequence validation:".to_string()); + for interface_id in internal_api_interfaces_by_id.keys() { + diagnostics.debug(|| format!(" {interface_id}")); + } +} + +fn build_component_context<'a>( + component_diagram: &ComponentDiagramArchitecture, + sequence_diagram: &'a SequenceDiagramIndex, + internal_api_diagram: &InternalApiIndex, +) -> ComponentContext<'a> { + let unit_bindings = build_unit_bindings(component_diagram); + let all_interfaces = build_all_interfaces(component_diagram, internal_api_diagram); + let observed_call_contexts = + build_observed_call_contexts(sequence_diagram.observed_calls(), &unit_bindings); + + ComponentContext { + observed_call_contexts, + unit_bindings, + all_interfaces, + } +} + +fn build_all_interfaces( + component_diagram: &ComponentDiagramArchitecture, + internal_api_diagram: &InternalApiIndex, +) -> BTreeSet { + let mut interface_ids: BTreeSet = component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface()) + .map(|entity| entity.id.clone()) + .collect(); + + interface_ids.extend( + internal_api_diagram + .interfaces() + .map(|interface| interface.id.clone()), + ); + + interface_ids +} + +fn matching_interfaces_with_method( + internal_api_interfaces_by_id: &BTreeMap, + interface_ids: &BTreeSet, + method_name: &str, +) -> BTreeSet { + interface_ids + .iter() + .filter(|interface_id| { + internal_api_interfaces_by_id + .get(interface_id.as_str()) + .is_some_and(|interface| interface.method_names.contains(method_name)) + }) + .cloned() + .collect() +} + +fn role_interfaces(bindings: &UnitInterfaces) -> BTreeSet { + bindings + .required_interfaces + .union(&bindings.provided_interfaces) + .cloned() + .collect() +} + +fn build_internal_api_interfaces_by_id( + internal_api_diagram: &InternalApiIndex, +) -> BTreeMap { + let mut interfaces_by_id = BTreeMap::new(); + + for interface in internal_api_diagram.interfaces() { + interfaces_by_id.insert(interface.id.clone(), interface); + } + + interfaces_by_id +} + +fn collect_units_with_missing_internal_api_interfaces( + unit_bindings: &UnitBindings, + internal_api_interfaces_by_id: &BTreeMap, +) -> BTreeSet { + unit_bindings + .iter() + .filter(|(_, bindings)| { + bindings.all_interfaces.iter().any(|interface_id| { + !internal_api_interfaces_by_id.contains_key(interface_id.as_str()) + }) + }) + .map(|(unit_alias, _)| unit_alias.clone()) + .collect() +} + +fn collect_exercised_method_names(sequence_diagram: &SequenceDiagramIndex) -> BTreeSet { + let mut exercised_method_names = BTreeSet::new(); + + for call in sequence_diagram.observed_calls() { + let method_name = extract_method_name(&call.method); + if method_name.is_empty() { + continue; + } + + exercised_method_names.insert(method_name.to_string()); + } + + exercised_method_names +} + +fn format_interface_method_coverage_error( + interface: &InternalApiInterface, + missing_methods: &BTreeSet, +) -> String { + format!( + "Coverage consistency failure: internal API interface functions are not exercised in sequence diagrams:\n\ + Interface id : \"{interface_id}\"\n\ + Missing functions : {missing_functions}\n\ + Action : Add sequence interactions that call each missing function", + interface_id = interface.id, + missing_functions = format_name_list(missing_methods), + ) +} + +fn format_sequence_method_consistency_error( + call_context: &SequenceCallContext<'_>, + method_name: &str, + description: &str, + action: &str, +) -> String { + let sequence_call = format_sequence_call( + call_context.caller_unit, + call_context.callee_unit, + method_name, + ); + + format!( + "Method consistency failure: {description}:\n\ + Sequence call : {sequence_call}\n\ + Action : {action}", + ) +} + +fn format_sequence_role_consistency_error( + call_context: &SequenceCallContext<'_>, + method_name: &str, + expected_interfaces: &BTreeSet, +) -> String { + let sequence_call = format_sequence_call( + call_context.caller_unit, + call_context.callee_unit, + method_name, + ); + + format!( + "Interface consistency failure: sequence interaction does not match consumer/provider roles in the component diagram:\n\ + Sequence call : {sequence_call}\n\ + Expected caller role: \"{caller_unit}\" should require shared interface(s) {expected_interfaces}\n\ + Expected callee role: \"{callee_unit}\" should provide shared interface(s) {expected_interfaces}\n\ + Action : Reverse the sequence call or align the required/provided interface bindings in the component diagram", + caller_unit = call_context.caller_unit, + callee_unit = call_context.callee_unit, + expected_interfaces = format_name_list(expected_interfaces), + ) +} + +#[cfg(test)] +#[path = "test/sequence_internal_api_validator_test.rs"] +mod tests; diff --git a/validation/core/src/validators/shared/diagram_analysis.rs b/validation/core/src/validators/shared/diagram_analysis.rs new file mode 100644 index 00000000..2441c68c --- /dev/null +++ b/validation/core/src/validators/shared/diagram_analysis.rs @@ -0,0 +1,126 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Derived diagram analysis shared by validators. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::models::{ + ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, LogicComponentExt, + ObservedSequenceCall, +}; + +pub(in crate::validators) type UnitBindings = BTreeMap; + +#[derive(Clone, Default)] +pub(in crate::validators) struct UnitInterfaces { + pub(in crate::validators) all_interfaces: BTreeSet, + pub(in crate::validators) required_interfaces: BTreeSet, + pub(in crate::validators) provided_interfaces: BTreeSet, +} + +#[derive(Clone)] +pub(in crate::validators) struct SequenceCallContext<'a> { + pub(in crate::validators) caller_unit: &'a str, + pub(in crate::validators) callee_unit: &'a str, + pub(in crate::validators) method: &'a str, + pub(in crate::validators) caller_interfaces: BTreeSet, + pub(in crate::validators) callee_interfaces: BTreeSet, +} + +impl SequenceCallContext<'_> { + pub(in crate::validators) fn has_shared_interfaces(&self) -> bool { + !self.caller_interfaces.is_disjoint(&self.callee_interfaces) + } +} + +pub(in crate::validators) fn build_unit_bindings( + component_diagram: &ComponentDiagramArchitecture, +) -> UnitBindings { + let interface_ids: BTreeSet<&str> = component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface()) + .map(|entity| entity.id.as_str()) + .collect(); + let mut unit_bindings = BTreeMap::new(); + + for entity in component_diagram + .entities + .iter() + .filter(|entity| entity.is_unit()) + { + let Some(alias) = entity.alias.clone() else { + continue; + }; + + let mut bindings = UnitInterfaces::default(); + + for relation in &entity.relations { + if !interface_ids.contains(relation.target.as_str()) { + continue; + } + + bindings.all_interfaces.insert(relation.target.clone()); + + if relation.relation_type != ComponentRelationType::InterfaceBinding { + continue; + } + + match relation.source_role { + EndpointRole::Required => { + bindings.required_interfaces.insert(relation.target.clone()); + } + EndpointRole::Provided => { + bindings.provided_interfaces.insert(relation.target.clone()); + } + EndpointRole::None => {} + } + } + + unit_bindings.insert(alias, bindings); + } + + unit_bindings +} + +pub(in crate::validators) fn all_interfaces_for_alias( + unit_bindings: &UnitBindings, + alias: &str, +) -> BTreeSet { + unit_bindings + .get(alias) + .map(|bindings| bindings.all_interfaces.clone()) + .unwrap_or_default() +} + +pub(in crate::validators) fn build_observed_call_contexts<'a>( + observed_calls: &'a [ObservedSequenceCall], + unit_bindings: &UnitBindings, +) -> Vec> { + observed_calls + .iter() + .map(|call| { + let caller_interfaces = all_interfaces_for_alias(unit_bindings, &call.caller); + let callee_interfaces = all_interfaces_for_alias(unit_bindings, &call.callee); + + SequenceCallContext { + caller_unit: call.caller.as_str(), + callee_unit: call.callee.as_str(), + method: call.method.as_str(), + caller_interfaces, + callee_interfaces, + } + }) + .collect() +} diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs new file mode 100644 index 00000000..0c2ba682 --- /dev/null +++ b/validation/core/src/validators/shared/helpers.rs @@ -0,0 +1,50 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Helper functions shared by validators. + +use std::collections::BTreeSet; + +pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> String { + if names.is_empty() { + return "".to_string(); + } + + names + .iter() + .map(|name| format!("\"{name}\"")) + .collect::>() + .join(", ") +} + +pub(in crate::validators) fn format_sequence_call( + caller_unit: &str, + callee_unit: &str, + method_name: &str, +) -> String { + format!("\"{caller_unit}\" -> \"{callee_unit}\" : \"{method_name}\"") +} + +pub(in crate::validators) fn extract_method_name(method: &str) -> &str { + method.split('(').next().unwrap_or(method).trim() +} + +pub(in crate::validators) fn intersect_interfaces( + left_interfaces: &BTreeSet, + right_interfaces: &BTreeSet, +) -> BTreeSet { + left_interfaces + .intersection(right_interfaces) + .cloned() + .collect() +} diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs new file mode 100644 index 00000000..1eca4cad --- /dev/null +++ b/validation/core/src/validators/shared/mod.rs @@ -0,0 +1,25 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Shared validator analysis and helper utilities. + +mod diagram_analysis; +mod helpers; + +pub(in crate::validators) use diagram_analysis::{ + build_observed_call_contexts, build_unit_bindings, SequenceCallContext, UnitBindings, + UnitInterfaces, +}; +pub(in crate::validators) use helpers::{ + extract_method_name, format_name_list, format_sequence_call, intersect_interfaces, +}; diff --git a/validation/core/src/validators/test/component_internal_api_validator_test.rs b/validation/core/src/validators/test/component_internal_api_validator_test.rs new file mode 100644 index 00000000..5f84e7a1 --- /dev/null +++ b/validation/core/src/validators/test/component_internal_api_validator_test.rs @@ -0,0 +1,136 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::super::fixtures::*; +use super::*; +use crate::models::ComponentDiagramInputs; +use crate::ValidationResult; + +fn validate( + component_diagrams: ComponentDiagramInputs, + internal_api: &InternalApiIndex, +) -> ValidationResult { + let mut setup_result = ValidationResult::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); + + validate_component_internal_api(&component_arch, internal_api) +} + +#[test] +fn reports_missing_component_interface_declared_by_internal_api() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface\"")); + assert!(!validation_result.failures[0].contains("Unit :")); +} + +#[test] +fn reports_each_missing_component_interface_once() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface", "InternalInterface1"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + interface("InternalInterface1"), + ]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface1\"")); +} + +#[test] +fn reports_missing_component_interface_even_without_unit_relation() { + let component_diagrams = + component_diagrams_with_entities(vec![unit("u1", &[]), interface("UnusedInterface")]); + let internal_api = internal_api_index(vec![]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"UnusedInterface\"")); +} + +#[test] +fn reports_all_missing_component_interfaces_in_one_message() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface", "InternalInterface1"]), + unit("u2", &["InternalInterface"]), + unit("u3", &["InternalInterface"]), + interface("InternalInterface"), + interface("InternalInterface1"), + ]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface1\"")); +} + +#[test] +fn reports_missing_component_interface_without_sequence_method_call() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface\"")); +} + +#[test] +fn reports_case_mismatch_between_component_and_internal_api_interface_names() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let internal_api = internal_api_index(vec![("internalinterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing internal API interface")); + assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface\"")); +} + +#[test] +fn matches_internal_api_by_component_interface_id_when_alias_differs() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["pkg.InternalInterface"]), + unit("u2", &["pkg.InternalInterface"]), + interface_with_id("pkg.InternalInterface", "InternalInterface"), + ]); + let internal_api = internal_api_index(vec![("pkg.InternalInterface", vec!["GetData"])]); + + let validation_result = validate(component_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} diff --git a/validation/core/src/validators/test/component_sequence_validator_test.rs b/validation/core/src/validators/test/component_sequence_validator_test.rs index 42bac4f8..8122ace4 100644 --- a/validation/core/src/validators/test/component_sequence_validator_test.rs +++ b/validation/core/src/validators/test/component_sequence_validator_test.rs @@ -10,171 +10,21 @@ // // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use super::super::fixtures::*; use super::*; -use crate::models::{ - ComponentDiagramElementType, ComponentDiagramInput, ComponentDiagramInputs, - ComponentDiagramRelation, SequenceDiagramInput, SequenceDiagramInputs, -}; -use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; -use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; - -fn relation_with_role(target: &str, source_role: &str) -> ComponentDiagramRelation { - ComponentDiagramRelation { - target: target.to_string(), - annotation: None, - relation_type: Some("InterfaceBinding".to_string()), - source_role: Some(source_role.to_string()), - } -} - -fn required_relation(target: &str) -> ComponentDiagramRelation { - relation_with_role(target, "Required") -} - -fn provided_relation(target: &str) -> ComponentDiagramRelation { - relation_with_role(target, "Provided") -} - -fn unit(alias: &str, interface_targets: &[&str]) -> ComponentDiagramInput { - unit_with_interface_roles(alias, interface_targets, interface_targets) -} - -fn unit_with_interface_roles( - alias: &str, - required_interfaces: &[&str], - provided_interfaces: &[&str], -) -> ComponentDiagramInput { - let mut relations = Vec::new(); - for target in required_interfaces { - relations.push(required_relation(target)); - } - for target in provided_interfaces { - relations.push(provided_relation(target)); - } - - unit_with_relations(alias, relations) -} - -fn unit_with_relations( - alias: &str, - relations: Vec, -) -> ComponentDiagramInput { - ComponentDiagramInput { - id: format!("some_id.{alias}"), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentDiagramElementType::Component, - stereotype: Some("unit".to_string()), - relations, - } -} - -fn interface(alias: &str) -> ComponentDiagramInput { - ComponentDiagramInput { - id: alias.to_string(), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentDiagramElementType::Interface, - stereotype: None, - relations: Vec::new(), - } -} - -fn interface_with_id(id: &str, alias: &str) -> ComponentDiagramInput { - ComponentDiagramInput { - id: id.to_string(), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentDiagramElementType::Interface, - stereotype: None, - relations: Vec::new(), - } -} - -fn component_diagrams(aliases: &[&str]) -> ComponentDiagramInputs { - ComponentDiagramInputs { - entities: aliases.iter().map(|alias| unit(alias, &[])).collect(), - } -} - -fn component_diagrams_with_entities( - entities: Vec, -) -> ComponentDiagramInputs { - ComponentDiagramInputs { entities } -} - -fn method(name: &str) -> Method { - Method { - name: name.to_string(), - return_type: None, - visibility: Visibility::Public, - parameters: Vec::new(), - template_parameters: None, - modifiers: Vec::new(), - } -} - -fn internal_api_index(interfaces: Vec<(&str, Vec<&str>)>) -> InternalApiIndex { - let diagrams = vec![ClassDiagram { - name: "internal_api".to_string(), - entities: interfaces - .into_iter() - .map(|(interface_name, methods)| SimpleEntity { - id: interface_name.to_string(), - name: interface_name.to_string(), - enclosing_namespace_id: None, - entity_type: EntityType::Interface, - type_aliases: Vec::new(), - variables: Vec::new(), - methods: methods.into_iter().map(method).collect(), - template_parameters: None, - enum_literals: Vec::new(), - relationships: Vec::new(), - source_file: None, - source_line: None, - }) - .collect(), - relationships: Vec::new(), - source_files: Vec::new(), - version: None, - }]; +use crate::models::{ComponentDiagramInputs, ComponentType, LogicComponent, SequenceDiagramInputs}; +use crate::ValidationResult; +fn validate( + component_diagrams: ComponentDiagramInputs, + sequence_diagrams: SequenceDiagramInputs, +) -> ValidationResult { let mut setup_result = ValidationResult::default(); - let index = InternalApiIndex::build_index(&diagrams, &mut setup_result); + let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); assert!(setup_result.is_empty()); - index -} -fn sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { - sequence_calls( - &participants - .iter() - .map(|participant| (*participant, *participant, "")) - .collect::>(), - ) -} - -fn sequence_calls(calls: &[(&str, &str, &str)]) -> SequenceDiagramInputs { - SequenceDiagramInputs { - diagrams: vec![SequenceDiagramInput { - tree: SequenceTree { - name: Some("seq".to_string()), - root_interactions: calls - .iter() - .map(|(caller, callee, method)| SequenceNode { - event: Event::Interaction(Interaction { - caller: (*caller).to_string(), - callee: (*callee).to_string(), - method: (*method).to_string(), - }), - branches_node: Vec::new(), - }) - .collect(), - }, - source_files: Vec::new(), - version: None, - }], - } + validate_component_sequence(&component_arch, &sequence_index) } #[test] @@ -182,12 +32,7 @@ fn passes_when_aliases_and_participants_are_identical() { let component_diagrams = component_diagrams(&["unit_1", "unit_2"]); let sequence_diagrams = sequence_diagrams(&["unit_1", "unit_2"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(validation_result.is_empty()); } @@ -196,12 +41,7 @@ fn reports_missing_and_extra() { let component_diagrams = component_diagrams(&["unit_1", "unit_2", "unit_3"]); let sequence_diagrams = sequence_diagrams(&["unit_2", "unit_4"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(!validation_result.is_empty()); assert_eq!(validation_result.failures.len(), 3); @@ -224,23 +64,19 @@ fn reports_missing_and_extra() { #[test] fn units_without_alias_are_ignored() { let component_diagrams = ComponentDiagramInputs { - entities: vec![ComponentDiagramInput { + entities: vec![LogicComponent { id: "module_a.unit_1".to_string(), + name: Some("unit_1".to_string()), alias: None, parent_id: None, - element_type: ComponentDiagramElementType::Component, + element_type: ComponentType::Component, stereotype: Some("unit".to_string()), relations: Vec::new(), }], }; let sequence_diagrams = sequence_diagrams(&[]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(validation_result.is_empty()); } @@ -249,12 +85,7 @@ fn reports_alias_missing_from_participants() { let component_diagrams = component_diagrams(&["u1", "u2"]); let sequence_diagrams = sequence_diagrams(&["u1"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"u2\"")); } @@ -264,12 +95,7 @@ fn reports_participant_not_in_aliases() { let component_diagrams = component_diagrams(&["u1"]); let sequence_diagrams = sequence_diagrams(&["u1", "orphan"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"orphan\"")); } @@ -282,12 +108,7 @@ fn reports_missing_component_alias_and_interface_connection_for_sequence_call() ]); let sequence_diagrams = sequence_calls(&[("u1", "orphan", "GetData()")]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -312,12 +133,7 @@ fn reports_missing_sequence_call_for_interface_connected_units() { ]); let sequence_diagrams = sequence_diagrams(&["u1", "u2"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -334,12 +150,7 @@ fn reports_missing_participant_and_missing_sequence_call_for_interface_connected ]); let sequence_diagrams = sequence_diagrams(&["u1"]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -363,12 +174,7 @@ fn reports_sequence_call_without_corresponding_shared_interface_connection() { ]); let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -386,614 +192,6 @@ fn passes_when_interface_connected_units_have_sequence_call() { ]); let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); - assert!(validation_result.is_empty()); -} - -#[test] -fn passes_when_sequence_call_matches_consumer_provider_roles() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit_with_interface_roles("u1", &["InternalInterface"], &[]), - unit_with_interface_roles("u2", &[], &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); - assert!(validation_result.is_empty()); -} - -#[test] -fn reports_cross_unit_sequence_call_with_invalid_consumer_provider_roles() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit_with_interface_roles("u1", &[], &["InternalInterface"]), - unit_with_interface_roles("u2", &["InternalInterface"], &[]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = validate_component_sequence(&component_arch, &sequence_index, None); - - assert_eq!(validation_result.failures.len(), 1); - assert!(validation_result.failures[0] - .contains("sequence interaction does not match consumer/provider roles")); - assert!(validation_result.failures[0] - .contains("Sequence call : \"u1\" -> \"u2\" : \"GetData()\"")); - assert!(validation_result.failures[0].contains( - "Expected caller role: \"u1\" should require shared interface(s) \"InternalInterface\"" - )); - assert!(validation_result.failures[0].contains( - "Expected callee role: \"u2\" should provide shared interface(s) \"InternalInterface\"" - )); -} - -#[test] -fn reports_sequence_function_missing_from_related_interface_methods() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence function name was not found in the related interface methods") - && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("internal API interface functions are not exercised in sequence diagrams") - && message.contains("\"InternalInterface\"") - && message.contains("\"OtherMethod\"") - })); -} - -#[test] -fn reports_interface_function_not_exercised_in_sequence_diagrams() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 1); - assert!(validation_result.failures[0] - .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(validation_result.failures[0].contains("\"InternalInterface\"")); - assert!(validation_result.failures[0].contains("\"SetData\"")); -} - -#[test] -fn reports_unreferenced_internal_api_interface_function_not_exercised_without_self_calls() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![ - ("InternalInterface", vec!["GetData"]), - ("OtherInterface", vec!["SetData"]), - ]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 1); - assert!(validation_result.failures[0] - .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(validation_result.failures[0].contains("\"OtherInterface\"")); - assert!(validation_result.failures[0].contains("\"SetData\"")); -} - -#[test] -fn reports_missing_internal_api_interface_for_related_interfaces() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u2\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); -} - -#[test] -fn reports_missing_internal_api_interface_for_caller_only() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface", "InternalInterface1"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - interface("InternalInterface1"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 1); - assert!(validation_result.failures[0].contains("Missing internal API interface")); - assert!(validation_result.failures[0].contains("Unit : \"u1\"")); - assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface1\"")); -} - -#[test] -fn reports_missing_internal_api_interface_for_callee_only() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface", "InternalInterface1"]), - interface("InternalInterface"), - interface("InternalInterface1"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 1); - assert!(validation_result.failures[0].contains("Missing internal API interface")); - assert!(validation_result.failures[0].contains("Unit : \"u2\"")); - assert!(validation_result.failures[0].contains("Missing interfaces : \"InternalInterface1\"")); -} - -#[test] -fn reports_missing_internal_api_interface_for_unit_only_once_across_call_roles() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface", "InternalInterface1"]), - unit("u2", &["InternalInterface"]), - unit("u3", &["InternalInterface"]), - interface("InternalInterface"), - interface("InternalInterface1"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()"), ("u3", "u1", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - let missing_internal_api_errors: Vec<&String> = validation_result - .failures - .iter() - .filter(|message| message.contains("Missing internal API interface")) - .collect(); - - assert_eq!(missing_internal_api_errors.len(), 1); - assert!(missing_internal_api_errors[0].contains("Unit : \"u1\"")); - assert!(missing_internal_api_errors[0].contains("Missing interfaces : \"InternalInterface1\"")); -} - -#[test] -fn reports_missing_internal_api_interface_without_sequence_method_call() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_diagrams(&["u1"]); - let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - let missing_internal_api_errors: Vec<&String> = validation_result - .failures - .iter() - .filter(|message| message.contains("Missing internal API interface")) - .collect(); - - assert_eq!(missing_internal_api_errors.len(), 1); - assert!(missing_internal_api_errors[0].contains("Unit : \"u1\"")); - assert!(missing_internal_api_errors[0].contains("Missing interfaces : \"InternalInterface\"")); -} - -#[test] -fn reports_missing_component_alias_for_sequence_method_validation() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "orphan", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence participant not found in component unit aliases") - && message.contains("\"orphan\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message - .contains("sequence-connected units have no corresponding shared interface connection") - && message.contains("\"u1\"") - && message.contains("\"orphan\"") - })); -} - -#[test] -fn reports_self_call_method_mismatch_even_when_unit_has_missing_internal_api_interface() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["MissingInterface"]), - interface("MissingInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); - let internal_api = internal_api_index(vec![]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"MissingInterface\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence self-call function name was not found") - && message.contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"") - })); -} - -#[test] -fn passes_when_sequence_function_exists_on_related_interface() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert!(validation_result.is_empty()); -} - -#[test] -fn reports_self_call_function_missing_from_available_interfaces() { - let component_diagrams = component_diagrams(&["u1"]); - let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence self-call function name was not found") - && message.contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("internal API interface functions are not exercised in sequence diagrams") - && message.contains("\"InternalInterface\"") - && message.contains("\"OtherMethod\"") - })); -} - -#[test] -fn passes_when_self_call_uses_internal_api_interface_without_component_interfaces() { - let component_diagrams = component_diagrams(&["u1"]); - let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert!(validation_result.is_empty()); -} - -#[test] -fn passes_when_all_interface_functions_are_exercised_by_self_calls() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()"), ("u1", "u1", "SetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert!(validation_result.is_empty()); -} - -#[test] -fn reports_self_call_without_any_available_interfaces() { - let component_diagrams = component_diagrams(&["u1"]); - let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); - let internal_api = internal_api_index(vec![]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 1); - assert!( - validation_result.failures[0].contains("sequence self-call function name was not found") - ); - assert!(validation_result.failures[0] - .contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"")); -} - -#[test] -fn reports_method_declared_only_on_caller_side_interfaces() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["SharedInterface", "CallerOnlyInterface"]), - unit("u2", &["SharedInterface"]), - interface("SharedInterface"), - interface("CallerOnlyInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![ - ("SharedInterface", vec!["OtherMethod"]), - ("CallerOnlyInterface", vec!["GetData"]), - ]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence function name was not found in the related interface methods") - && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("internal API interface functions are not exercised in sequence diagrams") - && message.contains("\"SharedInterface\"") - && message.contains("\"OtherMethod\"") - })); - assert!(validation_result - .failures - .iter() - .all(|message| !message.contains("Missing functions : \"GetData\""))); -} - -#[test] -fn reports_method_declared_only_on_callee_side_interfaces() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["SharedInterface"]), - unit("u2", &["SharedInterface", "CalleeOnlyInterface"]), - interface("SharedInterface"), - interface("CalleeOnlyInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![ - ("SharedInterface", vec!["OtherMethod"]), - ("CalleeOnlyInterface", vec!["GetData"]), - ]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence function name was not found in the related interface methods") - && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("internal API interface functions are not exercised in sequence diagrams") - && message.contains("\"SharedInterface\"") - && message.contains("\"OtherMethod\"") - })); - assert!(validation_result - .failures - .iter() - .all(|message| !message.contains("Missing functions : \"GetData\""))); -} - -#[test] -fn reports_method_declared_on_both_sides_but_not_on_shared_interface() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["SharedInterface", "CallerOnlyInterface"]), - unit("u2", &["SharedInterface", "CalleeOnlyInterface"]), - interface("SharedInterface"), - interface("CallerOnlyInterface"), - interface("CalleeOnlyInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![ - ("SharedInterface", vec!["OtherMethod"]), - ("CallerOnlyInterface", vec!["GetData"]), - ("CalleeOnlyInterface", vec!["GetData"]), - ]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("sequence function name was not found in the related interface methods") - && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("internal API interface functions are not exercised in sequence diagrams") - && message.contains("\"SharedInterface\"") - && message.contains("\"OtherMethod\"") - })); - assert!(validation_result - .failures - .iter() - .all(|message| !message.contains("Missing functions : \"GetData\""))); -} - -#[test] -fn reports_case_mismatch_between_component_and_internal_api_interface_names() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("internalinterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - - assert_eq!(validation_result.failures.len(), 2); - assert!(validation_result.failures.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); - assert!(validation_result.failures.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u2\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); -} - -#[test] -fn matches_internal_api_by_component_interface_id_when_alias_differs() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["pkg.InternalInterface"]), - unit("u2", &["pkg.InternalInterface"]), - interface_with_id("pkg.InternalInterface", "InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("pkg.InternalInterface", vec!["GetData"])]); - - let mut setup_result = ValidationResult::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); - assert!(setup_result.is_empty()); - - let validation_result = - validate_component_sequence(&component_arch, &sequence_index, Some(&internal_api)); - + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(validation_result.is_empty()); } diff --git a/validation/core/src/validators/test/fixtures.rs b/validation/core/src/validators/test/fixtures.rs new file mode 100644 index 00000000..4ea7bdf2 --- /dev/null +++ b/validation/core/src/validators/test/fixtures.rs @@ -0,0 +1,167 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::models::{ + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, InternalApiIndex, + LogicComponent, LogicRelation, SequenceDiagramInputs, +}; +use crate::ValidationResult; +use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; +use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; + +pub(super) fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { + relation_with_type_and_role(target, ComponentRelationType::InterfaceBinding, source_role) +} + +pub(super) fn relation_with_type_and_role( + target: &str, + relation_type: ComponentRelationType, + source_role: EndpointRole, +) -> LogicRelation { + LogicRelation { + target: target.to_string(), + annotation: None, + relation_type, + source_role, + } +} + +pub(super) fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { + unit_with_interface_roles(alias, interface_targets, interface_targets) +} + +pub(super) fn unit_with_interface_roles( + alias: &str, + required_interfaces: &[&str], + provided_interfaces: &[&str], +) -> LogicComponent { + let mut relations = Vec::new(); + for target in required_interfaces { + relations.push(relation_with_role(target, EndpointRole::Required)); + } + for target in provided_interfaces { + relations.push(relation_with_role(target, EndpointRole::Provided)); + } + + LogicComponent { + id: format!("some_id.{alias}"), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: None, + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations, + } +} + +pub(super) fn interface(alias: &str) -> LogicComponent { + interface_with_id(alias, alias) +} + +pub(super) fn interface_with_id(id: &str, alias: &str) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: None, + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + } +} + +pub(super) fn component_diagrams(aliases: &[&str]) -> ComponentDiagramInputs { + ComponentDiagramInputs { + entities: aliases.iter().map(|alias| unit(alias, &[])).collect(), + } +} + +pub(super) fn component_diagrams_with_entities( + entities: Vec, +) -> ComponentDiagramInputs { + ComponentDiagramInputs { entities } +} + +pub(super) fn sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { + sequence_calls( + &participants + .iter() + .map(|participant| (*participant, *participant, "")) + .collect::>(), + ) +} + +pub(super) fn sequence_calls(calls: &[(&str, &str, &str)]) -> SequenceDiagramInputs { + SequenceDiagramInputs { + diagrams: vec![SequenceTree { + name: Some("seq".to_string()), + root_interactions: calls + .iter() + .map(|(caller, callee, method)| SequenceNode { + event: Event::Interaction(Interaction { + caller: (*caller).to_string(), + callee: (*callee).to_string(), + method: (*method).to_string(), + }), + branches_node: Vec::new(), + }) + .collect(), + }], + } +} + +pub(super) fn internal_api_index(interfaces: Vec<(&str, Vec<&str>)>) -> InternalApiIndex { + let diagrams = vec![ClassDiagram { + name: "internal_api".to_string(), + entities: interfaces + .into_iter() + .map(|(interface_name, methods)| SimpleEntity { + id: interface_name.to_string(), + name: interface_name.to_string(), + enclosing_namespace_id: None, + entity_type: EntityType::Interface, + type_aliases: Vec::new(), + variables: Vec::new(), + methods: methods.into_iter().map(method).collect(), + template_parameters: None, + enum_literals: Vec::new(), + relationships: Vec::new(), + source_file: None, + source_line: None, + }) + .collect(), + relationships: Vec::new(), + source_files: Vec::new(), + version: None, + }]; + + let mut setup_result = ValidationResult::default(); + let index = InternalApiIndex::build_index(&diagrams, &mut setup_result); + assert!( + setup_result.is_empty(), + "test fixture construction failed: {:?}", + setup_result.failures + ); + index +} + +fn method(name: &str) -> Method { + Method { + name: name.to_string(), + return_type: None, + visibility: Visibility::Public, + parameters: Vec::new(), + template_parameters: None, + modifiers: Vec::new(), + } +} diff --git a/validation/core/src/validators/test/sequence_internal_api_validator_test.rs b/validation/core/src/validators/test/sequence_internal_api_validator_test.rs new file mode 100644 index 00000000..dde14262 --- /dev/null +++ b/validation/core/src/validators/test/sequence_internal_api_validator_test.rs @@ -0,0 +1,451 @@ +// ******************************************************************************* +// Copyright (c) 2026 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use super::super::fixtures::*; +use super::*; +use crate::models::{ + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, + SequenceDiagramInputs, +}; +use crate::ValidationResult; + +fn validate( + sequence_diagrams: SequenceDiagramInputs, + internal_api: &InternalApiIndex, +) -> ValidationResult { + let mut setup_result = ValidationResult::default(); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); + + validate_sequence_internal_api(&sequence_index, internal_api, None) +} + +fn validate_with_component_context( + component_diagrams: ComponentDiagramInputs, + sequence_diagrams: SequenceDiagramInputs, + internal_api: &InternalApiIndex, +) -> ValidationResult { + let mut setup_result = ValidationResult::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut setup_result); + + validate_sequence_internal_api(&sequence_index, internal_api, Some(&component_arch)) +} + +fn unit_with_non_binding_interface(alias: &str, interface_id: &str) -> LogicComponent { + LogicComponent { + id: format!("some_id.{alias}"), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: None, + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations: vec![relation_with_type_and_role( + interface_id, + ComponentRelationType::Dependency, + EndpointRole::None, + )], + } +} + +#[test] +fn does_not_check_sequence_method_names_without_component_context() { + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec![])]); + + let validation_result = validate(sequence_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_internal_api_interface_function_not_exercised_without_method_name_check() { + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); + + let validation_result = validate(sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(validation_result.failures[0].contains("\"InternalInterface\"")); + assert!(validation_result.failures[0].contains("\"OtherMethod\"")); + assert!(validation_result + .failures + .iter() + .all(|message| !message.contains("Method consistency failure"))); +} + +#[test] +fn reports_internal_api_interface_function_not_exercised() { + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); + + let validation_result = validate(sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(validation_result.failures[0].contains("\"InternalInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); +} + +#[test] +fn self_calls_count_as_internal_api_method_usage() { + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = validate(sequence_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_sequence_function_missing_from_available_interfaces_with_component_context() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found in available interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(validation_result.failures.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"InternalInterface\"") + && message.contains("\"OtherMethod\"") + })); +} + +#[test] +fn reports_sequence_function_missing_when_shared_interface_has_no_direction_roles() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit_with_non_binding_interface("u1", "InternalInterface"), + unit_with_non_binding_interface("u2", "InternalInterface"), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found in available interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(validation_result + .failures + .iter() + .all(|message| !message.contains("consumer/provider roles"))); + assert!(validation_result.failures.iter().all(|message| { + !message.contains("sequence function name was not found in the related interface methods") + })); +} + +#[test] +fn reports_interface_function_not_exercised_in_sequence_diagrams_with_component_context() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(validation_result.failures[0].contains("\"InternalInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); +} + +#[test] +fn reports_unreferenced_internal_api_interface_function_not_exercised_without_self_calls() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![ + ("InternalInterface", vec!["GetData"]), + ("OtherInterface", vec!["SetData"]), + ]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(validation_result.failures[0].contains("\"OtherInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); +} + +#[test] +fn reports_self_call_method_mismatch_when_unit_has_missing_internal_api_interface() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["MissingInterface"]), + interface("MissingInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); + let internal_api = internal_api_index(vec![]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found") + && message.contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"") + })); +} + +#[test] +fn passes_when_sequence_function_exists_on_related_interface_with_component_context() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + unit("u2", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_self_call_function_missing_from_available_interfaces() { + let component_diagrams = component_diagrams(&["u1"]); + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found") + && message.contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"") + })); + assert!(validation_result.failures.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"InternalInterface\"") + && message.contains("\"OtherMethod\"") + })); +} + +#[test] +fn passes_when_self_call_uses_internal_api_interface_without_component_interfaces() { + let component_diagrams = component_diagrams(&["u1"]); + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn passes_when_all_interface_functions_are_exercised_by_self_calls() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()"), ("u1", "u1", "SetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_self_call_without_any_available_interfaces() { + let component_diagrams = component_diagrams(&["u1"]); + let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); + let internal_api = internal_api_index(vec![]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("sequence function name was not found")); + assert!(validation_result.failures[0] + .contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"")); +} + +#[test] +fn reports_method_declared_only_on_caller_side_interfaces() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["SharedInterface", "CallerOnlyInterface"]), + unit("u2", &["SharedInterface"]), + interface("SharedInterface"), + interface("CallerOnlyInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![ + ("SharedInterface", vec!["OtherMethod"]), + ("CallerOnlyInterface", vec!["GetData"]), + ]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(validation_result.failures.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(validation_result + .failures + .iter() + .all(|message| !message.contains("Missing functions : \"GetData\""))); +} + +#[test] +fn reports_method_declared_only_on_callee_side_interfaces() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["SharedInterface"]), + unit("u2", &["SharedInterface", "CalleeOnlyInterface"]), + interface("SharedInterface"), + interface("CalleeOnlyInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![ + ("SharedInterface", vec!["OtherMethod"]), + ("CalleeOnlyInterface", vec!["GetData"]), + ]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(validation_result.failures.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(validation_result + .failures + .iter() + .all(|message| !message.contains("Missing functions : \"GetData\""))); +} + +#[test] +fn reports_method_declared_on_both_sides_but_not_on_shared_interface() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit("u1", &["SharedInterface", "CallerOnlyInterface"]), + unit("u2", &["SharedInterface", "CalleeOnlyInterface"]), + interface("SharedInterface"), + interface("CallerOnlyInterface"), + interface("CalleeOnlyInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![ + ("SharedInterface", vec!["OtherMethod"]), + ("CallerOnlyInterface", vec!["GetData"]), + ("CalleeOnlyInterface", vec!["GetData"]), + ]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 2); + assert!(validation_result.failures.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(validation_result.failures.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(validation_result + .failures + .iter() + .all(|message| !message.contains("Missing functions : \"GetData\""))); +} + +#[test] +fn reports_role_violation_when_method_exists_only_on_reverse_direction_interface() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit_with_interface_roles("u1", &[], &["InternalInterface"]), + unit_with_interface_roles("u2", &["InternalInterface"], &[]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("sequence interaction does not match consumer/provider roles")); + assert!(validation_result.failures[0] + .contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"")); + assert!(validation_result.failures[0].contains( + "Expected caller role: \"u1\" should require shared interface(s) \"InternalInterface\"" + )); + assert!(validation_result.failures[0].contains( + "Expected callee role: \"u2\" should provide shared interface(s) \"InternalInterface\"" + )); + assert!(!validation_result.failures[0].contains("Method consistency failure")); +} + +#[test] +fn passes_when_method_interface_matches_call_direction_roles() { + let component_diagrams = component_diagrams_with_entities(vec![ + unit_with_interface_roles("u1", &["InternalInterface"], &[]), + unit_with_interface_roles("u2", &[], &["InternalInterface"]), + interface("InternalInterface"), + ]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); + let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); + + let validation_result = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!( + validation_result.failures.is_empty(), + "Expected pass, got: {:?}", + validation_result.failures + ); +}