diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 8f958492..4efa5164 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -140,7 +140,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 c1d6d591..12e33858 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"], ) @@ -49,9 +51,13 @@ rust_library( "src/readers/mod.rs", "src/readers/sequence_diagram_reader.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/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 index ad079aaf..32235622 100644 --- 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 @@ -1,7 +1,7 @@ { "should_pass": false, "error_contains": [ - "Method consistency violation: Missing internal API interface", + "Internal API consistency violation: Missing internal API interface", "\"package_a.InternalInterface\"" ] } diff --git a/validation/core/src/models/component_diagram_models.rs b/validation/core/src/models/component_diagram_models.rs index e50a24ae..29746f5c 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -13,58 +13,45 @@ use std::collections::BTreeMap; +pub use component_diagram::{ + ComponentRelationType, ComponentType, EndpointRole, LogicComponent, LogicRelation, +}; + use super::{EntityKey, Errors}; -/// 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") } } @@ -74,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 { @@ -89,12 +76,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, @@ -107,10 +94,10 @@ impl ComponentDiagramArchitecture { /// `<>` go into `comp_set`; /// `<>` go into `unit_set`. /// Duplicates (same [`EntityKey`]) are reported via `errors`. - fn from_entities(entities: &[ComponentDiagramInput], errors: &mut Errors) -> Self { + fn from_entities(entities: &[LogicComponent], errors: &mut Errors) -> 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) { @@ -123,15 +110,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(); @@ -154,10 +141,10 @@ impl ComponentDiagramArchitecture { } fn build_set( - items: &[&ComponentDiagramInput], - id_index: &BTreeMap, + items: &[&LogicComponent], + id_index: &BTreeMap, errors: &mut Errors, - ) -> BTreeMap { + ) -> BTreeMap { let mut set = BTreeMap::new(); for entity in items { let alias = entity.match_key(); @@ -195,12 +182,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, } } @@ -208,12 +195,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, @@ -230,7 +218,7 @@ mod tests { "safety_software_seooc_example", Some("safety_software_seooc_example"), None, - ComponentDiagramElementType::Package, + ComponentType::Package, Some("SEooC"), Vec::new(), ), @@ -238,7 +226,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(), ), @@ -246,7 +234,7 @@ mod tests { "safety_software_seooc_example.InternalInterface", Some("InternalInterface"), Some("safety_software_seooc_example"), - ComponentDiagramElementType::Interface, + ComponentType::Interface, None, Vec::new(), ), @@ -254,7 +242,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 71bbab6f..17877cc0 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -28,10 +28,10 @@ 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 error_models::Errors; 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 34be0a1c..740276a5 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 super::Errors; -/// 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], errors: &mut Errors) -> Self { + fn from_diagrams(diagrams: &[SequenceTree], errors: &mut Errors) -> 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, errors); } } @@ -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 22baedd9..9832307a 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -16,7 +16,9 @@ use crate::models::{ InternalApiIndex, 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 serde::Deserialize; use super::profile::{merge_errors, read_and_convert, ProfileRun}; @@ -52,10 +54,24 @@ pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { if let (Some(component), Some(sequence)) = (component.as_ref(), sequence.as_ref()) { merge_errors( &mut errors, - validate_component_sequence( - component, + 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_errors( + &mut errors, + 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_errors( + &mut errors, + validate_sequence_internal_api( sequence, - internal_api.as_ref(), + internal_api, + component.as_ref(), Errors::default(), ), ); 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 e881b4e4..208feb21 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -226,8 +226,7 @@ impl<'a> BazelComponentValidator<'a> { mod tests { use super::*; use crate::models::{ - BazelInput, BazelInputEntry, ComponentDiagramElementType, ComponentDiagramInput, - ComponentDiagramInputs, + BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentType, LogicComponent, }; use std::collections::BTreeMap; @@ -250,15 +249,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, @@ -267,7 +267,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 69e4ffaf..6d2001e9 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -17,7 +17,7 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::models::{ - ComponentDiagramArchitecture, Errors, InternalApiIndex, InternalApiInterface, + ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, Errors, LogicComponentExt, SequenceDiagramIndex, }; @@ -25,16 +25,9 @@ use crate::models::{ pub fn validate_component_sequence( component_diagram: &ComponentDiagramArchitecture, sequence_diagram: &SequenceDiagramIndex, - internal_api_diagram: Option<&InternalApiIndex>, errors: Errors, ) -> Errors { - ComponentSequenceValidator::new( - component_diagram, - sequence_diagram, - internal_api_diagram, - errors, - ) - .run() + ComponentSequenceValidator::new(component_diagram, sequence_diagram, errors).run() } struct ComponentSequenceValidator<'a> { @@ -42,8 +35,6 @@ struct ComponentSequenceValidator<'a> { observed_call_contexts: Vec>, connected_unit_pairs: BTreeMap<(String, String), BTreeSet>, unit_bindings: BTreeMap, - all_interfaces: BTreeSet, - internal_api_interfaces_by_id: Option>, errors: Errors, } @@ -104,12 +95,9 @@ impl<'a> ComponentSequenceValidator<'a> { fn new( component_diagram: &ComponentDiagramArchitecture, sequence_diagram: &'a SequenceDiagramIndex, - internal_api_diagram: Option<&'a InternalApiIndex>, errors: Errors, ) -> 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); @@ -118,10 +106,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, - ), errors, } } @@ -157,27 +141,15 @@ impl<'a> ComponentSequenceValidator<'a> { for (unit_alias, bindings) in &self.unit_bindings { log.push_str(&format!( " {unit_alias} -> {}\n", - format_interface_names(&bindings.all_interfaces) + format_name_list(&bindings.all_interfaces) )); } - log.push_str(&format!( - "DEBUG: All interfaces for self-call validation:\n {}\n", - format_interface_names(&self.all_interfaces) - )); - - if let Some(internal_api_interfaces_by_id) = self.internal_api_interfaces_by_id.as_ref() { - log.push_str("DEBUG: Internal API interfaces checked for method validation:\n"); - for interface_id in internal_api_interfaces_by_id.keys() { - log.push_str(&format!(" {interface_id}\n")); - } - } - log.push_str("DEBUG: Interface-connected unit pairs from component diagrams:\n"); for ((left, right), interfaces) in &self.connected_unit_pairs { log.push_str(&format!( " {left} <-> {right} via {}\n", - format_interface_names(interfaces) + format_name_list(interfaces) )); } @@ -189,8 +161,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) { @@ -233,7 +203,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), )); } } @@ -279,8 +249,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), )); } } @@ -293,15 +263,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(), @@ -310,189 +281,33 @@ 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 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, + let role_related_interfaces = intersect_interfaces( + &role_interfaces(caller_bindings), + &role_interfaces(callee_bindings), ); - if !directional_interfaces.is_empty() { + if role_related_interfaces.is_empty() { continue; } - self.errors.push(format_sequence_role_consistency_error( - call_context, + let directional_interfaces = intersect_interfaces( &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.errors - .push(format_missing_internal_api_interface_error( - unit_alias, - missing_interfaces, - )); - } - - let mut seen_calls = BTreeSet::new(); - - for call_context in &self.observed_call_contexts { - let is_self_call = call_context.caller_unit == call_context.callee_unit; - - let method_name = extract_method_name(call_context.method); - if method_name.is_empty() { - continue; - } - - let call_key = ( - call_context.caller_unit.to_string(), - call_context.callee_unit.to_string(), - method_name.to_string(), ); - if !seen_calls.insert(call_key) { - continue; - } - - if is_self_call { - let matching_interfaces = matching_interfaces_with_method( - internal_api_interfaces_by_id, - &self.all_interfaces, - method_name, - ); - - if matching_interfaces.is_empty() { - self.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 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() { + if !directional_interfaces.is_empty() { continue; } - self.errors.push(format_sequence_method_consistency_error( + self.errors.push(format_sequence_role_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.errors.push(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), + &role_related_interfaces, )); } } - - fn collect_missing_internal_api_interfaces_by_unit( - &self, - internal_api_interfaces_by_id: &BTreeMap, - ) -> BTreeMap> { - self.unit_bindings - .iter() - .filter_map(|(unit_alias, bindings)| { - let missing_interfaces = missing_internal_api_interfaces( - internal_api_interfaces_by_id, - &bindings.all_interfaces, - ); - - if missing_interfaces.is_empty() { - None - } else { - Some((unit_alias.clone(), missing_interfaces)) - } - }) - .collect() - } - - fn collect_exercised_method_names(&self) -> BTreeSet { - let mut exercised_method_names = BTreeSet::new(); - - for call_context in &self.observed_call_contexts { - let method_name = extract_method_name(call_context.method); - if method_name.is_empty() { - continue; - } - - exercised_method_names.insert(method_name.to_string()); - } - - exercised_method_names - } } fn build_connected_unit_pairs( @@ -505,11 +320,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; @@ -551,14 +371,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 => {} } } @@ -568,33 +392,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, @@ -605,13 +402,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, @@ -643,94 +433,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 violation: sequence interaction does not match consumer/provider roles in the component diagram:\n\ @@ -740,26 +459,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 violation: {description}:\n\ - Sequence call : {sequence_call}\n\ - Action : {action}", + expected_interfaces = format_name_list(expected_interfaces), ) } @@ -771,23 +471,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 violation: 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 5709821c..a58dfdfb 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 errors = Errors::default(); - let index = InternalApiIndex::build_index(&diagrams, &mut errors); - assert!(errors.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(), }], } } @@ -186,7 +121,7 @@ fn passes_when_aliases_and_participants_are_identical() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert!(errors.is_empty()); } @@ -199,7 +134,7 @@ fn reports_missing_and_extra() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert!(!errors.is_empty()); assert_eq!(errors.messages.len(), 3); @@ -222,11 +157,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(), }], @@ -237,7 +173,7 @@ fn units_without_alias_are_ignored() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert!(errors.is_empty()); } @@ -250,7 +186,7 @@ fn reports_alias_missing_from_participants() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 1); assert!(errors.messages[0].contains("\"u2\"")); } @@ -264,7 +200,7 @@ fn reports_participant_not_in_aliases() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 1); assert!(errors.messages[0].contains("\"orphan\"")); } @@ -281,7 +217,7 @@ fn reports_missing_component_alias_and_interface_connection_for_sequence_call() let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 2); assert!(errors.messages.iter().any(|message| { @@ -310,7 +246,7 @@ fn reports_missing_sequence_call_for_interface_connected_units() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 1); assert!(errors.messages[0] @@ -331,7 +267,7 @@ fn reports_missing_participant_and_missing_sequence_call_for_interface_connected let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 2); assert!(errors.messages.iter().any(|message| { @@ -359,7 +295,7 @@ fn reports_sequence_call_without_corresponding_shared_interface_connection() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 1); assert!(errors.messages[0] @@ -381,7 +317,7 @@ fn passes_when_interface_connected_units_have_sequence_call() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert!(errors.is_empty()); } @@ -398,7 +334,25 @@ fn passes_when_sequence_call_matches_consumer_provider_roles() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert!(errors.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 errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert!(errors.is_empty()); } @@ -415,7 +369,7 @@ fn reports_cross_unit_sequence_call_with_invalid_consumer_provider_roles() { let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence(&component_arch, &sequence_index, None, errors); + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); assert_eq!(errors.messages.len(), 1); assert!( @@ -431,614 +385,46 @@ fn reports_cross_unit_sequence_call_with_invalid_consumer_provider_roles() { } #[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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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() { - let component_diagrams = component_diagrams_with_entities(vec![ - unit("u1", &["InternalInterface"]), - unit("u2", &["InternalInterface"]), - interface("InternalInterface"), - ]); - let sequence_diagrams = sequence_calls(&[("u1", "u2", "GetData()")]); - let internal_api = internal_api_index(vec![("InternalInterface", vec!["GetData", "SetData"])]); - - let mut errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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_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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); - assert!(errors.messages.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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Unit : \"u1\"")); - assert!(errors.messages[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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 1); - assert!(errors.messages[0].contains("Missing internal API interface")); - assert!(errors.messages[0].contains("Unit : \"u2\"")); - assert!(errors.messages[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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - let missing_internal_api_errors: Vec<&String> = errors - .messages - .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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - let missing_internal_api_errors: Vec<&String> = errors - .messages - .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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("sequence participant not found in component unit aliases") - && message.contains("\"orphan\"") - })); - assert!(errors.messages.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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"MissingInterface\"") - })); - 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() { - 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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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 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); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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() { +fn ignores_source_roles_on_non_interface_binding_relations() { 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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - 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_case_mismatch_between_component_and_internal_api_interface_names() { - 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 errors = Errors::default(); - let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, - ); - - assert_eq!(errors.messages.len(), 2); - assert!(errors.messages.iter().any(|message| { - message.contains("Missing internal API interface") - && message.contains("Unit : \"u1\"") - && message.contains("Missing interfaces : \"InternalInterface\"") - })); - assert!(errors.messages.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 errors = Errors::default(); let component_arch = component_diagrams.to_diagram_architecture(&mut errors); let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); - let errors = validate_component_sequence( - &component_arch, - &sequence_index, - Some(&internal_api), - errors, + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert!( + errors.is_empty(), + "Expected pass, got: {:?}", + errors.messages ); - - assert!(errors.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\""))); +}