From 0a99e4ad51ba85b736127f0384650410f7d9f88f Mon Sep 17 00:00:00 2001 From: Xian Gu Date: Wed, 24 Jun 2026 17:34:39 +0800 Subject: [PATCH 1/3] split component sequence validator --- bazel/rules/rules_score/BUILD | 2 +- bazel/rules/rules_score/docs/index.rst | 2 + validation/core/BUILD | 6 + validation/core/README.md | 17 +- .../docs/assets/validation_core_flow.puml | 28 +- .../docs/assets/validation_core_overview.puml | 17 +- .../docs/requirements/tool_requirements.trlc | 44 +- .../specifications/component_internal_api.md | 63 ++ .../docs/specifications/component_sequence.md | 58 +- .../specifications/sequence_internal_api.md | 102 +++ .../src/models/component_diagram_models.rs | 107 ++- validation/core/src/models/mod.rs | 6 +- .../src/models/sequence_diagram_models.rs | 61 +- .../core/src/profiles/architectural_design.rs | 27 +- .../src/readers/component_diagram_reader.rs | 49 +- .../src/readers/sequence_diagram_reader.rs | 17 +- .../validators/bazel_component_validator.rs | 14 +- .../component_internal_api_validator.rs | 130 +++ .../component_sequence_validator.rs | 390 ++------- validation/core/src/validators/mod.rs | 4 + .../sequence_internal_api_validator.rs | 461 +++++++++++ .../component_internal_api_validator_test.rs | 232 ++++++ .../test/component_sequence_validator_test.rs | 759 +++--------------- .../sequence_internal_api_validator_test.rs | 477 +++++++++++ 24 files changed, 1844 insertions(+), 1229 deletions(-) create mode 100644 validation/core/docs/specifications/component_internal_api.md create mode 100644 validation/core/docs/specifications/sequence_internal_api.md create mode 100644 validation/core/src/validators/component_internal_api_validator.rs create mode 100644 validation/core/src/validators/sequence_internal_api_validator.rs create mode 100644 validation/core/src/validators/test/component_internal_api_validator_test.rs create mode 100644 validation/core/src/validators/test/sequence_internal_api_validator_test.rs 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..fdaf1510 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,13 @@ 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/test/component_internal_api_validator_test.rs", "src/validators/test/component_sequence_validator_test.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..c820540e 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -98,21 +98,47 @@ section "Tool Requirements" { satisfied_by = Verifier } + } + + section "Sequence Internal API Validator" { + 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.''' + 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 SequenceInternalApiInterfaceCoverage { + description = '''When sequence and internal API diagrams are + provided, the validator shall report an error when a function + declared in an internal API interface is never called in any + sequence interaction. Self-calls count as valid usage.''' 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.''' + 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..53b14e1f 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 @@ -69,52 +66,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 | @@ -124,9 +75,6 @@ unit_1 -> unit_2 : SetData(d) | 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 +85,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..4aaed6f1 --- /dev/null +++ b/validation/core/docs/specifications/sequence_internal_api.md @@ -0,0 +1,102 @@ + + +# 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 is checked only when component context is available. +Without component context, the validator does not run a weak global method-name +existence check. + +### Related Interface Method-Name Consistency With Component Context + +When component context is available, 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 component or Internal API +interfaces. +*(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 an Internal API interface must be called in at least +one sequence interaction. Self-calls count as valid usage. +*(Requirement: {requirement:downstream-ref}`Tools.SequenceInternalApiInterfaceCoverage`)* +*(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 | +|---|---| +| Method not declared in related interface | Related Interface Method-Name Consistency With Component Context | +| 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/src/models/component_diagram_models.rs b/validation/core/src/models/component_diagram_models.rs index 3664c2df..0f09aa47 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -13,59 +13,45 @@ use std::collections::BTreeMap; +pub use component_diagram::{ + ComponentRelationType, ComponentType, EndpointRole, LogicComponent, LogicRelation, +}; use super::EntityKey; use crate::ValidationResult; -/// 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")], ), 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..01d73d26 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -16,7 +16,9 @@ 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; @@ -52,8 +54,27 @@ pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { 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()), + &mut results, + validate_component_sequence(component, sequence, Errors::default()), + ); + ran_validator = true; + } + if let (Some(component), Some(internal_api)) = (component.as_ref(), internal_api.as_ref()) { + merge_results( + &mut results, + validate_component_internal_api(component, internal_api, Errors::default()), + ); + ran_validator = true; + } + if let (Some(sequence), Some(internal_api)) = (sequence.as_ref(), internal_api.as_ref()) { + merge_results( + &mut results, + validate_sequence_internal_api( + sequence, + internal_api, + component.as_ref(), + Errors::default(), + ), ); ran_validator = true; } 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..41e3a601 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 } } 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..a217566b --- /dev/null +++ b/validation/core/src/validators/component_internal_api_validator.rs @@ -0,0 +1,130 @@ +// ******************************************************************************* +// 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 crate::models::{ComponentDiagramArchitecture, Errors, InternalApiIndex, LogicComponentExt}; + +/// Run component-vs-internal-API interface reference validation. +pub fn validate_component_internal_api( + component_diagram: &ComponentDiagramArchitecture, + internal_api_diagram: &InternalApiIndex, + errors: Errors, +) -> Errors { + ComponentInternalApiValidator::new(component_diagram, internal_api_diagram, errors).run() +} + +struct ComponentInternalApiValidator { + component_interface_ids: BTreeSet, + internal_api_interface_ids: BTreeSet, + errors: Errors, +} + +impl ComponentInternalApiValidator { + fn new( + component_diagram: &ComponentDiagramArchitecture, + internal_api_diagram: &InternalApiIndex, + errors: Errors, + ) -> Self { + Self { + component_interface_ids: collect_component_interface_ids(component_diagram), + internal_api_interface_ids: collect_internal_api_interface_ids(internal_api_diagram), + errors, + } + } + + fn run(mut self) -> Errors { + self.errors.debug_output = self.build_debug_log(); + self.check_component_interfaces_declared_by_internal_api(); + self.errors + } + + fn build_debug_log(&self) -> String { + let mut log = String::new(); + + log.push_str("DEBUG: Component interfaces checked against internal API:\n"); + for interface_id in &self.component_interface_ids { + log.push_str(&format!(" {interface_id}\n")); + } + + log.push_str("DEBUG: Internal API interfaces available for component interfaces:\n"); + for interface_id in &self.internal_api_interface_ids { + log.push_str(&format!(" {interface_id}\n")); + } + + log + } + + 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.errors + .push(format_missing_internal_api_interface_error( + &missing_interfaces, + )); + } + } +} + +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 violation: 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), + ) +} + +fn format_name_list(names: &BTreeSet) -> String { + if names.is_empty() { + return "".to_string(); + } + + names + .iter() + .map(|name| format!("\"{name}\"")) + .collect::>() + .join(", ") +} + +#[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..063e3e7a 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -17,7 +17,8 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::models::{ - ComponentDiagramArchitecture, InternalApiIndex, InternalApiInterface, SequenceDiagramIndex, + ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, LogicComponentExt, + SequenceDiagramIndex, }; use crate::{Diagnostics, ValidationResult}; @@ -25,9 +26,8 @@ use crate::{Diagnostics, ValidationResult}; 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>; @@ -38,8 +38,6 @@ struct ComponentSequenceValidator<'a> { observed_call_contexts: Vec>, connected_unit_pairs: ConnectedUnitPairs, unit_bindings: BTreeMap, - all_interfaces: BTreeSet, - internal_api_interfaces_by_id: Option>, result: ValidationResult, } @@ -100,11 +98,8 @@ 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 +108,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(), } } @@ -140,8 +131,6 @@ impl<'a> ComponentSequenceValidator<'a> { 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 +173,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,8 +219,8 @@ 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), + left_interfaces = format_name_list(left_interfaces), + right_interfaces = format_name_list(right_interfaces), )); } } @@ -244,15 +233,16 @@ impl<'a> ComponentSequenceValidator<'a> { continue; } - if !self.unit_bindings.contains_key(call_context.caller_unit) - || !self.unit_bindings.contains_key(call_context.callee_unit) - { + if call_context.caller_unit == call_context.callee_unit { continue; } - if call_context.caller_unit == call_context.callee_unit { + let Some(caller_bindings) = self.unit_bindings.get(call_context.caller_unit) else { continue; - } + }; + let Some(callee_bindings) = self.unit_bindings.get(call_context.callee_unit) else { + continue; + }; if !seen_interactions.insert(( call_context.caller_unit.to_string(), @@ -261,15 +251,18 @@ impl<'a> ComponentSequenceValidator<'a> { continue; } - let caller_bindings = - unit_bindings_for_alias(&self.unit_bindings, call_context.caller_unit); - if !call_context.has_shared_interfaces() { continue; } + let role_related_interfaces = intersect_interfaces( + &role_interfaces(caller_bindings), + &role_interfaces(callee_bindings), + ); + + if role_related_interfaces.is_empty() { + 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, @@ -279,171 +272,12 @@ impl<'a> ComponentSequenceValidator<'a> { continue; } - self.result + self.errors.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, + &role_related_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), - )); - } - } - - 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 } } @@ -521,11 +355,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; @@ -567,14 +406,18 @@ fn build_unit_bindings( bindings.all_interfaces.insert(interface_id.clone()); - match relation.source_role.as_deref() { - Some("Required") => { + if relation.relation_type != ComponentRelationType::InterfaceBinding { + continue; + } + + match relation.source_role { + EndpointRole::Required => { bindings.required_interfaces.insert(interface_id); } - Some("Provided") => { + EndpointRole::Provided => { bindings.provided_interfaces.insert(interface_id); } - _ => {} + EndpointRole::None => {} } } @@ -584,33 +427,6 @@ fn build_unit_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, @@ -621,13 +437,6 @@ fn all_interfaces_for_alias( .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, @@ -659,94 +468,23 @@ fn intersect_interfaces( .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)) - }) +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: 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, + expected_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\ @@ -756,26 +494,7 @@ fn format_sequence_role_consistency_error( 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}", + expected_interfaces = format_name_list(expected_interfaces), ) } @@ -787,23 +506,6 @@ 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(); diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 94cf983d..2ad8b532 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -14,7 +14,11 @@ //! Validator entrypoints for architecture checks. mod bazel_component_validator; +mod component_internal_api_validator; mod component_sequence_validator; +mod sequence_internal_api_validator; 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..988b8022 --- /dev/null +++ b/validation/core/src/validators/sequence_internal_api_validator.rs @@ -0,0 +1,461 @@ +// ******************************************************************************* +// 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 crate::models::{ + ComponentDiagramArchitecture, Errors, InternalApiIndex, InternalApiInterface, + LogicComponentExt, ObservedSequenceCall, SequenceDiagramIndex, +}; + +/// 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>, + errors: Errors, +) -> Errors { + SequenceInternalApiValidator::new( + sequence_diagram, + internal_api_diagram, + component_diagram, + errors, + ) + .run() +} + +struct SequenceInternalApiValidator<'a> { + sequence_diagram: &'a SequenceDiagramIndex, + internal_api_interfaces_by_id: BTreeMap, + component_context: Option>, + errors: Errors, +} + +struct ComponentContext<'a> { + observed_call_contexts: Vec>, + unit_bindings: BTreeMap>, + all_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 has_shared_interfaces(&self) -> bool { + !self.caller_interfaces.is_disjoint(&self.callee_interfaces) + } +} + +impl<'a> SequenceInternalApiValidator<'a> { + fn new( + sequence_diagram: &'a SequenceDiagramIndex, + internal_api_diagram: &'a InternalApiIndex, + component_diagram: Option<&ComponentDiagramArchitecture>, + errors: Errors, + ) -> 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, + errors, + } + } + + fn run(mut self) -> Errors { + self.errors.debug_output = self.build_debug_log(); + self.check_sequence_call_method_consistency_with_component_context(); + self.check_interface_method_coverage(); + self.errors + } + + fn build_debug_log(&self) -> String { + let mut log = String::new(); + + if let Some(component_context) = self.component_context.as_ref() { + log.push_str( + "DEBUG: Sequence calls checked against related internal API interfaces:\n", + ); + for call_context in &component_context.observed_call_contexts { + log.push_str(&format!( + " {} -> {} : {}\n", + call_context.caller_unit, call_context.callee_unit, call_context.method + )); + } + + log.push_str("DEBUG: Unit interface targets from component diagrams:\n"); + for (unit_alias, bindings) in &component_context.unit_bindings { + log.push_str(&format!( + " {unit_alias} -> {}\n", + format_name_list(bindings) + )); + } + + log.push_str(&format!( + "DEBUG: All interfaces for self-call validation:\n {}\n", + format_name_list(&component_context.all_interfaces) + )); + } else { + log.push_str( + "DEBUG: Sequence method-name consistency skipped because component context is unavailable:\n", + ); + for call in self.sequence_diagram.observed_calls() { + log.push_str(&format!( + " {} -> {} : {}\n", + call.caller, call.callee, call.method + )); + } + } + + log.push_str("DEBUG: Internal API interfaces available for sequence validation:\n"); + for interface_id in self.internal_api_interfaces_by_id.keys() { + log.push_str(&format!(" {interface_id}\n")); + } + + log + } + + 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(); + + 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) { + continue; + } + + if is_self_call { + let matching_interfaces = matching_interfaces_with_method( + &self.internal_api_interfaces_by_id, + &component_context.all_interfaces, + method_name, + ); + + if matching_interfaces.is_empty() { + self.errors.push(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 component-sequence validator reports that this + // cross-unit call has no usable shared interface relation. + continue; + } + + if units_with_missing_internal_api_interfaces.contains(call_context.caller_unit) + || units_with_missing_internal_api_interfaces.contains(call_context.callee_unit) + { + continue; + } + + 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, + ); + + if !caller_matching_interfaces.is_disjoint(&callee_matching_interfaces) { + continue; + } + + self.errors.push(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 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.errors.push(format_interface_method_coverage_error( + interface, + &missing_methods, + )); + } + } +} + +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_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 = BTreeSet::new(); + + 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.insert(interface_id); + } + + unit_bindings.insert(alias, bindings); + } + + unit_bindings +} + +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 all_interfaces_for_alias( + unit_bindings: &BTreeMap>, + alias: &str, +) -> BTreeSet { + unit_bindings.get(alias).cloned().unwrap_or_default() +} + +fn build_observed_call_contexts<'a>( + observed_calls: &'a [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 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 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: &BTreeMap>, + internal_api_interfaces_by_id: &BTreeMap, +) -> BTreeSet { + unit_bindings + .iter() + .filter(|(_, bindings)| { + bindings.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 violation: 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 violation: {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_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/sequence_internal_api_validator_test.rs"] +mod tests; 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..98d2b725 --- /dev/null +++ b/validation/core/src/validators/test/component_internal_api_validator_test.rs @@ -0,0 +1,232 @@ +// ******************************************************************************* +// 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::*; +use crate::models::{ + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, + LogicRelation, +}; +use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; + +fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { + LogicRelation { + target: target.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role, + } +} + +fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { + let mut relations = Vec::new(); + for target in interface_targets { + relations.push(relation_with_role(target, EndpointRole::Required)); + 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, + } +} + +fn interface(alias: &str) -> LogicComponent { + LogicComponent { + id: alias.to_string(), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: None, + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + } +} + +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(), + } +} + +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, + }]; + + let mut errors = Errors::default(); + let index = InternalApiIndex::build_index(&diagrams, &mut errors); + assert!(errors.is_empty()); + index +} + +fn validate(component_diagrams: ComponentDiagramInputs, internal_api: &InternalApiIndex) -> Errors { + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + + validate_component_internal_api(&component_arch, internal_api, errors) +} + +#[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface\"")); + assert!(!errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("Missing internal API interface")); + assert!(errors.messages[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 errors = validate(component_diagrams, &internal_api); + + assert!(errors.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..507c37e7 100644 --- a/validation/core/src/validators/test/component_sequence_validator_test.rs +++ b/validation/core/src/validators/test/component_sequence_validator_test.rs @@ -12,30 +12,29 @@ // ******************************************************************************* use super::*; use crate::models::{ - ComponentDiagramElementType, ComponentDiagramInput, ComponentDiagramInputs, - ComponentDiagramRelation, SequenceDiagramInput, SequenceDiagramInputs, + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, + LogicRelation, 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 { +fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { + relation_with_type_and_role(target, ComponentRelationType::InterfaceBinding, source_role) +} + +fn relation_with_type_and_role( + target: &str, + relation_type: ComponentRelationType, + source_role: EndpointRole, +) -> LogicRelation { + LogicRelation { target: target.to_string(), annotation: None, - relation_type: Some("InterfaceBinding".to_string()), - source_role: Some(source_role.to_string()), + relation_type, + source_role, } } -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 { +fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { unit_with_interface_roles(alias, interface_targets, interface_targets) } @@ -43,49 +42,33 @@ fn unit_with_interface_roles( alias: &str, required_interfaces: &[&str], provided_interfaces: &[&str], -) -> ComponentDiagramInput { +) -> LogicComponent { let mut relations = Vec::new(); for target in required_interfaces { - relations.push(required_relation(target)); + relations.push(relation_with_role(target, EndpointRole::Required)); } for target in provided_interfaces { - relations.push(provided_relation(target)); + relations.push(relation_with_role(target, EndpointRole::Provided)); } - unit_with_relations(alias, relations) -} - -fn unit_with_relations( - alias: &str, - relations: Vec, -) -> ComponentDiagramInput { - ComponentDiagramInput { + LogicComponent { id: format!("some_id.{alias}"), + name: Some(alias.to_string()), alias: Some(alias.to_string()), parent_id: None, - element_type: ComponentDiagramElementType::Component, + element_type: ComponentType::Component, stereotype: Some("unit".to_string()), relations, } } -fn interface(alias: &str) -> ComponentDiagramInput { - ComponentDiagramInput { +fn interface(alias: &str) -> LogicComponent { + LogicComponent { id: alias.to_string(), + name: Some(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, + element_type: ComponentType::Interface, stereotype: None, relations: Vec::new(), } @@ -97,54 +80,10 @@ fn component_diagrams(aliases: &[&str]) -> ComponentDiagramInputs { } } -fn component_diagrams_with_entities( - entities: Vec, -) -> ComponentDiagramInputs { +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, - }]; - - let mut setup_result = ValidationResult::default(); - let index = InternalApiIndex::build_index(&diagrams, &mut setup_result); - assert!(setup_result.is_empty()); - index -} - fn sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { sequence_calls( &participants @@ -156,23 +95,19 @@ fn sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { 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, + 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(), }], } } @@ -187,7 +122,7 @@ fn passes_when_aliases_and_participants_are_identical() { 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_sequence(&component_arch, &sequence_index); assert!(validation_result.is_empty()); } @@ -201,7 +136,7 @@ fn reports_missing_and_extra() { 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_sequence(&component_arch, &sequence_index); assert!(!validation_result.is_empty()); assert_eq!(validation_result.failures.len(), 3); @@ -224,11 +159,12 @@ 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(), }], @@ -240,7 +176,7 @@ fn units_without_alias_are_ignored() { 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_sequence(&component_arch, &sequence_index); assert!(validation_result.is_empty()); } @@ -254,7 +190,7 @@ fn reports_alias_missing_from_participants() { 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"u2\"")); } @@ -269,7 +205,7 @@ fn reports_participant_not_in_aliases() { 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"orphan\"")); } @@ -287,7 +223,7 @@ fn reports_missing_component_alias_and_interface_connection_for_sequence_call() 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -317,7 +253,7 @@ fn reports_missing_sequence_call_for_interface_connected_units() { 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -339,7 +275,7 @@ fn reports_missing_participant_and_missing_sequence_call_for_interface_connected 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -368,7 +304,7 @@ fn reports_sequence_call_without_corresponding_shared_interface_connection() { 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_sequence(&component_arch, &sequence_index); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -391,7 +327,7 @@ fn passes_when_interface_connected_units_have_sequence_call() { 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_sequence(&component_arch, &sequence_index); assert!(validation_result.is_empty()); } @@ -409,591 +345,96 @@ fn passes_when_sequence_call_matches_consumer_provider_roles() { 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_sequence(&component_arch, &sequence_index); 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() { +fn passes_when_multiple_consumers_share_interface_without_calling_each_other() { let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), + unit_with_interface_roles("u1", &["InternalInterface"], &[]), + unit_with_interface_roles("u2", &[], &["InternalInterface"]), + unit_with_interface_roles("u3", &["InternalInterface"], &[]), interface("InternalInterface"), ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); + let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()"), ("u3", "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, 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\"")); + let validation_result = validate_component_sequence(&component_arch, &sequence_index); + assert!(validation_result.is_empty()); } #[test] -fn reports_unreferenced_internal_api_interface_function_not_exercised_without_self_calls() { +fn reports_cross_unit_sequence_call_with_invalid_consumer_provider_roles() { let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), + 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"]), - ("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)); + let validation_result = validate_component_sequence(&component_arch, &sequence_index); 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") - ); + .contains("sequence interaction does not match consumer/provider roles")); 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\""))); + .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_case_mismatch_between_component_and_internal_api_interface_names() { +fn ignores_source_roles_on_non_interface_binding_relations() { let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), + LogicComponent { + id: "some_id.u1".to_string(), + name: Some("u1".to_string()), + alias: Some("u1".to_string()), + parent_id: None, + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations: vec![relation_with_type_and_role( + "InternalInterface", + ComponentRelationType::Dependency, + EndpointRole::Provided, + )], + }, + LogicComponent { + id: "some_id.u2".to_string(), + name: Some("u2".to_string()), + alias: Some("u2".to_string()), + parent_id: None, + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations: vec![relation_with_type_and_role( + "InternalInterface", + ComponentRelationType::Dependency, + EndpointRole::Required, + )], + }, 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_sequence(&component_arch, &sequence_index); assert!(validation_result.is_empty()); } 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..90e6593b --- /dev/null +++ b/validation/core/src/validators/test/sequence_internal_api_validator_test.rs @@ -0,0 +1,477 @@ +// ******************************************************************************* +// 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::*; +use crate::models::{ + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, Errors, + LogicComponent, LogicRelation, SequenceDiagramInputs, +}; +use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; +use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; + +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, + }]; + + let mut errors = Errors::default(); + let index = InternalApiIndex::build_index(&diagrams, &mut errors); + assert!(errors.is_empty()); + index +} + +fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { + LogicRelation { + target: target.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role, + } +} + +fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { + unit_with_interface_roles(alias, interface_targets, interface_targets) +} + +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, + } +} + +fn interface(alias: &str) -> LogicComponent { + LogicComponent { + id: alias.to_string(), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: None, + element_type: ComponentType::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 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(), + }], + } +} + +fn validate(sequence_diagrams: SequenceDiagramInputs, internal_api: &InternalApiIndex) -> Errors { + let mut errors = Errors::default(); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + validate_sequence_internal_api(&sequence_index, internal_api, None, errors) +} + +fn validate_with_component_context( + component_diagrams: ComponentDiagramInputs, + sequence_diagrams: SequenceDiagramInputs, + internal_api: &InternalApiIndex, +) -> Errors { + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + validate_sequence_internal_api(&sequence_index, internal_api, Some(&component_arch), errors) +} + +#[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 errors = validate(sequence_diagrams, &internal_api); + + assert!(errors.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 errors = validate(sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(errors.messages[0].contains("\"InternalInterface\"")); + assert!(errors.messages[0].contains("\"OtherMethod\"")); + assert!(errors + .messages + .iter() + .all(|message| !message.contains("Method consistency violation"))); +} + +#[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 errors = validate(sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(errors.messages[0].contains("\"InternalInterface\"")); + assert!(errors.messages[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 errors = validate(sequence_diagrams, &internal_api); + + assert!(errors.is_empty()); +} + +#[test] +fn reports_sequence_function_missing_from_related_interface_methods_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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(errors.messages.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_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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(errors.messages[0].contains("\"InternalInterface\"")); + assert!(errors.messages[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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0] + .contains("internal API interface functions are not exercised in sequence diagrams")); + assert!(errors.messages[0].contains("\"OtherInterface\"")); + assert!(errors.messages[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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages.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_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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(errors.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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("sequence self-call function name was not found") + && message.contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"") + })); + assert!(errors.messages.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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(errors.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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert!(errors.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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("sequence self-call function name was not found")); + assert!(errors.messages[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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(errors.messages.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(errors + .messages + .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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(errors.messages.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(errors + .messages + .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 errors = + validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); + + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("sequence function name was not found in the related interface methods") + && message.contains("Sequence call : \"u1\" -> \"u2\" : \"GetData\"") + })); + assert!(errors.messages.iter().any(|message| { + message.contains("internal API interface functions are not exercised in sequence diagrams") + && message.contains("\"SharedInterface\"") + && message.contains("\"OtherMethod\"") + })); + assert!(errors + .messages + .iter() + .all(|message| !message.contains("Missing functions : \"GetData\""))); +} From 21dfff7e63f13f761273973da01d046bd476657b Mon Sep 17 00:00:00 2001 From: Xian Gu Date: Thu, 2 Jul 2026 10:05:58 +0800 Subject: [PATCH 2/3] split integration test --- validation/core/integration_test/BUILD | 58 ++++++++++-- validation/core/integration_test/README.md | 50 ++++++++--- .../BUILD | 38 ++++++++ .../component_diagram.puml | 0 .../expected.json | 7 ++ .../internal_api_diagram.puml | 0 .../expected.json | 7 ++ .../positive_interface_match/BUILD | 37 ++++++++ .../component_diagram.puml | 0 .../positive_interface_match}/expected.json | 0 .../internal_api_diagram.puml | 0 .../expected.json | 7 -- .../BUILD | 0 .../component_diagram.puml | 0 .../expected.json | 0 .../internal_api_diagram.puml | 0 .../sequence_diagram.puml | 0 .../BUILD | 0 .../component_diagram.puml | 0 .../expected.json | 0 .../internal_api_diagram.puml | 22 +++++ .../sequence_diagram.puml | 0 .../BUILD | 0 .../component_diagram.puml | 30 +++++++ .../expected.json | 3 +- .../internal_api_diagram.puml | 26 ++++++ .../sequence_diagram.puml | 0 .../BUILD | 1 + .../component_diagram.puml | 0 .../expected.json | 9 ++ .../internal_api_diagram.puml | 0 .../sequence_diagram.puml | 0 .../positive_internal_api_method_match/BUILD | 0 .../component_diagram.puml | 27 ++++++ .../expected.json | 0 .../internal_api_diagram.puml | 0 .../sequence_diagram.puml | 0 .../positive_self_call_method_match/BUILD | 0 .../component_diagram.puml | 0 .../expected.json | 4 + .../internal_api_diagram.puml | 22 +++++ .../sequence_diagram.puml | 0 .../src/component_internal_api_suite.rs | 60 +++++++++++++ .../src/component_sequence_suite.rs | 40 +-------- .../src/sequence_internal_api_suite.rs | 88 +++++++++++++++++++ 45 files changed, 468 insertions(+), 68 deletions(-) create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/BUILD rename validation/core/integration_test/{component_sequence/negative_interface_function_not_exercised => component_internal_api/negative_interface_missing_from_internal_api}/component_diagram.puml (100%) create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api/expected.json rename validation/core/integration_test/{component_sequence/negative_method_missing_from_internal_api => component_internal_api/negative_interface_missing_from_internal_api}/internal_api_diagram.puml (100%) create mode 100644 validation/core/integration_test/component_internal_api/negative_method_missing_from_internal_api/expected.json create mode 100644 validation/core/integration_test/component_internal_api/positive_interface_match/BUILD rename validation/core/integration_test/{component_sequence/negative_invalid_consumer_provider_direction => component_internal_api/positive_interface_match}/component_diagram.puml (100%) rename validation/core/integration_test/{component_sequence/positive_internal_api_method_match => component_internal_api/positive_interface_match}/expected.json (100%) rename validation/core/integration_test/{component_sequence/positive_internal_api_method_match => component_internal_api/positive_interface_match}/internal_api_diagram.puml (100%) delete mode 100644 validation/core/integration_test/component_sequence/negative_method_missing_from_internal_api/expected.json rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_interface_function_not_exercised/BUILD (100%) rename validation/core/integration_test/{component_sequence/negative_method_missing_from_internal_api => sequence_internal_api/negative_interface_function_not_exercised}/component_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_interface_function_not_exercised/expected.json (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_interface_function_not_exercised/internal_api_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_interface_function_not_exercised/sequence_diagram.puml (100%) rename validation/core/integration_test/{component_sequence/negative_method_missing_from_internal_api => sequence_internal_api/negative_invalid_consumer_provider_direction}/BUILD (100%) rename validation/core/integration_test/{component_sequence/negative_missing_method_in_related_interface => sequence_internal_api/negative_invalid_consumer_provider_direction}/component_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_invalid_consumer_provider_direction/expected.json (100%) create mode 100644 validation/core/integration_test/sequence_internal_api/negative_invalid_consumer_provider_direction/internal_api_diagram.puml rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_invalid_consumer_provider_direction/sequence_diagram.puml (100%) rename validation/core/integration_test/{component_sequence/negative_missing_method_in_related_interface => sequence_internal_api/negative_method_available_but_not_on_related_interface}/BUILD (100%) create mode 100644 validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/component_diagram.puml rename validation/core/integration_test/{component_sequence/negative_missing_method_in_related_interface => sequence_internal_api/negative_method_available_but_not_on_related_interface}/expected.json (74%) create mode 100644 validation/core/integration_test/sequence_internal_api/negative_method_available_but_not_on_related_interface/internal_api_diagram.puml rename validation/core/integration_test/{component_sequence/negative_method_missing_from_internal_api => sequence_internal_api/negative_method_available_but_not_on_related_interface}/sequence_diagram.puml (100%) rename validation/core/integration_test/{component_sequence/negative_invalid_consumer_provider_direction => sequence_internal_api/negative_missing_method_in_related_interface}/BUILD (95%) rename validation/core/integration_test/{component_sequence/positive_internal_api_method_match => sequence_internal_api/negative_missing_method_in_related_interface}/component_diagram.puml (100%) create mode 100644 validation/core/integration_test/sequence_internal_api/negative_missing_method_in_related_interface/expected.json rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_missing_method_in_related_interface/internal_api_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/negative_missing_method_in_related_interface/sequence_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/positive_internal_api_method_match/BUILD (100%) create mode 100644 validation/core/integration_test/sequence_internal_api/positive_internal_api_method_match/component_diagram.puml rename validation/core/integration_test/{component_sequence/positive_self_call_method_match => sequence_internal_api/positive_internal_api_method_match}/expected.json (100%) rename validation/core/integration_test/{component_sequence/positive_self_call_method_match => sequence_internal_api/positive_internal_api_method_match}/internal_api_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/positive_internal_api_method_match/sequence_diagram.puml (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/positive_self_call_method_match/BUILD (100%) rename validation/core/integration_test/{component_sequence => sequence_internal_api}/positive_self_call_method_match/component_diagram.puml (100%) create mode 100644 validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/expected.json create mode 100644 validation/core/integration_test/sequence_internal_api/positive_self_call_method_match/internal_api_diagram.puml rename validation/core/integration_test/{component_sequence => sequence_internal_api}/positive_self_call_method_match/sequence_diagram.puml (100%) create mode 100644 validation/core/integration_test/src/component_internal_api_suite.rs create mode 100644 validation/core/integration_test/src/sequence_internal_api_suite.rs 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"); +} From 8f19bd106f40c69b87cf73986c0ad33e2971be39 Mon Sep 17 00:00:00 2001 From: Xian Gu Date: Thu, 2 Jul 2026 10:46:15 +0800 Subject: [PATCH 3/3] fix comments --- validation/core/BUILD | 4 + .../docs/requirements/tool_requirements.trlc | 17 +- .../docs/specifications/component_sequence.md | 6 +- .../specifications/sequence_internal_api.md | 51 +- validation/core/src/models/bazel_models.rs | 39 ++ .../src/models/component_diagram_models.rs | 76 ++- .../core/src/profiles/architectural_design.rs | 59 ++- .../validators/bazel_component_validator.rs | 55 --- .../component_internal_api_validator.rs | 74 ++- .../component_sequence_validator.rs | 256 +--------- validation/core/src/validators/mod.rs | 5 + .../sequence_internal_api_validator.rs | 456 ++++++++++-------- .../src/validators/shared/diagram_analysis.rs | 126 +++++ .../core/src/validators/shared/helpers.rs | 50 ++ validation/core/src/validators/shared/mod.rs | 25 + .../component_internal_api_validator_test.rs | 170 ++----- .../test/component_sequence_validator_test.rs | 287 +---------- .../core/src/validators/test/fixtures.rs | 167 +++++++ .../sequence_internal_api_validator_test.rs | 370 +++++++------- 19 files changed, 1103 insertions(+), 1190 deletions(-) create mode 100644 validation/core/src/validators/shared/diagram_analysis.rs create mode 100644 validation/core/src/validators/shared/helpers.rs create mode 100644 validation/core/src/validators/shared/mod.rs create mode 100644 validation/core/src/validators/test/fixtures.rs diff --git a/validation/core/BUILD b/validation/core/BUILD index fdaf1510..3d17159e 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -56,8 +56,12 @@ rust_library( "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", diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index c820540e..b92c3777 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -102,7 +102,7 @@ section "Tool Requirements" { section "Sequence Internal API Validator" { - ToolQualification.ToolRequirement ComponentSequenceMethodNameConsistency { + 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 @@ -112,16 +112,19 @@ section "Tool Requirements" { satisfied_by = Verifier } - ToolQualification.ToolRequirement SequenceInternalApiInterfaceCoverage { - description = '''When sequence and internal API diagrams are - provided, the validator shall report an error when a function - declared in an internal API interface is never called in any - sequence interaction. Self-calls count as valid usage.''' + 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 { + 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 diff --git a/validation/core/docs/specifications/component_sequence.md b/validation/core/docs/specifications/component_sequence.md index 53b14e1f..4ab39190 100644 --- a/validation/core/docs/specifications/component_sequence.md +++ b/validation/core/docs/specifications/component_sequence.md @@ -47,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 @@ -74,7 +73,6 @@ unit_1 -> unit_2 : GetData() | 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 | ## Debug Output diff --git a/validation/core/docs/specifications/sequence_internal_api.md b/validation/core/docs/specifications/sequence_internal_api.md index 4aaed6f1..0f5751e7 100644 --- a/validation/core/docs/specifications/sequence_internal_api.md +++ b/validation/core/docs/specifications/sequence_internal_api.md @@ -30,20 +30,49 @@ the participating units. All comparisons are case-sensitive. -Method-name consistency is 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 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. -### Related Interface Method-Name Consistency With Component Context +### Method-Name Consistency -When component context is available, every function used in a sequence -interaction must be declared in the related Internal API interface context. +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 component or Internal API +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. -*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceMethodNameConsistency`)* + +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 @@ -68,8 +97,7 @@ interface "IData" as IData <> { 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.SequenceInternalApiInterfaceCoverage`)* -*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInterfaceCoverage`)* +*(Requirement: {requirement:downstream-ref}`Tools.ComponentSequenceInternalApiInterfaceCoverage`)* ```text ' internal_api diagram @@ -89,7 +117,8 @@ unit_1 -> unit_2 : SetData(d) | Failure case | Validation rule | |---|---| -| Method not declared in related interface | Related Interface Method-Name Consistency With Component Context | +| 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 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 0f09aa47..6151f6b3 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -13,11 +13,11 @@ use std::collections::BTreeMap; +use super::EntityKey; +use crate::ValidationResult; pub use component_diagram::{ ComponentRelationType, ComponentType, EndpointRole, LogicComponent, LogicRelation, }; -use super::EntityKey; -use crate::ValidationResult; /// Validation-specific helpers for component metamodel entities. pub trait LogicComponentExt { @@ -275,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/profiles/architectural_design.rs b/validation/core/src/profiles/architectural_design.rs index 01d73d26..3275e677 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -24,6 +24,8 @@ 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 { @@ -33,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::( @@ -51,32 +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 results, - validate_component_sequence(component, sequence, Errors::default()), - ); - ran_validator = true; - } - if let (Some(component), Some(internal_api)) = (component.as_ref(), internal_api.as_ref()) { - merge_results( - &mut results, - validate_component_internal_api(component, internal_api, Errors::default()), - ); - ran_validator = true; - } - if let (Some(sequence), Some(internal_api)) = (sequence.as_ref(), internal_api.as_ref()) { - merge_results( - &mut results, - validate_sequence_internal_api( - sequence, - internal_api, - component.as_ref(), - Errors::default(), - ), - ); - 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/validators/bazel_component_validator.rs b/validation/core/src/validators/bazel_component_validator.rs index 41e3a601..ca30562c 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -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 index a217566b..5e10ef55 100644 --- a/validation/core/src/validators/component_internal_api_validator.rs +++ b/validation/core/src/validators/component_internal_api_validator.rs @@ -16,56 +16,44 @@ use std::collections::BTreeSet; -use crate::models::{ComponentDiagramArchitecture, Errors, InternalApiIndex, LogicComponentExt}; +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, - errors: Errors, -) -> Errors { - ComponentInternalApiValidator::new(component_diagram, internal_api_diagram, errors).run() +) -> ValidationResult { + ComponentInternalApiValidator::new(component_diagram, internal_api_diagram).run() } struct ComponentInternalApiValidator { component_interface_ids: BTreeSet, internal_api_interface_ids: BTreeSet, - errors: Errors, + result: ValidationResult, } impl ComponentInternalApiValidator { fn new( component_diagram: &ComponentDiagramArchitecture, internal_api_diagram: &InternalApiIndex, - errors: Errors, ) -> Self { Self { component_interface_ids: collect_component_interface_ids(component_diagram), internal_api_interface_ids: collect_internal_api_interface_ids(internal_api_diagram), - errors, + result: ValidationResult::default(), } } - fn run(mut self) -> Errors { - self.errors.debug_output = self.build_debug_log(); + 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.errors - } - - fn build_debug_log(&self) -> String { - let mut log = String::new(); - - log.push_str("DEBUG: Component interfaces checked against internal API:\n"); - for interface_id in &self.component_interface_ids { - log.push_str(&format!(" {interface_id}\n")); - } - - log.push_str("DEBUG: Internal API interfaces available for component interfaces:\n"); - for interface_id in &self.internal_api_interface_ids { - log.push_str(&format!(" {interface_id}\n")); - } - - log + self.result } fn check_component_interfaces_declared_by_internal_api(&mut self) { @@ -76,14 +64,30 @@ impl ComponentInternalApiValidator { .collect(); if !missing_interfaces.is_empty() { - self.errors - .push(format_missing_internal_api_interface_error( + 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 { @@ -106,25 +110,13 @@ fn format_missing_internal_api_interface_error( missing_internal_api_interfaces: &BTreeSet, ) -> String { format!( - "Internal API consistency violation: Missing internal API interface:\n\ + "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), ) } -fn format_name_list(names: &BTreeSet) -> String { - if names.is_empty() { - return "".to_string(); - } - - names - .iter() - .map(|name| format!("\"{name}\"")) - .collect::>() - .join(", ") -} - #[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 063e3e7a..8b0baf7e 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -16,10 +16,11 @@ use std::collections::{BTreeMap, BTreeSet}; -use crate::models::{ - ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, LogicComponentExt, - 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. @@ -31,31 +32,15 @@ pub fn validate_component_sequence( } 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, + 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 { @@ -88,10 +73,6 @@ impl SequenceCallContext<'_> { &self.callee_interfaces } } - - fn has_shared_interfaces(&self) -> bool { - !self.caller_interfaces.is_disjoint(&self.callee_interfaces) - } } impl<'a> ComponentSequenceValidator<'a> { @@ -118,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(); @@ -130,7 +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(); } fn check_participant_aliases(&mut self) { @@ -224,70 +202,13 @@ impl<'a> ComponentSequenceValidator<'a> { )); } } - - 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 call_context.caller_unit == call_context.callee_unit { - continue; - } - - let Some(caller_bindings) = self.unit_bindings.get(call_context.caller_unit) else { - continue; - }; - let Some(callee_bindings) = self.unit_bindings.get(call_context.callee_unit) else { - continue; - }; - - if !seen_interactions.insert(( - call_context.caller_unit.to_string(), - call_context.callee_unit.to_string(), - )) { - continue; - } - - if !call_context.has_shared_interfaces() { - continue; - } - let role_related_interfaces = intersect_interfaces( - &role_interfaces(caller_bindings), - &role_interfaces(callee_bindings), - ); - - if role_related_interfaces.is_empty() { - continue; - } - - let directional_interfaces = intersect_interfaces( - &caller_bindings.required_interfaces, - &callee_bindings.provided_interfaces, - ); - - if !directional_interfaces.is_empty() { - continue; - } - - self.errors.result - .add_failure(format_sequence_role_consistency_error( - call_context, - &role_related_interfaces, - )); - } - } } 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()); @@ -315,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(); @@ -378,150 +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()); - - if relation.relation_type != ComponentRelationType::InterfaceBinding { - continue; - } - - match relation.source_role { - EndpointRole::Required => { - bindings.required_interfaces.insert(interface_id); - } - EndpointRole::Provided => { - bindings.provided_interfaces.insert(interface_id); - } - EndpointRole::None => {} - } - } - - unit_bindings.insert(alias, bindings); - } - - unit_bindings -} - -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 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 role_interfaces(bindings: &UnitInterfaces) -> BTreeSet { - bindings - .required_interfaces - .union(&bindings.provided_interfaces) - .cloned() - .collect() -} - -fn format_sequence_role_consistency_error( - call_context: &SequenceCallContext<'_>, - expected_interfaces: &BTreeSet, -) -> String { - let sequence_call = format_sequence_call( - call_context.caller_unit, - call_context.callee_unit, - call_context.method, - ); - - 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), - ) -} - -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_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 2ad8b532..be1c55ee 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -17,6 +17,11 @@ 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; diff --git a/validation/core/src/validators/sequence_internal_api_validator.rs b/validation/core/src/validators/sequence_internal_api_validator.rs index 988b8022..96e72600 100644 --- a/validation/core/src/validators/sequence_internal_api_validator.rs +++ b/validation/core/src/validators/sequence_internal_api_validator.rs @@ -17,60 +17,44 @@ 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, Errors, InternalApiIndex, InternalApiInterface, - LogicComponentExt, ObservedSequenceCall, SequenceDiagramIndex, + 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>, - errors: Errors, -) -> Errors { - SequenceInternalApiValidator::new( - sequence_diagram, - internal_api_diagram, - component_diagram, - errors, - ) - .run() +) -> 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>, - errors: Errors, + result: ValidationResult, } struct ComponentContext<'a> { observed_call_contexts: Vec>, - unit_bindings: BTreeMap>, + unit_bindings: UnitBindings, all_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 has_shared_interfaces(&self) -> bool { - !self.caller_interfaces.is_disjoint(&self.callee_interfaces) - } -} - impl<'a> SequenceInternalApiValidator<'a> { fn new( sequence_diagram: &'a SequenceDiagramIndex, internal_api_diagram: &'a InternalApiIndex, component_diagram: Option<&ComponentDiagramArchitecture>, - errors: Errors, ) -> Self { let internal_api_interfaces_by_id = build_internal_api_interfaces_by_id(internal_api_diagram); @@ -82,61 +66,20 @@ impl<'a> SequenceInternalApiValidator<'a> { sequence_diagram, internal_api_interfaces_by_id, component_context, - errors, + result: ValidationResult::default(), } } - fn run(mut self) -> Errors { - self.errors.debug_output = self.build_debug_log(); + 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.errors - } - - fn build_debug_log(&self) -> String { - let mut log = String::new(); - - if let Some(component_context) = self.component_context.as_ref() { - log.push_str( - "DEBUG: Sequence calls checked against related internal API interfaces:\n", - ); - for call_context in &component_context.observed_call_contexts { - log.push_str(&format!( - " {} -> {} : {}\n", - call_context.caller_unit, call_context.callee_unit, call_context.method - )); - } - - log.push_str("DEBUG: Unit interface targets from component diagrams:\n"); - for (unit_alias, bindings) in &component_context.unit_bindings { - log.push_str(&format!( - " {unit_alias} -> {}\n", - format_name_list(bindings) - )); - } - - log.push_str(&format!( - "DEBUG: All interfaces for self-call validation:\n {}\n", - format_name_list(&component_context.all_interfaces) - )); - } else { - log.push_str( - "DEBUG: Sequence method-name consistency skipped because component context is unavailable:\n", - ); - for call in self.sequence_diagram.observed_calls() { - log.push_str(&format!( - " {} -> {} : {}\n", - call.caller, call.callee, call.method - )); - } - } - - log.push_str("DEBUG: Internal API interfaces available for sequence validation:\n"); - for interface_id in self.internal_api_interfaces_by_id.keys() { - log.push_str(&format!(" {interface_id}\n")); - } - - log + self.result } fn check_sequence_call_method_consistency_with_component_context(&mut self) { @@ -150,6 +93,7 @@ impl<'a> SequenceInternalApiValidator<'a> { ); 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; @@ -165,62 +109,170 @@ impl<'a> SequenceInternalApiValidator<'a> { method_name.to_string(), ); if !seen_calls.insert(call_key) { + // Repeated calls exercise the same method relation once. continue; } - if is_self_call { - let matching_interfaces = matching_interfaces_with_method( - &self.internal_api_interfaces_by_id, - &component_context.all_interfaces, - method_name, - ); - - if matching_interfaces.is_empty() { - self.errors.push(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", - )); - } - + if let Some(error) = self.check_method_exists_in_internal_api( + component_context, + call_context, + method_name, + ) { + consistency_errors.push(error); continue; } - if !call_context.has_shared_interfaces() { - // The structural component-sequence validator reports that this - // cross-unit call has no usable shared interface relation. + if is_self_call { + // Self-calls are not checked for cross-unit role consistency. continue; } - if units_with_missing_internal_api_interfaces.contains(call_context.caller_unit) - || units_with_missing_internal_api_interfaces.contains(call_context.callee_unit) - { - 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); } + } - 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, + 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", + )); + } - if !caller_matching_interfaces.is_disjoint(&callee_matching_interfaces) { - continue; - } + 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; + } - self.errors.push(format_sequence_method_consistency_error( + 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) { @@ -237,12 +289,64 @@ impl<'a> SequenceInternalApiValidator<'a> { continue; } - self.errors.push(format_interface_method_coverage_error( - interface, - &missing_methods, - )); + 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>( @@ -262,41 +366,6 @@ fn build_component_context<'a>( } } -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 = BTreeSet::new(); - - 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.insert(interface_id); - } - - unit_bindings.insert(alias, bindings); - } - - unit_bindings -} - fn build_all_interfaces( component_diagram: &ComponentDiagramArchitecture, internal_api_diagram: &InternalApiIndex, @@ -317,34 +386,6 @@ fn build_all_interfaces( interface_ids } -fn all_interfaces_for_alias( - unit_bindings: &BTreeMap>, - alias: &str, -) -> BTreeSet { - unit_bindings.get(alias).cloned().unwrap_or_default() -} - -fn build_observed_call_contexts<'a>( - observed_calls: &'a [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 matching_interfaces_with_method( internal_api_interfaces_by_id: &BTreeMap, interface_ids: &BTreeSet, @@ -361,6 +402,14 @@ fn matching_interfaces_with_method( .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 { @@ -374,13 +423,13 @@ fn build_internal_api_interfaces_by_id( } fn collect_units_with_missing_internal_api_interfaces( - unit_bindings: &BTreeMap>, + unit_bindings: &UnitBindings, internal_api_interfaces_by_id: &BTreeMap, ) -> BTreeSet { unit_bindings .iter() .filter(|(_, bindings)| { - bindings.iter().any(|interface_id| { + bindings.all_interfaces.iter().any(|interface_id| { !internal_api_interfaces_by_id.contains_key(interface_id.as_str()) }) }) @@ -408,7 +457,7 @@ fn format_interface_method_coverage_error( missing_methods: &BTreeSet, ) -> String { format!( - "Coverage consistency violation: internal API interface functions are not exercised in sequence diagrams:\n\ + "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", @@ -430,30 +479,33 @@ fn format_sequence_method_consistency_error( ); format!( - "Method consistency violation: {description}:\n\ + "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_name_list(names: &BTreeSet) -> String { - if names.is_empty() { - return "".to_string(); - } - - names - .iter() - .map(|name| format!("\"{name}\"")) - .collect::>() - .join(", ") -} +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, + ); -fn extract_method_name(method: &str) -> &str { - method.split('(').next().unwrap_or(method).trim() + 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)] 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 index 98d2b725..5f84e7a1 100644 --- a/validation/core/src/validators/test/component_internal_api_validator_test.rs +++ b/validation/core/src/validators/test/component_internal_api_validator_test.rs @@ -10,115 +10,19 @@ // // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use super::super::fixtures::*; use super::*; -use crate::models::{ - ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, - LogicRelation, -}; -use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; - -fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { - LogicRelation { - target: target.to_string(), - annotation: None, - relation_type: ComponentRelationType::InterfaceBinding, - source_role, - } -} - -fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { - let mut relations = Vec::new(); - for target in interface_targets { - relations.push(relation_with_role(target, EndpointRole::Required)); - 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, - } -} - -fn interface(alias: &str) -> LogicComponent { - LogicComponent { - id: alias.to_string(), - name: Some(alias.to_string()), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentType::Interface, - stereotype: None, - relations: Vec::new(), - } -} - -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(), - } -} - -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, - }]; - - let mut errors = Errors::default(); - let index = InternalApiIndex::build_index(&diagrams, &mut errors); - assert!(errors.is_empty()); - index -} +use crate::models::ComponentDiagramInputs; +use crate::ValidationResult; -fn validate(component_diagrams: ComponentDiagramInputs, internal_api: &InternalApiIndex) -> Errors { - let mut errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); +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, errors) + validate_component_internal_api(&component_arch, internal_api) } #[test] @@ -130,12 +34,12 @@ fn reports_missing_component_interface_declared_by_internal_api() { ]); let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface\"")); - assert!(!errors.messages[0].contains("Unit :")); + 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] @@ -148,11 +52,11 @@ fn reports_each_missing_component_interface_once() { ]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface1\"")); + 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] @@ -161,11 +65,11 @@ fn reports_missing_component_interface_even_without_unit_relation() { component_diagrams_with_entities(vec![unit("u1", &[]), interface("UnusedInterface")]); let internal_api = internal_api_index(vec![]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"UnusedInterface\"")); + 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] @@ -179,11 +83,11 @@ fn reports_all_missing_component_interfaces_in_one_message() { ]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface1\"")); + 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] @@ -194,11 +98,11 @@ fn reports_missing_component_interface_without_sequence_method_call() { ]); let internal_api = internal_api_index(vec![("OtherInterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface\"")); + 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] @@ -210,11 +114,11 @@ fn reports_case_mismatch_between_component_and_internal_api_interface_names() { ]); let internal_api = internal_api_index(vec![("internalinterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Missing interfaces : \"InternalInterface\"")); + 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] @@ -226,7 +130,7 @@ fn matches_internal_api_by_component_interface_id_when_alias_differs() { ]); let internal_api = internal_api_index(vec![("pkg.InternalInterface", vec!["GetData"])]); - let errors = validate(component_diagrams, &internal_api); + let validation_result = validate(component_diagrams, &internal_api); - assert!(errors.is_empty()); + 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 507c37e7..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,106 +10,21 @@ // // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use super::super::fixtures::*; use super::*; -use crate::models::{ - ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, - LogicRelation, SequenceDiagramInputs, -}; -use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; +use crate::models::{ComponentDiagramInputs, ComponentType, LogicComponent, SequenceDiagramInputs}; +use crate::ValidationResult; -fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { - relation_with_type_and_role(target, ComponentRelationType::InterfaceBinding, source_role) -} - -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, - } -} - -fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { - unit_with_interface_roles(alias, interface_targets, interface_targets) -} - -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, - } -} - -fn interface(alias: &str) -> LogicComponent { - LogicComponent { - id: alias.to_string(), - name: Some(alias.to_string()), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentType::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 sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { - sequence_calls( - &participants - .iter() - .map(|participant| (*participant, *participant, "")) - .collect::>(), - ) -} +fn validate( + component_diagrams: ComponentDiagramInputs, + sequence_diagrams: SequenceDiagramInputs, +) -> 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); + assert!(setup_result.is_empty()); -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(), - }], - } + validate_component_sequence(&component_arch, &sequence_index) } #[test] @@ -117,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(validation_result.is_empty()); } @@ -131,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(!validation_result.is_empty()); assert_eq!(validation_result.failures.len(), 3); @@ -171,12 +76,7 @@ fn units_without_alias_are_ignored() { }; 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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert!(validation_result.is_empty()); } @@ -185,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"u2\"")); } @@ -200,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0].contains("\"orphan\"")); } @@ -218,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -248,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -270,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 2); assert!(validation_result.failures.iter().any(|message| { @@ -299,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); + let validation_result = validate(component_diagrams, sequence_diagrams); assert_eq!(validation_result.failures.len(), 1); assert!(validation_result.failures[0] @@ -322,119 +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); - 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); - assert!(validation_result.is_empty()); -} - -#[test] -fn passes_when_multiple_consumers_share_interface_without_calling_each_other() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit_with_interface_roles("u1", &["InternalInterface"], &[]), - unit_with_interface_roles("u2", &[], &["InternalInterface"]), - unit_with_interface_roles("u3", &["InternalInterface"], &[]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()"), ("u3", "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); - 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); - - 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 ignores_source_roles_on_non_interface_binding_relations() { - let component_diagrams = component_diagrams_with_entities(vec![ - LogicComponent { - id: "some_id.u1".to_string(), - name: Some("u1".to_string()), - alias: Some("u1".to_string()), - parent_id: None, - element_type: ComponentType::Component, - stereotype: Some("unit".to_string()), - relations: vec![relation_with_type_and_role( - "InternalInterface", - ComponentRelationType::Dependency, - EndpointRole::Provided, - )], - }, - LogicComponent { - id: "some_id.u2".to_string(), - name: Some("u2".to_string()), - alias: Some("u2".to_string()), - parent_id: None, - element_type: ComponentType::Component, - stereotype: Some("unit".to_string()), - relations: vec![relation_with_type_and_role( - "InternalInterface", - ComponentRelationType::Dependency, - EndpointRole::Required, - )], - }, - 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); + 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 index 90e6593b..dde14262 100644 --- a/validation/core/src/validators/test/sequence_internal_api_validator_test.rs +++ b/validation/core/src/validators/test/sequence_internal_api_validator_test.rs @@ -11,82 +11,37 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use super::super::fixtures::*; use super::*; use crate::models::{ - ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, Errors, - LogicComponent, LogicRelation, SequenceDiagramInputs, + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicComponent, + SequenceDiagramInputs, }; -use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; -use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; - -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(), - } -} +use crate::ValidationResult; -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 errors = Errors::default(); - let index = InternalApiIndex::build_index(&diagrams, &mut errors); - assert!(errors.is_empty()); - index -} +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); -fn relation_with_role(target: &str, source_role: EndpointRole) -> LogicRelation { - LogicRelation { - target: target.to_string(), - annotation: None, - relation_type: ComponentRelationType::InterfaceBinding, - source_role, - } + validate_sequence_internal_api(&sequence_index, internal_api, None) } -fn unit(alias: &str, interface_targets: &[&str]) -> LogicComponent { - unit_with_interface_roles(alias, interface_targets, interface_targets) -} +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); -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)); - } + 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()), @@ -94,78 +49,22 @@ fn unit_with_interface_roles( parent_id: None, element_type: ComponentType::Component, stereotype: Some("unit".to_string()), - relations, + relations: vec![relation_with_type_and_role( + interface_id, + ComponentRelationType::Dependency, + EndpointRole::None, + )], } } -fn interface(alias: &str) -> LogicComponent { - LogicComponent { - id: alias.to_string(), - name: Some(alias.to_string()), - alias: Some(alias.to_string()), - parent_id: None, - element_type: ComponentType::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 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(), - }], - } -} - -fn validate(sequence_diagrams: SequenceDiagramInputs, internal_api: &InternalApiIndex) -> Errors { - let mut errors = Errors::default(); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - validate_sequence_internal_api(&sequence_index, internal_api, None, errors) -} - -fn validate_with_component_context( - component_diagrams: ComponentDiagramInputs, - sequence_diagrams: SequenceDiagramInputs, - internal_api: &InternalApiIndex, -) -> Errors { - let mut errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - validate_sequence_internal_api(&sequence_index, internal_api, Some(&component_arch), errors) -} - #[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 errors = validate(sequence_diagrams, &internal_api); + let validation_result = validate(sequence_diagrams, &internal_api); - assert!(errors.is_empty()); + assert!(validation_result.failures.is_empty()); } #[test] @@ -173,17 +72,17 @@ fn reports_internal_api_interface_function_not_exercised_without_method_name_che let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); - let errors = validate(sequence_diagrams, &internal_api); + let validation_result = validate(sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0] + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(errors.messages[0].contains("\"InternalInterface\"")); - assert!(errors.messages[0].contains("\"OtherMethod\"")); - assert!(errors - .messages + 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 violation"))); + .all(|message| !message.contains("Method consistency failure"))); } #[test] @@ -191,13 +90,13 @@ 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 errors = validate(sequence_diagrams, &internal_api); + let validation_result = validate(sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0] + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(errors.messages[0].contains("\"InternalInterface\"")); - assert!(errors.messages[0].contains("\"SetData\"")); + assert!(validation_result.failures[0].contains("\"InternalInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); } #[test] @@ -205,13 +104,13 @@ 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 errors = validate(sequence_diagrams, &internal_api); + let validation_result = validate(sequence_diagrams, &internal_api); - assert!(errors.is_empty()); + assert!(validation_result.failures.is_empty()); } #[test] -fn reports_sequence_function_missing_from_related_interface_methods_with_component_context() { +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"]), @@ -220,21 +119,48 @@ fn reports_sequence_function_missing_from_related_interface_methods_with_compone let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("sequence function name was not found in the related interface methods") + 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!(errors.messages.iter().any(|message| { + 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![ @@ -245,14 +171,14 @@ fn reports_interface_function_not_exercised_in_sequence_diagrams_with_component_ let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0] + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(errors.messages[0].contains("\"InternalInterface\"")); - assert!(errors.messages[0].contains("\"SetData\"")); + assert!(validation_result.failures[0].contains("\"InternalInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); } #[test] @@ -268,14 +194,14 @@ fn reports_unreferenced_internal_api_interface_function_not_exercised_without_se ("OtherInterface", vec!["SetData"]), ]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0] + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] .contains("internal API interface functions are not exercised in sequence diagrams")); - assert!(errors.messages[0].contains("\"OtherInterface\"")); - assert!(errors.messages[0].contains("\"SetData\"")); + assert!(validation_result.failures[0].contains("\"OtherInterface\"")); + assert!(validation_result.failures[0].contains("\"SetData\"")); } #[test] @@ -287,12 +213,12 @@ fn reports_self_call_method_mismatch_when_unit_has_missing_internal_api_interfac let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); let internal_api = internal_api_index(vec![]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages.iter().any(|message| { - message.contains("sequence self-call function name was not found") + 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\"") })); } @@ -307,10 +233,10 @@ fn passes_when_sequence_function_exists_on_related_interface_with_component_cont let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert!(errors.is_empty()); + assert!(validation_result.failures.is_empty()); } #[test] @@ -319,15 +245,15 @@ fn reports_self_call_function_missing_from_available_interfaces() { let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["OtherMethod"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("sequence self-call function name was not found") + 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!(errors.messages.iter().any(|message| { + 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\"") @@ -340,10 +266,10 @@ fn passes_when_self_call_uses_internal_api_interface_without_component_interface let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert!(errors.is_empty()); + assert!(validation_result.failures.is_empty()); } #[test] @@ -355,10 +281,10 @@ fn passes_when_all_interface_functions_are_exercised_by_self_calls() { let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()"), ("u1", "u1", "SetData()")]); let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert!(errors.is_empty()); + assert!(validation_result.failures.is_empty()); } #[test] @@ -367,12 +293,13 @@ fn reports_self_call_without_any_available_interfaces() { let sequence_diagrams = sequence_calls(&[("u1", "u1", "GetData()")]); let internal_api = internal_api_index(vec![]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("sequence self-call function name was not found")); - assert!(errors.messages[0].contains("Sequence call : \"u1\" -> \"u1\" : \"GetData\"")); + 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] @@ -389,21 +316,21 @@ fn reports_method_declared_only_on_caller_side_interfaces() { ("CallerOnlyInterface", vec!["GetData"]), ]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { + 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!(errors.messages.iter().any(|message| { + 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!(errors - .messages + assert!(validation_result + .failures .iter() .all(|message| !message.contains("Missing functions : \"GetData\""))); } @@ -422,21 +349,21 @@ fn reports_method_declared_only_on_callee_side_interfaces() { ("CalleeOnlyInterface", vec!["GetData"]), ]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { + 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!(errors.messages.iter().any(|message| { + 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!(errors - .messages + assert!(validation_result + .failures .iter() .all(|message| !message.contains("Missing functions : \"GetData\""))); } @@ -457,21 +384,68 @@ fn reports_method_declared_on_both_sides_but_not_on_shared_interface() { ("CalleeOnlyInterface", vec!["GetData"]), ]); - let errors = + let validation_result = validate_with_component_context(component_diagrams, sequence_diagrams, &internal_api); - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { + 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!(errors.messages.iter().any(|message| { + 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!(errors - .messages + 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 + ); +}