diff --git a/bazel/rules/rules_score/examples/minimal/BUILD b/bazel/rules/rules_score/examples/minimal/BUILD index 72ca078a..42fd89ad 100644 --- a/bazel/rules/rules_score/examples/minimal/BUILD +++ b/bazel/rules/rules_score/examples/minimal/BUILD @@ -87,7 +87,7 @@ unit( component( name = "MyComponent", components = [":MyUnit"], - requirements = [], + requirements = [":feature_requirements"], tests = [], ) diff --git a/bazel/rules/rules_score/examples/minimal/docs/class_design.puml b/bazel/rules/rules_score/examples/minimal/docs/class_design.puml index 96ec0003..9bb9310c 100644 --- a/bazel/rules/rules_score/examples/minimal/docs/class_design.puml +++ b/bazel/rules/rules_score/examples/minimal/docs/class_design.puml @@ -15,10 +15,16 @@ ' Class diagram for MyUnit — internal design of the key-value store unit. -class MyUnit { - - store_ : std::unordered_map - + configure(key : const string&, value : const string&) : void - + get(key : const string&) : string +namespace my_element { + namespace MyComponent { + namespace MyUnit { + class KeyValueStore { + - store_ : std::unordered_map + + configure(key : const string&, value : const string&) : void + + get(key : const string&) : string + } + } + } } @enduml diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp b/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp index 7c11009e..e8f1022a 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit.cpp @@ -11,13 +11,22 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "src/my_unit.h" +#include "my_unit.h" -void MyUnit::configure(const std::string &key, const std::string &value) { +namespace my_element { +namespace MyComponent { +namespace MyUnit { + +void KeyValueStore::configure(const std::string &key, + const std::string &value) { store_[key] = value; } -std::string MyUnit::get(const std::string &key) const { +std::string KeyValueStore::get(const std::string &key) const { const auto it = store_.find(key); return it != store_.end() ? it->second : ""; } + +} // namespace MyUnit +} // namespace MyComponent +} // namespace my_element diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit.h b/bazel/rules/rules_score/examples/minimal/src/my_unit.h index 10160d0c..763c3b30 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit.h +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit.h @@ -16,7 +16,11 @@ #include #include -class MyUnit { +namespace my_element { +namespace MyComponent { +namespace MyUnit { + +class KeyValueStore { public: // trace: MinimalExample.FEAT_001 void configure(const std::string &key, const std::string &value); @@ -27,3 +31,7 @@ class MyUnit { private: std::unordered_map store_; }; + +} // namespace MyUnit +} // namespace MyComponent +} // namespace my_element diff --git a/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp b/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp index 5c8a1204..51ee6b0d 100644 --- a/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp +++ b/bazel/rules/rules_score/examples/minimal/src/my_unit_test.cpp @@ -13,15 +13,15 @@ #include -#include "src/my_unit.h" +#include "my_unit.h" TEST(MyUnitTest, ConfigureAndGet) { ::testing::Test::RecordProperty( - "lobster-tracing", "MinimalExample.FEAT_001 MinimalExample.FEAT_002"); + "lobster-tracing", "MinimalExample.FEAT_001,MinimalExample.FEAT_002"); ::testing::Test::RecordProperty("given", - "a default-constructed MyUnit instance"); - MyUnit unit{}; + "a default-constructed MyUnit::KeyValueStore instance"); + my_element::MyComponent::MyUnit::KeyValueStore unit{}; ::testing::Test::RecordProperty( "when", @@ -36,8 +36,8 @@ TEST(MyUnitTest, MissingKeyReturnsEmpty) { ::testing::Test::RecordProperty("lobster-tracing", "MinimalExample.FEAT_002"); ::testing::Test::RecordProperty("given", - "a default-constructed MyUnit instance"); - MyUnit unit{}; + "a default-constructed MyUnit::KeyValueStore instance"); + my_element::MyComponent::MyUnit::KeyValueStore unit{}; ::testing::Test::RecordProperty( "when", "When get is called with a key that hasn't been configured yet"); diff --git a/bazel/rules/rules_score/examples/seooc/design/static_design.puml b/bazel/rules/rules_score/examples/seooc/design/static_design.puml index 32de5545..c6c71cc7 100644 --- a/bazel/rules/rules_score/examples/seooc/design/static_design.puml +++ b/bazel/rules/rules_score/examples/seooc/design/static_design.puml @@ -20,7 +20,7 @@ component component_example <> { interface InternalInterface unit_1 -l-( InternalInterface - unit_2 -r- InternalInterface + unit_2 )-r- InternalInterface } @enduml diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml index 6170a9c9..eb0cad86 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_1/docs/unit_1_class_diagram.puml @@ -13,11 +13,19 @@ @startuml unit_1_class_diagram -namespace unit_1 { - class Foo { +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(BindingType binding) : safety_software_seooc_example::component_example::Data* + } - + GetNumber() : uint8_t - + SetNumber(value : uint8_t) : void + namespace unit_1 { + class Foo { + + + GetNumber() : uint8_t + + SetNumber(value : uint8_t) : void + } + } } } diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp b/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp index 65c52d8a..91338cb8 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo.cpp @@ -13,8 +13,13 @@ #include "unit_1/foo.h" +namespace safety_software_seooc_example { +namespace component_example { namespace unit_1 { // trace: SampleComponent.REQ_COMP_001 SampleLibraryAPI.GetNumber std::uint8_t Foo::GetNumber() const { return 42u; } + } // namespace unit_1 +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h index 30457e8c..6fb75f4e 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo.h +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo.h @@ -16,6 +16,16 @@ #include +namespace safety_software_seooc_example { +namespace component_example { +struct BindingType; +struct Data; + +class InternalInterface { +public: + virtual Data *GetData(BindingType binding) = 0; +}; + namespace unit_1 { // trace: SampleComponent.REQ_COMP_002 @@ -28,5 +38,7 @@ class Foo final { }; } // namespace unit_1 +} // namespace component_example +} // namespace safety_software_seooc_example #endif // FOO_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp index a8323f28..883e38e5 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp @@ -17,6 +17,8 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; + TEST(Foo, GetNumber) { ::testing::Test::RecordProperty("lobster-tracing", "SampleComponent.REQ_COMP_001"); diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp b/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp index d4c3c584..f0bf40c1 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar.cpp @@ -19,10 +19,13 @@ namespace { std::uint8_t kExpectedNumber = 42u; } +namespace safety_software_seooc_example { +namespace component_example { namespace unit_2 { // trace: SampleComponent.REQ_COMP_004 SampleComponent.REQ_COMP_005 -Bar::Bar(std::unique_ptr foo) : foo_{std::move(foo)} {} +Bar::Bar(std::unique_ptr foo) + : foo_{std::move(foo)} {} // trace: SampleComponent.REQ_COMP_003 bool Bar::AssertNumber() const { @@ -30,3 +33,5 @@ bool Bar::AssertNumber() const { return foo_->GetNumber() == kExpectedNumber; } } // namespace unit_2 +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar.h b/bazel/rules/rules_score/examples/seooc/unit_2/bar.h index 6f24c0ee..c85bc8d8 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar.h +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar.h @@ -19,19 +19,23 @@ #include #include +namespace safety_software_seooc_example { +namespace component_example { namespace unit_2 { // trace: SampleComponent.REQ_COMP_003 SampleComponent.REQ_COMP_004 // SampleComponent.REQ_COMP_005 class Bar final { public: - explicit Bar(std::unique_ptr foo); + explicit Bar(std::unique_ptr foo); bool AssertNumber() const; private: - std::unique_ptr foo_; + std::unique_ptr foo_; }; } // namespace unit_2 +} // namespace component_example +} // namespace safety_software_seooc_example #endif // BAR_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp index eaa5fc8a..c5522966 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_2/bar_test.cpp @@ -15,6 +15,9 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; +namespace unit_2 = safety_software_seooc_example::component_example::unit_2; + TEST(Bar, AssertNumber) { ::testing::Test::RecordProperty( "lobster-tracing", "SampleComponent.REQ_COMP_003,SampleComponent.REQ_" diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml index fafab5f2..d7944d7c 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml @@ -13,15 +13,20 @@ @startuml unit_2_class_diagram -' Detail of `Bar`. No namespace: the declaration matches the bodyless `Bar` of -' unit_2_overview.puml (name, element type and stereotype) and is anchored onto -' `unit_2`. -class Bar <> { - -- - - foo_ : unique_ptr - -- - + Bar(foo : unique_ptr) - + AssertNumber() : bool +' Detail of Bar; Foo and the uses relationship are declared in +' unit_2_overview.puml. +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + class Bar <> { + -- + - foo_ : unique_ptr + -- + + Bar(foo : unique_ptr) + + AssertNumber() : bool + } + } + } } @enduml diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml index bdf08aa8..44bea2c9 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_overview.puml @@ -17,12 +17,16 @@ ' All classes are references (no body); `Bar` is elaborated in ' unit_2_class_diagram.puml, `Foo` is owned and elaborated by unit_1. -namespace unit_1 { - class Foo <> -} +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Foo <> + } -namespace unit_2 { - class Bar <> + namespace unit_2 { + class Bar <> + } + } } Bar --> Foo : uses diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml index b8d504b4..4fc3a9b0 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_3/docs/unit_3_class_diagram.puml @@ -13,9 +13,15 @@ @startuml unit_3_class_diagram -namespace unit_3 { - class Formatter <> { - + Format(foo : const unit_1::Foo &) : std::string +namespace safety_software_seooc_example { + namespace component_example { + namespace sub_component_example { + namespace unit_3 { + class Formatter <> { + + Format(foo : const safety_software_seooc_example::component_example::unit_1::Foo &) : std::string + } + } + } } } diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp index 688bb7c9..271310ae 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.cpp @@ -13,11 +13,18 @@ #include "unit_3/formatter.h" +namespace safety_software_seooc_example { +namespace component_example { +namespace sub_component_example { namespace unit_3 { // trace: SampleComponent.REQ_COMP_006 -std::string Formatter::Format(const unit_1::Foo &foo) const { +std::string Formatter::Format( + const safety_software_seooc_example::component_example::unit_1::Foo &foo) const { return std::to_string(foo.GetNumber()); } } // namespace unit_3 +} // namespace sub_component_example +} // namespace component_example +} // namespace safety_software_seooc_example diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h index 1cd0e435..f851d99d 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter.h @@ -18,14 +18,21 @@ #include +namespace safety_software_seooc_example { +namespace component_example { +namespace sub_component_example { namespace unit_3 { class Formatter final { public: // trace: SampleComponent.REQ_COMP_006 - std::string Format(const unit_1::Foo &foo) const; + std::string Format( + const safety_software_seooc_example::component_example::unit_1::Foo &foo) const; }; } // namespace unit_3 +} // namespace sub_component_example +} // namespace component_example +} // namespace safety_software_seooc_example #endif // FORMATTER_H diff --git a/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp index 6a636763..c1ee0a35 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_3/formatter_test.cpp @@ -15,6 +15,9 @@ #include +namespace unit_1 = safety_software_seooc_example::component_example::unit_1; +namespace unit_3 = safety_software_seooc_example::component_example::sub_component_example::unit_3; + TEST(Formatter, Format) { ::testing::Test::RecordProperty("lobster-tracing", "SampleComponent.REQ_COMP_006"); diff --git a/bazel/rules/rules_score/examples/some_other_library/BUILD b/bazel/rules/rules_score/examples/some_other_library/BUILD index 1b185bc4..91428b89 100644 --- a/bazel/rules/rules_score/examples/some_other_library/BUILD +++ b/bazel/rules/rules_score/examples/some_other_library/BUILD @@ -22,6 +22,7 @@ load( "fault_trees", "safety_analysis", "unit", + "unit_design", ) architectural_design( @@ -71,12 +72,17 @@ unit( name = "abc", scope = ["//:__pkg__"], tests = [], - unit_design = [], + unit_design = [":abc_design"], implementation = [ ":some_other_library", ], ) +unit_design( + name = "abc_design", + static = ["abc_class_design.puml"], +) + component( name = "component_example", components = [ diff --git a/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml b/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml new file mode 100644 index 00000000..d68ca28a --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/abc_class_design.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml abc_class_design + +namespace other_seooc { + namespace component_example { + namespace abc { + class AbcImpl + } + } +} + +@enduml diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 8dd2e1d7..26157e08 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1009,17 +1009,28 @@ def _collect_architecture_components(ctx): return all_components -def _run_validation(ctx, arch_json, static_fbs_files): +def _run_validation(ctx, arch_json, static_fbs_files, internal_api_fbs_files, unit_design_class_fbs_by_label): """Run the dependable-element validation profile. Args: ctx: Rule context arch_json: The architecture JSON File object (already declared and written) static_fbs_files: Component-diagram FlatBuffer files from architectural_design targets. + internal_api_fbs_files: Internal API FlatBuffer files from architectural_design targets. + unit_design_class_fbs_by_label: Dict from Bazel unit labels to their class-diagram FlatBuffer files. Returns: validation_log File object """ + unit_design_class_fbs = [ + depset(fbs_files) + for fbs_files in unit_design_class_fbs_by_label.values() + ] + action_inputs = depset( + direct = [arch_json] + static_fbs_files + internal_api_fbs_files, + transitive = unit_design_class_fbs, + ).to_list() + return run_validation( ctx = ctx, validation_cli = ctx.executable._validation_cli, @@ -1027,8 +1038,13 @@ def _run_validation(ctx, arch_json, static_fbs_files): input_bundle = { "architecture": arch_json.path, "component_diagrams": [f.path for f in static_fbs_files], + "internal_api_diagrams": [f.path for f in internal_api_fbs_files], + "unit_design_class_diagrams": { + unit_label: [f.path for f in fbs_files] + for unit_label, fbs_files in unit_design_class_fbs_by_label.items() + }, }, - inputs = [arch_json] + static_fbs_files, + inputs = action_inputs, mnemonic = "DependableElementValidate", maturity = ctx.attr.maturity, log_level = get_log_level(ctx), @@ -1324,14 +1340,30 @@ def _dependable_element_index_impl(ctx): # Collect static FlatBuffers from architectural_design targets (the expected # static architecture) and verify them against the current architecture. static_fbs_files = [] + internal_api_fbs_files = [] validation_logs = [] for ad in ctx.attr.architectural_design: if ArchitecturalDesignInfo in ad: static_fbs_files.extend(ad[ArchitecturalDesignInfo].static.to_list()) + internal_api_fbs_files.extend(ad[ArchitecturalDesignInfo].internal_api.to_list()) validation_logs.extend(ad[ArchitecturalDesignInfo].validation_logs) + unit_design_class_fbs_by_label = {} + for unit_target in all_units.values(): + unit_design_class_fbs_by_label[str(unit_target.label)] = ( + unit_target[UnitInfo].unit_design_static_fbs.to_list() + ) + # Run validation; build fails automatically on non-zero exit - validation_logs.append(_run_validation(ctx, arch_json, static_fbs_files)) + validation_logs.append( + _run_validation( + ctx, + arch_json, + static_fbs_files, + internal_api_fbs_files, + unit_design_class_fbs_by_label, + ), + ) validation_output_files = [] for log in validation_logs: diff --git a/validation/core/BUILD b/validation/core/BUILD index 7381e77c..fe302e4e 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -22,6 +22,7 @@ exports_files( filegroup( name = "specifications", srcs = [ + "docs/specifications/architectural_unit_class_design.md", "docs/specifications/bazel_component.md", "docs/specifications/class_design_implementation.md", "docs/specifications/class_design_sequence.md", @@ -58,6 +59,7 @@ rust_library( "src/results/diagnostics.rs", "src/results/error_models.rs", "src/results/mod.rs", + "src/validators/architectural_unit_class_design_validator.rs", "src/validators/bazel_component_validator.rs", "src/validators/class_design_implementation_validator.rs", "src/validators/class_design_sequence_validator.rs", diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index 33c92771..73f03ea0 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -221,6 +221,40 @@ section "Tool Requirements" { } + section "Architectural Unit Class Design Validator" { + + ToolQualification.ToolRequirement ArchitecturalUnitClassDesignConsistency { + description = '''The validator shall report an error when a + unit declared in the architectural component diagram has no + corresponding class, struct, interface, or abstract-class + entity in the static class diagram of the unit design bound to + that unit. An enum alone shall not satisfy this requirement. + + A corresponding entity shall have a canonical identifier with + the architectural unit identifier as a complete path prefix, + followed by at least one additional entity-name segment. + References to entities outside that path shall not constitute + corresponding entities.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + ToolQualification.ToolRequirement ArchitecturalUnitInternalApiClassDesignConsistency { + description = '''When an interface declared in the component + diagram is also specified in the Internal API diagram and is + required by one or more units, the validator shall report an + error if any requiring unit's static class diagram does not + declare a corresponding interface containing every method + name specified by the Internal API interface. Units that + provide the interface are not checked by this requirement. + Interfaces with no required unit bindings are outside this + consistency check.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + } + section "Class Design Implementation Validator" { ToolQualification.ToolRequirement ClassDesignImplementationEntityConsistency { diff --git a/validation/core/docs/specifications/architectural_unit_class_design.md b/validation/core/docs/specifications/architectural_unit_class_design.md new file mode 100644 index 00000000..4cf63e32 --- /dev/null +++ b/validation/core/docs/specifications/architectural_unit_class_design.md @@ -0,0 +1,252 @@ + + +# Architectural Unit Class Design Specification + +## Purpose + +This validator enforces refinement consistency between a unit declared in an +architectural component diagram and the static class diagram of the +`unit_design` bound to that unit. + +It shall make sure that each architectural unit is refined by at least one +class, struct, interface, or abstract class owned by that unit in its detailed +design. Enums alone do not satisfy this refinement requirement. + +It shall also make sure that an interface declared in the component diagram +and specified in the Internal API diagram is represented in the detailed class +design of every unit that requires that interface. The detail-design interface shall +contain every method name specified by the Internal API. Provider units are +not checked by this rule because they do not consume the interface contract. +This check validates the required-side design model; it does not claim that the +provider implements the interface in C++. + +## What is Validated + +The validator uses four architectural inputs, with the detailed class design +associated through the Bazel `unit` target: + +| Input | Source | Meaning | +|---|---|---| +| Architectural unit | `architectural_design.static` component diagram | A `<>` entity and its canonical identifier | +| Detailed class design | `unit_design.static` class diagram bound to that Bazel `unit` | Class, struct, interface, and abstract-class entities that refine the unit | +| Component interfaces and bindings | `architectural_design.static` component diagram | Interfaces and the units bound to each interface | +| Internal API | `architectural_design.internal_api` class diagram | Interface entities and their method names | + +The existing Bazel-component validation establishes the mapping from an +architectural `<>` to its Bazel `unit` target. This validator then uses +that target's `unit_design` binding to select the detailed class diagram to +compare. It does not validate the Bazel-to-component mapping itself. + +### Unit-Class Refinement Consistency + +For every architectural `<>`, the static class diagrams bound to its +corresponding Bazel `unit` target must declare at least one class, struct, +interface, or abstract class in that unit's canonical identifier path. Matching +uses the full architectural unit ID as a complete path prefix, followed by at +least one entity-name segment. An enum does not satisfy this requirement. + +*(Requirement: {requirement:downstream-ref}`Tools.ArchitecturalUnitClassDesignConsistency`)* + +For example, `Filter` is an architectural unit with canonical identifier +`system.control.Filter`. Its Bazel target has design name `Filter`; the unit +design bound to that target refines it when it declares an entity such as +`system.control.Filter.FilterService`. + +```text +' architectural component diagram +package system { + package control { + component Filter <> + } +} +``` + +```text +@startuml +namespace system { + namespace control { + namespace Filter { + class FilterService + } + } +} +@enduml +``` + +The following does not refine `system.control.Filter`, because it has the same +leaf name under a different parent path: + +```text +@startuml +namespace system { + namespace sensor { + namespace Filter { + class SensorDriver + } + } +} +@enduml +``` + +Class diagrams are evaluated only within the class-diagram files bound to the +matching Bazel unit. A collaborator stub in another unit's namespace does not +refine the current unit. + +References to external entities are permitted, but do not satisfy this rule. +For example, a relationship from `FilterService` to +`system.sensor.Sensor.SensorDriver` is an external reference; only an eligible +entity declared in `Filter`'s unit design can satisfy the requirement. + +### Identifier Matching + +Identifiers are compared as canonical identifier paths. The eligible entity +identifier must begin with the full architectural unit ID followed by `.` and +at least one additional entity-name segment. A bare or lexical prefix match is not +sufficient: `system.control.Filtering.Service` does not refine +`system.control.Filter`. + +Aliases and display names do not take part in matching. + +### Bound Interface and Method Consistency + +For each interface declared in the component diagram and also declared in the +Internal API diagram, the validator shall inspect every unit whose interface +binding role is `Required`. Each such unit's bound static class diagram shall +declare a corresponding interface entity and that detail-design interface +shall contain every method name declared by the Internal API interface. + +Units whose binding role is `Provided` are not checked by this rule. The +Internal API and component-interface consistency checks establish the existence +of the shared contract; this check ensures that a consumer's unit design +represents the contract it requires. It does not prove that a provider exists +or implements the interface in C++. + +The component interface and Internal API interface are matched by canonical +identifier, including their enclosing package/component context. Matching only +by the interface's final name segment across the diagrams is insufficient. +Within each Required unit's detail class diagram, the corresponding entity +must be an `Interface` declaration with the same canonical interface +identifier as the Internal API interface. The interface remains in the shared package/component +namespace; it is not moved beneath the unit namespace. Each Required unit's +diagram is checked independently, so repeating the same interface declaration +in different unit-design files does not make one unit's declaration satisfy +another Required unit's check. A collaborator stub or an interface declaration +that only appears in another unit's design does not satisfy the check. If +multiple detail interfaces in the same Required unit match the canonical +identifier, the validator shall report an ambiguity. + +Method names are compared case-sensitively. This rule does not compare method +parameters, return types, visibility, modifiers, or overload signatures. +Additional methods in the detail-design interface are permitted. + +If the component interface is not declared in the Internal API diagram, the +Component Internal API validator reports that issue; this rule does not report +it again. An Internal API interface with no Required unit bindings is outside +this check because there is no consumer unit detail class diagram to inspect. +The absence of a provider or requester role by itself is not an error under +this rule. + +For example, the component diagram below binds both `unit_1` and `unit_2` to +`InternalInterface`: + +```text +' component diagram +package example <> { + component service <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +``` + +The Internal API diagram defines the interface method: + +```text +' internal API diagram +namespace example { + namespace service { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +``` + +Only `unit_1` requires the interface, so only its detail class diagram must +declare the corresponding interface and `GetData()`. For example, this +declaration in `unit_1`'s design satisfies the interface consistency check. +The same diagram must also contain at least one refining entity below the +`unit_1` namespace to satisfy Unit-Class Refinement Consistency: + +```text +' unit_1 detail class diagram +namespace example { + namespace service { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_1 { + class Unit1Service + } + } +} +``` + +`unit_2` provides the interface, so this rule does not require its detail +class diagram to redeclare the shared contract. If `unit_1`'s design omits +`InternalInterface` or declares it without `GetData()`, validation fails for +`unit_1`. A provider implementation check is outside the scope of this rule. + +*(Requirement: {requirement:downstream-ref}`Tools.ArchitecturalUnitInternalApiClassDesignConsistency`)* + +### Not Validated Here + +This validator does not validate: + +- the correspondence between architectural units and Bazel `unit` targets; + that is covered by the Bazel Component validator. +- component-diagram interface declarations against public or internal API + diagrams; that is covered by the Component Public API and Component Internal + API validators. +- method signatures, modifiers, or class-design entities against C++ + implementation code; that is covered by the Class Design Implementation + validator. This specification checks only the presence of the bound + interface and Internal API method names in the unit's detail class design. +- whether external class references are allowed architectural dependencies. +- whether the Provider exists or implements the interface; this rule validates + only the Required-side unit-design contract. + +## Failure Cases + +| Failure case | Validation rule | +|---|---| +| The bound static unit-design class diagrams declare no eligible entity below the full architectural unit ID | Unit-Class Refinement Consistency | +| The bound static unit-design class diagrams only declare eligible entities below another architectural unit ID | Unit-Class Refinement Consistency | +| The bound static unit-design class diagrams only reference external entities | Unit-Class Refinement Consistency | +| A unit with a Required binding to a component interface has no corresponding interface entity in its own detail class design | Bound Interface and Method Consistency | +| A Required unit's corresponding detail-design interface omits one or more Internal API method names | Bound Interface and Method Consistency | +| A Required unit's matching interface exists only in another unit's class design or as a collaborator stub | Bound Interface and Method Consistency | +| Multiple detail interfaces in one Required unit ambiguously match the component interface | Bound Interface and Method Consistency | + +## Debug Output + +The validator shall emit debug output containing: + +- the architectural unit identifier; +- the Bazel `unit` target and its bound `unit_design` target; +- the eligible entity identifiers read from the bound static class diagrams; +- the eligible entity identifiers selected as refinements of each architectural unit. diff --git a/validation/core/integration_test/architectural_unit_class_design/BUILD b/validation/core/integration_test/architectural_unit_class_design/BUILD new file mode 100644 index 00000000..7c20a0d6 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/BUILD @@ -0,0 +1,39 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_test") + +filegroup( + name = "architectural_unit_class_design_test_data", + srcs = [ + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface:case_data", + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_class:case_data", + "//validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method:case_data", + "//validation/core/integration_test/architectural_unit_class_design/negative_path_prefix:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_refining_class:case_data", + "//validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types:case_data", + ], +) + +rust_test( + name = "architectural_unit_class_design_integration_test", + srcs = ["architectural_unit_class_design_suite.rs"], + crate_root = "architectural_unit_class_design_suite.rs", + data = [":architectural_unit_class_design_test_data"], + deps = [ + "//validation/core:validation_cli", + "//validation/core/integration_test:test_framework", + "@crates_score_tooling//:serde_json", + ], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs new file mode 100644 index 00000000..1f9b693f --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/architectural_unit_class_design_suite.rs @@ -0,0 +1,140 @@ +// ******************************************************************************* +// 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 serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use test_framework::{ + assert_cli_result, case_file_path, collect_case_fbs_files, load_expected_yaml_fixture, + normalize_yaml_result, run_validation_profile, CliRunResult, +}; + +const SUITE_DIR: &str = "architectural_unit_class_design"; + +fn collect_unit_design_class_fbs( + architecture_path: &Path, + class_fbs_paths: &[String], +) -> BTreeMap> { + let architecture: Value = serde_json::from_slice( + &std::fs::read(architecture_path).expect("failed to read architecture fixture"), + ) + .expect("failed to parse architecture fixture"); + let components = architecture["components"] + .as_object() + .expect("architecture fixture must contain a components object"); + + let mut class_fbs_by_unit_label = BTreeMap::new(); + let mut assigned_paths = BTreeSet::new(); + for component in components.values() { + let Some(units) = component.get("units").and_then(Value::as_array) else { + continue; + }; + + for unit in units { + let label = unit["label"] + .as_str() + .expect("architecture unit must have a label"); + let design_name = unit["design_name"] + .as_str() + .expect("architecture unit must have a design_name"); + let file_prefix = format!("{design_name}_"); + let unit_class_fbs = class_fbs_paths + .iter() + .filter(|path| { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with(&file_prefix)) + }) + .cloned() + .collect::>(); + + for path in &unit_class_fbs { + assert!( + assigned_paths.insert(path.clone()), + "class-diagram FlatBuffer {path} matches multiple unit design names" + ); + } + class_fbs_by_unit_label.insert(label.to_string(), unit_class_fbs); + } + } + + assert_eq!( + assigned_paths.len(), + class_fbs_paths.len(), + "every unit-design class diagram must match a unit design_name" + ); + class_fbs_by_unit_label +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let internal_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "internal_api"); + let class_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "unit_design_class"); + let architecture_path = case_file_path(&format!( + "validation/core/integration_test/{SUITE_DIR}/{case_dir}/architecture.json" + )); + let unit_design_class_diagrams = + collect_unit_design_class_fbs(&architecture_path, &class_fbs_paths); + + let result: CliRunResult = run_validation_profile( + &format!("architectural_unit_class_design_{case_dir}"), + "dependable-element", + serde_json::json!({ + "architecture": architecture_path.display().to_string(), + "component_diagrams": component_fbs_paths, + "internal_api_diagrams": internal_api_fbs_paths, + "unit_design_class_diagrams": unit_design_class_diagrams, + }), + ); + + let result = normalize_yaml_result(result); + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn positive_refining_class() { + assert_case("positive_refining_class"); +} + +#[test] +fn positive_refining_supported_entity_types() { + assert_case("positive_refining_supported_entity_types"); +} + +#[test] +fn negative_path_prefix() { + assert_case("negative_path_prefix"); +} + +#[test] +fn negative_missing_bound_interface() { + assert_case("negative_missing_bound_interface"); +} + +#[test] +fn negative_missing_internal_api_method() { + assert_case("negative_missing_internal_api_method"); +} + +#[test] +fn negative_missing_class() { + assert_case("negative_missing_class"); +} + +#[test] +fn positive_internal_api_class_design() { + assert_case("positive_internal_api_class_design"); +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml new file mode 100644 index 00000000..9fc694e1 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/expected.yaml @@ -0,0 +1,25 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: | + [Interface] Bound unit "safety_software_seooc_example.component_example.unit_1" has no corresponding interface "safety_software_seooc_example.component_example.InternalInterface" in its unit design. + Component interface : "safety_software_seooc_example.component_example.InternalInterface" + Bound unit : "safety_software_seooc_example.component_example.unit_1" + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Component source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/component_diagram.puml" + Component source line : 19 + Internal API source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml" + Internal API source line : 17 + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml" + Design source line : 18 + Fix : Declare interface "InternalInterface" in the unit design for "safety_software_seooc_example.component_example.unit_1" using the Internal API namespace. diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/internal_api.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml new file mode 100644 index 00000000..7b3ab28c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_1_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml new file mode 100644 index 00000000..a724306b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_bound_interface/unit_2_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD new file mode 100644 index 00000000..259ff4b0 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/BUILD @@ -0,0 +1,35 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [":architecture"], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/component_diagram.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml new file mode 100644 index 00000000..27f4c082 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_class/expected.yaml @@ -0,0 +1,26 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: + - | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_1" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_1 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Unit design entities : + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_1". + - | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_2" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_2 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_2:unit_2 + Unit design entities : + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_2". diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml new file mode 100644 index 00000000..f69fd42e --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/expected.yaml @@ -0,0 +1,26 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: | + [Method] Unit design interface "safety_software_seooc_example.component_example.InternalInterface" is missing Internal API method(s) "GetData". + Component interface : "safety_software_seooc_example.component_example.InternalInterface" + Bound unit : "safety_software_seooc_example.component_example.unit_1" + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Detail interface : "safety_software_seooc_example.component_example.InternalInterface" + Component source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/component_diagram.puml" + Component source line : 19 + Internal API source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml" + Internal API source line : 17 + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml" + Design source line : 17 + Fix : Add "GetData" to the detail interface, matching the Internal API method names exactly. diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/internal_api.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml new file mode 100644 index 00000000..5b99e878 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_1_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} SetData(Data*): void + } + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml new file mode 100644 index 00000000..bcd3dbb1 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_missing_internal_api_method/unit_2_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} SetData(Data*): void + } + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD new file mode 100644 index 00000000..85f1e266 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/component_diagram.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml new file mode 100644 index 00000000..3579992f --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/expected.yaml @@ -0,0 +1,21 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: | + [Design] Architectural unit "safety_software_seooc_example.component_example.unit_1" has no refining entity in its unit design. + Architectural unit : safety_software_seooc_example.component_example.unit_1 + Bazel unit : @//bazel/rules/rules_score/examples/seooc/unit_1:unit_1 + Entity : "safety_software_seooc_example.component_example.unit_1_extra.Service" + Design source file : "validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml" + Design source line : 18 + Fix : Add a class, struct, interface, or abstract class whose ID is below the architectural unit ID "safety_software_seooc_example.component_example.unit_1". diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml new file mode 100644 index 00000000..c4a0471f --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_1_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1_extra { + class Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml new file mode 100644 index 00000000..bbc5e0bf --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/negative_path_prefix/unit_2_class_diagram.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service <> + } + + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD new file mode 100644 index 00000000..a4aa596b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/BUILD @@ -0,0 +1,50 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + internal_api = ["internal_api.puml"], + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml new file mode 100644 index 00000000..b1eaa692 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/component_diagram.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + interface InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml new file mode 100644 index 00000000..32c4cf5c --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/internal_api.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml new file mode 100644 index 00000000..a32454f7 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_1_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml new file mode 100644 index 00000000..a41cc8ab --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_internal_api_class_design/unit_2_class_diagram.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + interface InternalInterface <> { + {abstract} GetData(): Data* + } + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD new file mode 100644 index 00000000..85f1e266 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json new file mode 100644 index 00000000..43792eb9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/architecture.json @@ -0,0 +1,26 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml new file mode 100644 index 00000000..350cd76d --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/component_diagram.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml new file mode 100644 index 00000000..c3c9671b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_1_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml new file mode 100644 index 00000000..225637b4 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_class/unit_2_class_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +' Collaborator stub for a class owned by unit_1; it must not be indexed as a +' refinement of unit_2. +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + class Unit1Service <> + } + + namespace unit_2 { + class Unit2Service + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD new file mode 100644 index 00000000..3a17cd47 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/BUILD @@ -0,0 +1,61 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "unit_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "architecture", + static = ["component_diagram.puml"], +) + +unit_design( + name = "unit_1_design", + static = ["unit_1_class_diagram.puml"], +) + +unit_design( + name = "unit_2_design", + static = ["unit_2_class_diagram.puml"], +) + +unit_design( + name = "unit_3_design", + static = ["unit_3_class_diagram.puml"], +) + +unit_design( + name = "unit_4_design", + static = ["unit_4_class_diagram.puml"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + deps = [ + ":architecture", + ":unit_1_design", + ":unit_2_design", + ":unit_3_design", + ":unit_4_design", + ], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json new file mode 100644 index 00000000..428c3d63 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/architecture.json @@ -0,0 +1,34 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc:component_example", + "design_name": "component_example" + } + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "design_name": "unit_1" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2", + "design_name": "unit_2" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_3:unit_3", + "design_name": "unit_3" + }, + { + "label": "@//bazel/rules/rules_score/examples/seooc/unit_4:unit_4", + "design_name": "unit_4" + } + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml new file mode 100644 index 00000000..5106d645 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/component_diagram.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +package safety_software_seooc_example <> { + component component_example <> { + component unit_1 <> + component unit_2 <> + component unit_3 <> + component unit_4 <> + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml new file mode 100644 index 00000000..8666d650 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_1_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_1 { + struct Unit1State + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml new file mode 100644 index 00000000..f25a0a89 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_2_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_2 { + interface Unit2Port + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml new file mode 100644 index 00000000..df55eee9 --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_3_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_3 { + abstract class Unit3Base + } + } +} +@enduml diff --git a/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml new file mode 100644 index 00000000..20508d0b --- /dev/null +++ b/validation/core/integration_test/architectural_unit_class_design/positive_refining_supported_entity_types/unit_4_class_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml +namespace safety_software_seooc_example { + namespace component_example { + namespace unit_4 { + class Unit4Service + } + } +} +@enduml diff --git a/validation/core/src/profiles/dependable_element.rs b/validation/core/src/profiles/dependable_element.rs index 4ec0e9f3..ec562d0a 100644 --- a/validation/core/src/profiles/dependable_element.rs +++ b/validation/core/src/profiles/dependable_element.rs @@ -12,21 +12,58 @@ // ******************************************************************************* use crate::models::{ - BazelArchitecture, BazelInput, ComponentDiagramArchitecture, ComponentDiagramInputs, + BazelArchitecture, BazelInput, ClassDiagramInputs, ClassEntityIndex, + ComponentDiagramArchitecture, ComponentDiagramInputs, InternalApiIndex, +}; +use crate::readers::{BazelReader, ClassDiagramReader, ComponentDiagramReader}; +use crate::validators::{ + validate_architectural_unit_class_design, validate_bazel_component, UnitDesignEntity, }; -use crate::readers::{BazelReader, ComponentDiagramReader}; -use crate::validators::validate_bazel_component; use crate::ValidationResult; use serde::Deserialize; +use std::collections::BTreeMap; use super::profile::{merge_results, read_and_convert, ProfileRun}; +type ProfileValidator<'a> = Box Option + 'a>; + #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct DependableElementInputs { architecture: String, #[serde(default)] component_diagrams: Vec, + #[serde(default)] + unit_design_class_diagrams: Option>>, + #[serde(default)] + internal_api_diagrams: Vec, +} + +fn registered_validators<'a>( + bazel: &'a Option, + component: &'a Option, + unit_design_classes: &'a Option>>, + internal_api: &'a Option, +) -> Vec> { + vec![ + Box::new(move || { + let (bazel, component) = (bazel.as_ref()?, component.as_ref()?); + Some(validate_bazel_component(bazel, component)) + }), + Box::new(move || { + let (bazel, component, unit_design_classes) = ( + bazel.as_ref()?, + component.as_ref()?, + unit_design_classes.as_ref()?, + ); + Some(validate_architectural_unit_class_design( + bazel, + component, + unit_design_classes, + internal_api.as_ref(), + )) + }), + ] } pub fn run(inputs: &DependableElementInputs) -> Result { @@ -41,11 +78,52 @@ pub fn run(inputs: &DependableElementInputs) -> Result { &mut result, |raw: ComponentDiagramInputs, errs| raw.to_diagram_architecture(errs), )?; + let internal_api = read_and_convert::( + inputs.internal_api_diagrams.as_slice(), + &mut result, + |raw: ClassDiagramInputs, _errs| InternalApiIndex::build_index(&raw), + )?; + let unit_design_classes = match &inputs.unit_design_class_diagrams { + Some(diagrams_by_label) => { + let mut entity_ids_by_label = BTreeMap::new(); + for (unit_label, paths) in diagrams_by_label { + let diagrams = read_and_convert::( + paths.as_slice(), + &mut result, + |raw: ClassDiagramInputs, _errs| raw, + )? + .unwrap_or_default(); + let index = ClassEntityIndex::build_diagram_index(&diagrams, &mut result); + let entities = index + .entities() + .filter(|entity| is_unit_refinement_entity(entity.entity_type)) + .map(|entity| UnitDesignEntity { + id: entity.id.clone(), + entity_type: entity.entity_type, + method_names: entity + .methods + .iter() + .map(|method| method.name.clone()) + .filter(|name| !name.is_empty()) + .collect(), + source_location: entity.source_location.clone(), + }) + .collect(); + entity_ids_by_label.insert(unit_label.clone(), entities); + } + Some(entity_ids_by_label) + } + None => None, + }; + + let validators = registered_validators(&bazel, &component, &unit_design_classes, &internal_api); let mut ran_validator = false; - if let (Some(bazel), Some(component)) = (bazel.as_ref(), component.as_ref()) { - merge_results(&mut result, validate_bazel_component(bazel, component)); - ran_validator = true; + for validator in validators { + if let Some(validator_result) = validator() { + merge_results(&mut result, validator_result); + ran_validator = true; + } } Ok(ProfileRun { @@ -53,3 +131,33 @@ pub fn run(inputs: &DependableElementInputs) -> Result { result, }) } + +fn is_unit_refinement_entity(entity_type: class_diagram::EntityType) -> bool { + matches!( + entity_type, + class_diagram::EntityType::Class + | class_diagram::EntityType::Struct + | class_diagram::EntityType::Interface + | class_diagram::EntityType::AbstractClass + ) +} + +#[cfg(test)] +mod tests { + use super::is_unit_refinement_entity; + use class_diagram::EntityType; + + #[test] + fn unit_refinement_accepts_class_like_entities_but_not_enums() { + for entity_type in [ + EntityType::Class, + EntityType::Struct, + EntityType::Interface, + EntityType::AbstractClass, + ] { + assert!(is_unit_refinement_entity(entity_type)); + } + + assert!(!is_unit_refinement_entity(EntityType::Enum)); + } +} diff --git a/validation/core/src/validators/architectural_unit_class_design_validator.rs b/validation/core/src/validators/architectural_unit_class_design_validator.rs new file mode 100644 index 00000000..7f3e789b --- /dev/null +++ b/validation/core/src/validators/architectural_unit_class_design_validator.rs @@ -0,0 +1,1035 @@ +// ******************************************************************************* +// 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 std::collections::{BTreeMap, BTreeSet}; + +use source_location::SourceLocation; + +use crate::models::{ + BazelArchitecture, ComponentDiagramArchitecture, ComponentRelationType, EndpointRole, + InternalApiIndex, LogicComponentExt, +}; +use crate::results::{Diagnostics, ErrorBuilder, ErrorCategory}; +use crate::validators::shared::{format_quoted_list, format_source_file, format_source_line}; +use crate::ValidationResult; + +#[derive(Clone, Debug)] +pub(crate) struct UnitDesignEntity { + pub id: String, + pub entity_type: class_diagram::EntityType, + pub method_names: Vec, + pub source_location: SourceLocation, +} + +enum UnitDesignDiagnostic<'a> { + Refinement { + refinements: &'a [&'a UnitDesignEntity], + }, + BoundInterface { + api_interface: &'a crate::models::InternalApiInterface, + matching_interfaces: &'a [&'a UnitDesignEntity], + }, +} + +/// Verify that every architectural unit is refined by an eligible entity whose +/// ID is nested below the full architectural unit ID in the bound unit design. +pub fn validate_architectural_unit_class_design( + bazel: &BazelArchitecture, + component: &ComponentDiagramArchitecture, + design_entities_by_unit_label: &BTreeMap>, + internal_api: Option<&InternalApiIndex>, +) -> ValidationResult { + let mut result = ValidationResult::default(); + + for (unit_key, architectural_unit) in &component.unit_set { + let Some((unit_label, entities)) = + unit_design_context(unit_key, &bazel.unit_set, design_entities_by_unit_label) + else { + // Bazel-component validation reports this independently. + continue; + }; + + validate_unit(architectural_unit, unit_label, entities, &mut result); + } + + if let Some(internal_api) = internal_api { + validate_bound_interfaces_and_methods( + bazel, + component, + internal_api, + design_entities_by_unit_label, + &mut result, + ); + } + + result +} + +fn unit_design_context<'a, K: Ord>( + unit_key: &K, + bazel_units: &'a BTreeMap, + design_entities_by_unit_label: &'a BTreeMap>, +) -> Option<(&'a str, &'a [UnitDesignEntity])> { + let unit_label = bazel_units.get(unit_key)?; + let entities = design_entities_by_unit_label + .get(unit_label) + .map(Vec::as_slice) + .unwrap_or(&[]); + Some((unit_label, entities)) +} + +fn validate_bound_interfaces_and_methods( + bazel: &BazelArchitecture, + component: &ComponentDiagramArchitecture, + internal_api: &InternalApiIndex, + design_entities_by_unit_label: &BTreeMap>, + result: &mut ValidationResult, +) { + let internal_interfaces: BTreeMap<&str, _> = internal_api + .interfaces() + .map(|interface| (interface.id.as_str(), interface)) + .collect(); + let component_interfaces: BTreeMap<&str, _> = component + .entities + .iter() + .filter(|entity| entity.is_interface() && entity.parent_id.is_some()) + .map(|interface| (interface.id.as_str(), interface)) + .collect(); + + for (unit_key, unit) in &component.unit_set { + let Some((unit_label, design_entities)) = + unit_design_context(unit_key, &bazel.unit_set, design_entities_by_unit_label) + else { + // The Bazel-component validator reports the missing Bazel mapping. + continue; + }; + + let bound_interface_targets: BTreeSet<&str> = unit + .relations + .iter() + .filter(|relation| { + relation.relation_type == ComponentRelationType::InterfaceBinding + && relation.source_role == EndpointRole::Required + }) + .map(|relation| relation.target.as_str()) + .collect(); + + for target in bound_interface_targets { + let Some(component_interface) = component_interfaces.get(target) else { + continue; + }; + let Some(api_interface) = internal_interfaces.get(target) else { + // The component-internal-API validator reports this mismatch. + continue; + }; + + validate_bound_unit_interface( + &unit.id, + unit_label, + component_interface, + api_interface, + design_entities, + result, + ); + } + } +} + +fn validate_bound_unit_interface( + unit_id: &str, + unit_label: &str, + component_interface: &crate::models::LogicComponent, + api_interface: &crate::models::InternalApiInterface, + design_entities: &[UnitDesignEntity], + result: &mut ValidationResult, +) { + let matching_interfaces: Vec<&UnitDesignEntity> = design_entities + .iter() + .filter(|entity| { + entity.entity_type == class_diagram::EntityType::Interface + && entity.id == api_interface.id + }) + .collect(); + + log_unit_design_diagnostics( + &mut result.diagnostics, + unit_id, + unit_label, + design_entities, + UnitDesignDiagnostic::BoundInterface { + api_interface, + matching_interfaces: &matching_interfaces, + }, + ); + + let Some(detail_interface) = matching_interfaces.first().copied() else { + result.add_failure(missing_interface_error( + unit_id, + unit_label, + &component_interface.id, + &component_interface.source_location, + api_interface, + design_entities, + )); + return; + }; + + // ClassEntityIndex rejects duplicate canonical IDs before entities reach + // this validator, so at most one exact interface can match here. + let detail_methods: BTreeSet<&str> = detail_interface + .method_names + .iter() + .map(String::as_str) + .collect(); + let missing_methods: BTreeSet<&str> = api_interface + .method_names + .iter() + .map(String::as_str) + .filter(|method| !detail_methods.contains(method)) + .collect(); + if !missing_methods.is_empty() { + result.add_failure(missing_methods_error( + unit_id, + unit_label, + &component_interface.id, + &component_interface.source_location, + api_interface, + detail_interface, + &missing_methods, + )); + } +} + +fn missing_interface_error( + unit_id: &str, + unit_label: &str, + interface_id: &str, + component_source: &SourceLocation, + api_interface: &crate::models::InternalApiInterface, + design_entities: &[UnitDesignEntity], +) -> String { + let mut error = ErrorBuilder::new(ErrorCategory::Interface) + .title(format!( + "Bound unit \"{unit_id}\" has no corresponding interface \"{interface_id}\" in its unit design" + )) + .field("component interface", format!("\"{interface_id}\"")) + .field("bound unit", format!("\"{unit_id}\"")) + .field("bazel unit", unit_label); + error = add_interface_source_fields(error, component_source, &api_interface.source_location); + let design_sources: BTreeSet<(String, u32)> = design_entities + .iter() + .map(|entity| entity.source_location.display()) + .collect(); + for (design_file, design_line) in design_sources { + error = error + .field("design source file", format!("\"{design_file}\"")) + .field("design source line", design_line.to_string()); + } + error + .fix(format!( + "declare interface \"{}\" in the unit design for \"{unit_id}\" using the Internal API namespace", + interface_id.rsplit('.').next().unwrap_or(interface_id) + )) + .build() +} + +fn missing_methods_error( + unit_id: &str, + unit_label: &str, + interface_id: &str, + component_source: &SourceLocation, + api_interface: &crate::models::InternalApiInterface, + detail_interface: &UnitDesignEntity, + missing_methods: &BTreeSet<&str>, +) -> String { + let methods = format_quoted_list(missing_methods.iter().copied()); + let (design_file, design_line) = detail_interface.source_location.display(); + let mut error = ErrorBuilder::new(ErrorCategory::Method) + .title(format!( + "Unit design interface \"{}\" is missing Internal API method(s) {methods}", + detail_interface.id + )) + .field("component interface", format!("\"{interface_id}\"")) + .field("bound unit", format!("\"{unit_id}\"")) + .field("bazel unit", unit_label) + .field("detail interface", format!("\"{}\"", detail_interface.id)); + error = add_interface_source_fields(error, component_source, &api_interface.source_location); + error + .field("design source file", format!("\"{design_file}\"")) + .field("design source line", design_line.to_string()) + .fix(format!( + "add {methods} to the detail interface, matching the Internal API method names exactly" + )) + .build() +} + +fn add_interface_source_fields( + error: ErrorBuilder, + component_source: &SourceLocation, + api_source: &SourceLocation, +) -> ErrorBuilder { + let (component_file, component_line) = component_source.display(); + let (api_file, api_line) = api_source.display(); + error + .field("component source file", format!("\"{component_file}\"")) + .field("component source line", component_line.to_string()) + .field("internal API source file", format!("\"{api_file}\"")) + .field("internal API source line", api_line.to_string()) +} + +fn validate_unit( + architectural_unit: &crate::models::LogicComponent, + unit_label: &str, + entities: &[UnitDesignEntity], + result: &mut ValidationResult, +) { + let refinements: Vec<&UnitDesignEntity> = entities + .iter() + .filter(|entity| refines_unit(&entity.id, &architectural_unit.id)) + .collect(); + + log_unit_design_diagnostics( + &mut result.diagnostics, + &architectural_unit.id, + unit_label, + entities, + UnitDesignDiagnostic::Refinement { + refinements: &refinements, + }, + ); + + if refinements.is_empty() { + result.add_failure(missing_refinement_error( + &architectural_unit.id, + unit_label, + entities, + )); + } +} + +fn refines_unit(entity_id: &str, unit_id: &str) -> bool { + entity_id + .to_lowercase() + .starts_with(&format!("{}.", unit_id.to_lowercase())) +} + +fn missing_refinement_error( + unit_id: &str, + unit_label: &str, + entities: &[UnitDesignEntity], +) -> String { + let mut error = ErrorBuilder::new(ErrorCategory::Design) + .title(format!( + "Architectural unit \"{unit_id}\" has no refining entity in its unit design" + )) + .field("architectural unit", unit_id) + .field("bazel unit", unit_label); + + if entities.is_empty() { + error = error.field("unit design entities", ""); + } else { + for entity in entities { + error = error + .field("entity", format!("\"{}\"", entity.id)) + .field( + "design source file", + format!("\"{}\"", format_source_file(&entity.source_location)), + ) + .field( + "design source line", + format_source_line(&entity.source_location), + ); + } + } + + error + .fix(format!( + "add a class, struct, interface, or abstract class whose ID is below the architectural unit ID \"{unit_id}\"" + )) + .build() +} + +fn log_unit_design_diagnostics( + diagnostics: &mut Diagnostics, + unit_id: &str, + unit_label: &str, + entities: &[UnitDesignEntity], + context: UnitDesignDiagnostic<'_>, +) { + diagnostics.debug(|| { + match context { + UnitDesignDiagnostic::Refinement { refinements } => format!( + "Architectural unit: {}\nBazel unit: {}\nEligible unit-design entities: {}\nSelected refinements: {}", + unit_id, + unit_label, + display_entities(entities), + display_refinement_ids(refinements), + ), + UnitDesignDiagnostic::BoundInterface { + api_interface, + matching_interfaces, + } => { + let detail_interfaces = entities + .iter() + .filter(|entity| entity.entity_type == class_diagram::EntityType::Interface) + .map(|entity| { + format!( + "{} [{}]", + entity.id, + display_list(entity.method_names.iter().cloned()) + ) + }) + .collect::>(); + format!( + "Bound interface consistency:\nArchitectural unit: {}\nBazel unit: {}\nInternal API interface: {}\nRequired methods: {}\nDetail-design interfaces: {}\nExact matching interfaces: {}", + unit_id, + unit_label, + api_interface.id, + display_list(api_interface.method_names.iter().cloned()), + display_list(detail_interfaces), + display_entity_refs(matching_interfaces), + ) + } + } + }); +} + +fn display_entities(entities: &[UnitDesignEntity]) -> String { + display_list(entities.iter().map(display_entity)) +} + +fn display_entity_refs(entities: &[&UnitDesignEntity]) -> String { + display_list(entities.iter().map(|entity| display_entity(entity))) +} + +fn display_entity(entity: &UnitDesignEntity) -> String { + let (file, line) = entity.source_location.display(); + format!( + "{} {:?} methods [{}] ({}:{})", + entity.id, + entity.entity_type, + display_list(entity.method_names.iter().cloned()), + file, + line, + ) +} + +fn display_refinement_ids(entities: &[&UnitDesignEntity]) -> String { + display_list(entities.iter().map(|entity| entity.id.clone())) +} + +fn display_list(items: impl IntoIterator) -> String { + let mut output = String::new(); + let mut is_first = true; + for item in items { + if !is_first { + output.push_str(", "); + } + output.push_str(&item); + is_first = false; + } + if is_first { + "".to_string() + } else { + output + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{ + BazelEntityRef, BazelInput, BazelInputEntry, ComponentDiagramInputs, ComponentRelationType, + ComponentType, EndpointRole, InternalApiIndex, LogicComponent, LogicRelation, + }; + use class_diagram::{ClassDiagram, EntityType, Method, SimpleEntity, Visibility}; + use std::collections::BTreeMap; + + fn source_location() -> source_location::SourceLocation { + source_location::SourceLocation::new("test.puml", 1) + } + + fn component_entity(id: &str, parent_id: Option<&str>, stereotype: &str) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: id.rsplit('.').next().map(str::to_string), + alias: None, + parent_id: parent_id.map(str::to_string), + element_type: if stereotype == "SEooC" { + ComponentType::Package + } else { + ComponentType::Component + }, + stereotype: Some(stereotype.to_string()), + relations: Vec::new(), + source_location: source_location(), + } + } + + fn bazel_ref(label: &str, design_name: &str) -> BazelEntityRef { + BazelEntityRef { + label: label.to_string(), + design_name: design_name.to_string(), + } + } + + fn inputs( + class_ids: &[&str], + ) -> ( + BazelArchitecture, + ComponentDiagramArchitecture, + BTreeMap>, + ) { + let mut components = BTreeMap::new(); + components.insert( + "@//:system".to_string(), + BazelInputEntry { + units: Vec::new(), + components: vec![bazel_ref("@//:control", "control")], + }, + ); + components.insert( + "@//:control".to_string(), + BazelInputEntry { + units: vec![bazel_ref("@//:filter", "Filter")], + components: Vec::new(), + }, + ); + + let mut result = ValidationResult::default(); + let bazel = BazelInput { components }.to_bazel_architecture(&mut result); + let component = ComponentDiagramInputs { + entities: vec![ + component_entity("system", None, "SEooC"), + component_entity("system.control", Some("system"), "component"), + component_entity("system.control.Filter", Some("system.control"), "unit"), + ], + } + .to_diagram_architecture(&mut result); + assert!( + result.is_empty(), + "fixture setup failed: {:?}", + result.failures + ); + + let classes = BTreeMap::from([( + "@//:filter".to_string(), + class_ids + .iter() + .map(|id| UnitDesignEntity { + id: (*id).to_string(), + entity_type: class_diagram::EntityType::Class, + method_names: Vec::new(), + source_location: source_location(), + }) + .collect(), + )]); + + (bazel, component, classes) + } + + #[test] + fn accepts_a_class_in_the_bound_unit_design_namespace() { + let (bazel, component, classes) = inputs(&["system.control.Filter.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn rejects_a_class_from_another_namespace() { + let (bazel, component, classes) = inputs(&["system.control.OtherUnit.Service"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains( + "Architectural unit \"system.control.Filter\" has no refining entity in its unit design." + )); + assert!(result.failures[0].contains("Design source file : \"test.puml\"")); + assert!(result.failures[0].contains("Design source line : 1")); + } + + #[test] + fn rejects_a_class_with_the_same_unit_name_under_another_parent() { + let (bazel, component, classes) = inputs(&["system.other.Filter.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); + + assert_eq!(result.failures.len(), 1); + } + + #[test] + fn rejects_a_lexical_but_not_segment_prefix() { + let (bazel, component, classes) = inputs(&["system.control.Filtering.FilterService"]); + + let result = validate_architectural_unit_class_design(&bazel, &component, &classes, None); + + assert_eq!(result.failures.len(), 1); + } + + const INTERNAL_INTERFACE_ID: &str = + "safety_software_seooc_example.component_example.InternalInterface"; + + fn api_index() -> InternalApiIndex { + api_index_with_methods(&["GetData"]) + } + + fn api_index_with_methods(method_names: &[&str]) -> InternalApiIndex { + let source_location = source_location(); + InternalApiIndex::build_index(&[ClassDiagram { + name: "internal_api".to_string(), + entities: vec![SimpleEntity { + id: INTERNAL_INTERFACE_ID.to_string(), + name: "InternalInterface".to_string(), + enclosing_namespace_id: Some( + "safety_software_seooc_example.component_example".to_string(), + ), + stereotypes: Vec::new(), + entity_type: EntityType::Interface, + type_aliases: Vec::new(), + variables: Vec::new(), + methods: method_names + .iter() + .map(|name| Method { + name: (*name).to_string(), + return_type: None, + source_location: source_location.clone(), + visibility: Visibility::Public, + parameters: Vec::new(), + template_parameters: None, + modifiers: Vec::new(), + }) + .collect(), + template_parameters: None, + enum_literals: Vec::new(), + relationships: Vec::new(), + source_location, + }], + free_functions: Vec::new(), + }]) + } + + fn unit_label(name: &str) -> String { + format!("@//bazel/rules/rules_score/examples/seooc/{name}:{name}") + } + + fn bound_interface_inputs( + bindings: &[(&str, EndpointRole)], + ) -> ( + BazelArchitecture, + ComponentDiagramArchitecture, + BTreeMap>, + ) { + let mut bazel_units = BTreeMap::new(); + let mut component_units = BTreeMap::new(); + let mut designs = BTreeMap::new(); + + for (name, role) in bindings { + let unit_id = format!("safety_software_seooc_example.component_example.{name}"); + let key = (name.to_lowercase(), Some("component_example".to_string())); + let label = unit_label(name); + bazel_units.insert(key.clone(), label.clone()); + component_units.insert( + key, + LogicComponent { + id: unit_id, + name: Some((*name).to_string()), + alias: None, + parent_id: Some("safety_software_seooc_example.component_example".to_string()), + element_type: ComponentType::Component, + stereotype: Some("unit".to_string()), + relations: vec![LogicRelation { + target: INTERNAL_INTERFACE_ID.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role: *role, + source_location: source_location(), + }], + source_location: source_location(), + }, + ); + designs.insert( + label, + vec![ + UnitDesignEntity { + id: INTERNAL_INTERFACE_ID.to_string(), + entity_type: EntityType::Interface, + method_names: vec!["GetData".to_string()], + source_location: source_location(), + }, + UnitDesignEntity { + id: format!( + "safety_software_seooc_example.component_example.{name}.UnitClass" + ), + entity_type: EntityType::Class, + method_names: Vec::new(), + source_location: source_location(), + }, + ], + ); + } + + let component = ComponentDiagramArchitecture { + seooc_set: BTreeMap::new(), + comp_set: BTreeMap::new(), + unit_set: component_units, + entities: vec![LogicComponent { + id: INTERNAL_INTERFACE_ID.to_string(), + name: Some("InternalInterface".to_string()), + alias: None, + parent_id: Some("safety_software_seooc_example.component_example".to_string()), + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + source_location: source_location(), + }], + }; + let bazel = BazelArchitecture { + seooc_set: BTreeMap::new(), + comp_set: BTreeMap::new(), + unit_set: bazel_units, + }; + (bazel, component, designs) + } + + #[test] + fn accepts_required_and_provided_units_when_the_required_design_is_complete() { + let (bazel, component, designs) = bound_interface_inputs(&[ + ("unit_1", EndpointRole::Required), + ("unit_2", EndpointRole::Provided), + ]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn does_not_require_provided_unit_design_to_declare_the_interface() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Provided)]); + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "provided-side interface declaration must not be checked: {:?}", + result.failures + ); + } + + #[test] + fn reports_when_any_bound_unit_lacks_the_interface() { + let (bazel, component, mut designs) = bound_interface_inputs(&[ + ("unit_1", EndpointRole::Provided), + ("unit_2", EndpointRole::Required), + ]); + designs + .get_mut(&unit_label("unit_2")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!( + result.failures[0].contains("safety_software_seooc_example.component_example.unit_2") + ); + assert!(result.failures[0].contains("no corresponding interface")); + } + + #[test] + fn reports_internal_api_method_missing_from_bound_unit_interface() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].method_names = + vec!["SetData".to_string()]; + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + assert!( + result.failures[0].contains("safety_software_seooc_example.component_example.unit_1") + ); + } + + #[test] + fn skips_internal_api_interface_without_unit_bindings() { + let (bazel, mut component, designs) = bound_interface_inputs(&[]); + component.entities.push(LogicComponent { + id: INTERNAL_INTERFACE_ID.to_string(), + name: Some("InternalInterface".to_string()), + alias: None, + parent_id: Some("example.service".to_string()), + element_type: ComponentType::Interface, + stereotype: None, + relations: Vec::new(), + source_location: source_location(), + }); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn requires_the_exact_canonical_interface_id() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].id = + "safety_software_seooc_example.component_example.other.InternalInterface".to_string(); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("no corresponding interface")); + } + + #[test] + fn accepts_additional_detail_interface_methods() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .push("LocalHelper".to_string()); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn compares_method_names_case_sensitively() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0].method_names = + vec!["getdata".to_string()]; + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + } + + #[test] + fn reports_all_missing_api_methods_together() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .clear(); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index_with_methods(&["GetData", "SetData"])), + ); + + assert_eq!(result.failures.len(), 1); + assert!(result.failures[0].contains("GetData")); + assert!(result.failures[0].contains("SetData")); + } + + #[test] + fn ignores_units_without_interface_bindings() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + component + .unit_set + .values_mut() + .next() + .unwrap() + .relations + .clear(); + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn ignores_non_interface_binding_relations() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + component.unit_set.values_mut().next().unwrap().relations[0].relation_type = + ComponentRelationType::Dependency; + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn skips_component_interface_missing_from_internal_api() { + let (bazel, component, designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + let empty_internal_api = InternalApiIndex::build_index(&[]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&empty_internal_api), + ); + + assert!( + result.is_empty(), + "unexpected failures: {:?}", + result.failures + ); + } + + #[test] + fn deduplicates_multiple_bindings_to_the_same_interface() { + let (bazel, mut component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + // Add a second interface binding relation to the same interface + let unit = component.unit_set.values_mut().next().unwrap(); + unit.relations.push(LogicRelation { + target: INTERNAL_INTERFACE_ID.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role: EndpointRole::Provided, + source_location: source_location(), + }); + // Remove the interface from unit design to trigger failure + designs + .get_mut(&unit_label("unit_1")) + .unwrap() + .retain(|entity| entity.entity_type != EntityType::Interface); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&api_index()), + ); + + // Expect exactly 1 failure despite having 2 relations to the same interface + assert_eq!(result.failures.len(), 1); + } + + #[test] + fn deduplicates_overloaded_method_names_in_missing_methods_error() { + let (bazel, component, mut designs) = + bound_interface_inputs(&[("unit_1", EndpointRole::Required)]); + designs.get_mut(&unit_label("unit_1")).unwrap()[0] + .method_names + .clear(); + + // Internal API with overloaded methods (same name) + let overloaded_api = api_index_with_methods(&["GetData", "GetData"]); + + let result = validate_architectural_unit_class_design( + &bazel, + &component, + &designs, + Some(&overloaded_api), + ); + + assert_eq!(result.failures.len(), 1); + let error = &result.failures[0]; + // Ensure "GetData" appears only once in the error title and fix message + assert_eq!(error.matches("\"GetData\"").count(), 2); // 1 in title, 1 in fix + } +} diff --git a/validation/core/src/validators/class_design_implementation_validator.rs b/validation/core/src/validators/class_design_implementation_validator.rs index 58b71a18..491e302c 100644 --- a/validation/core/src/validators/class_design_implementation_validator.rs +++ b/validation/core/src/validators/class_design_implementation_validator.rs @@ -14,7 +14,7 @@ //! Class implementation validation: compare unit design class diagrams with //! C++ implementation produced by the C++ parser. -use super::shared::best_string_suggestion; +use super::shared::{best_string_suggestion, format_source_file, format_source_line}; use crate::models::ClassEntityIndex; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::ValidationResult; @@ -22,7 +22,6 @@ use class_diagram::{ EnumLiteral, FreeFunctionDecl, MemberVariable, Method, Relationship, SimpleEntity, TemplateParameter, TypeAlias, }; -use source_location::SourceLocation; use std::collections::BTreeMap; /// Run design-class-vs-implementation-class validation using indexed inputs. @@ -574,9 +573,12 @@ impl ClassDesignImplementationValidator { .field("class", format!("\"{}\"", entity.id)) .field( "design source file", - format!("\"{}\"", source_file(&entity.source_location)), + format!("\"{}\"", format_source_file(&entity.source_location)), + ) + .field( + "design source line", + format_source_line(&entity.source_location), ) - .field("design source line", source_line(&entity.source_location)) .fix(format!( "add implementation class \"{}\" in the C++ class implementation, or remove it from the unit design", entity.id @@ -603,9 +605,12 @@ impl ClassDesignImplementationValidator { .field("free function", format!("\"{function_name}\"")) .field( "design source file", - format!("\"{}\"", source_file(&function.source_location)), + format!("\"{}\"", format_source_file(&function.source_location)), + ) + .field( + "design source line", + format_source_line(&function.source_location), ) - .field("design source line", source_line(&function.source_location)) .fix(format!( "add free function \"{function_name}\" to the C++ implementation, or remove it from the unit design" )); @@ -636,23 +641,23 @@ impl ClassDesignImplementationValidator { .field("design value", design_value) .field( "design source file", - format!("\"{}\"", source_file(&design_function.source_location)), + format!("\"{}\"", format_source_file(&design_function.source_location)), ) .field( "design source line", - source_line(&design_function.source_location), + format_source_line(&design_function.source_location), ) .field("implementation value", implementation_value) .field( "implementation source file", format!( "\"{}\"", - source_file(&implementation_function.source_location) + format_source_file(&implementation_function.source_location) ), ) .field( "implementation source line", - source_line(&implementation_function.source_location), + format_source_line(&implementation_function.source_location), ) .fix(format!( "make {field} on free function \"{function_name}\" consistent between the unit design and the C++ implementation" @@ -675,9 +680,12 @@ impl ClassDesignImplementationValidator { .field("member", format!("{member_type} \"{member_name}\"")) .field( "design source file", - format!("\"{}\"", source_file(&design_entity.source_location)), + format!("\"{}\"", format_source_file(&design_entity.source_location)), + ) + .field( + "design source line", + format_source_line(&design_entity.source_location), ) - .field("design source line", source_line(&design_entity.source_location)) .fix(format!( "add {member_type} \"{member_name}\" to entity \"{}\" in the C++ class implementation, or remove it from the unit design", design_entity.id @@ -712,17 +720,23 @@ impl ClassDesignImplementationValidator { .field("design value", design_value) .field( "design source file", - format!("\"{}\"", source_file(&design_entity.source_location)), + format!("\"{}\"", format_source_file(&design_entity.source_location)), + ) + .field( + "design source line", + format_source_line(&design_entity.source_location), ) - .field("design source line", source_line(&design_entity.source_location)) .field("implementation value", implementation_value) .field( "implementation source file", - format!("\"{}\"", source_file(&implementation_entity.source_location)), + format!( + "\"{}\"", + format_source_file(&implementation_entity.source_location) + ), ) .field( "implementation source line", - source_line(&implementation_entity.source_location), + format_source_line(&implementation_entity.source_location), ) .fix(format!( "make {field_reference} in entity \"{}\" consistent between the unit design and the C++ class implementation", @@ -739,26 +753,6 @@ fn format_field_reference(field: &str, style: FieldReferenceStyle) -> String { } } -fn source_file(source_location: &SourceLocation) -> String { - let (file, _) = source_location.display(); - - if file.is_empty() { - "".to_string() - } else { - file - } -} - -fn source_line(source_location: &SourceLocation) -> String { - let (_, line) = source_location.display(); - - if line == 0 { - "".to_string() - } else { - line.to_string() - } -} - fn format_class_comparison_debug( design_entity: &SimpleEntity, implementation_entity: &SimpleEntity, diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 011fbf5a..ee7b9fa4 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -13,6 +13,7 @@ //! Validator entrypoints for architecture checks. +mod architectural_unit_class_design_validator; mod bazel_component_validator; mod class_design_implementation_validator; mod class_design_sequence_validator; @@ -27,6 +28,8 @@ mod shared; #[path = "test/fixtures.rs"] pub(crate) mod fixtures; +pub use architectural_unit_class_design_validator::validate_architectural_unit_class_design; +pub(crate) use architectural_unit_class_design_validator::UnitDesignEntity; pub use bazel_component_validator::validate_bazel_component; pub use class_design_implementation_validator::validate_class_design_implementation; pub use class_design_sequence_validator::validate_class_design_sequence; diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs index 09c37675..833f6ff2 100644 --- a/validation/core/src/validators/shared/helpers.rs +++ b/validation/core/src/validators/shared/helpers.rs @@ -20,14 +20,38 @@ use strsim::jaro_winkler; pub(in crate::validators) const DEFAULT_SUGGESTION_THRESHOLD: f64 = 0.75; +pub(in crate::validators) fn format_source_file(source_location: &SourceLocation) -> String { + let (file, _) = source_location.display(); + if file.is_empty() { + "".to_string() + } else { + file + } +} + +pub(in crate::validators) fn format_source_line(source_location: &SourceLocation) -> String { + let (_, line) = source_location.display(); + if line == 0 { + "".to_string() + } else { + line.to_string() + } +} + pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> String { if names.is_empty() { return "".to_string(); } - names - .iter() - .map(|name| format!("\"{name}\"")) + format_quoted_list(names.iter().map(String::as_str)) +} + +pub(in crate::validators) fn format_quoted_list<'a>( + values: impl IntoIterator, +) -> String { + values + .into_iter() + .map(|value| format!("\"{value}\"")) .collect::>() .join(", ") } @@ -119,10 +143,11 @@ pub(in crate::validators) fn best_id_suggestion<'a>( #[cfg(test)] mod tests { use super::{ - best_id_suggestion, best_string_suggestion, earliest_source_by_id, - DEFAULT_SUGGESTION_THRESHOLD, + best_id_suggestion, best_string_suggestion, earliest_source_by_id, format_name_list, + format_quoted_list, DEFAULT_SUGGESTION_THRESHOLD, }; use source_location::SourceLocation; + use std::collections::BTreeSet; use strsim::jaro_winkler; #[test] @@ -130,6 +155,21 @@ mod tests { assert_eq!(best_string_suggestion("Service", std::iter::empty()), None); } + #[test] + fn format_quoted_list_preserves_order_and_duplicates() { + assert_eq!( + format_quoted_list(["second", "first", "second"]), + "\"second\", \"first\", \"second\"" + ); + } + + #[test] + fn format_name_list_keeps_sorted_order_and_empty_marker() { + let names = BTreeSet::from(["second".to_string(), "first".to_string()]); + assert_eq!(format_name_list(&names), "\"first\", \"second\""); + assert_eq!(format_name_list(&BTreeSet::new()), ""); + } + #[test] fn best_string_suggestion_accepts_exact_threshold_match() { let score = jaro_winkler("a", "baaa"); diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs index 5a40198d..66f33fa7 100644 --- a/validation/core/src/validators/shared/mod.rs +++ b/validation/core/src/validators/shared/mod.rs @@ -22,5 +22,6 @@ pub(in crate::validators) use diagram_analysis::{ }; pub(in crate::validators) use helpers::{ best_id_suggestion, best_string_suggestion, earliest_source_by_id, extract_method_name, - format_name_list, format_sequence_call, intersect_interfaces, + format_name_list, format_quoted_list, format_sequence_call, format_source_file, + format_source_line, intersect_interfaces, };