From 4161a178acd6333d5f06337d26e962bc1ec06462 Mon Sep 17 00:00:00 2001 From: Iago Bonnici Date: Mon, 9 Sep 2024 14:38:19 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Fixed=20@conflicts=20macro=20tes?= =?UTF-8?q?ts..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Framework/add.jl | 78 ++++++-- src/Framework/component.jl | 23 +++ src/Framework/component_macro.jl | 88 +-------- src/Framework/conflicts_macro.jl | 75 +++++++ src/Framework/macro_helpers.jl | 14 +- test/dedicated_test_failures/framework.jl | 1 + test/framework/01-regular_use.jl | 10 +- test/framework/03-component_macro.jl | 7 +- test/framework/04-conflicts_macro.jl | 228 +++++++++++++++------- test/framework/runtests.jl | 1 + 10 files changed, 338 insertions(+), 187 deletions(-) diff --git a/src/Framework/add.jl b/src/Framework/add.jl index a93ec7374..4a3c4be46 100644 --- a/src/Framework/add.jl +++ b/src/Framework/add.jl @@ -111,7 +111,7 @@ function check( node::Node, system::System, brought::BroughtList, - checked::OrderedSet{<:CompType}, + checked::OrderedDict{<:CompType,Node}, ) # Recursively check children first. @@ -128,18 +128,42 @@ function check( # Check against the current system value. has_component(system, R) && continue # Check against other components about to be brought. - any(C -> R <: C, checked) || + any(C -> R <: C, keys(checked)) || throw(MissingRequiredComponent(requirer, R, node, reason)) end end # Guard against conflicts. - for (F, reason) in conflicts_(C) - has_component(system, F) && - throw(ConflictWithSystemComponent(C, node, F, reason)) - for Chk in checked - F <: typeof(Chk) && - throw(ConflictWithBroughtComponent(C, node, brought[F], reason)) + for (C_as, Other, reason) in all_conflicts(C) + if has_component(system, Other) + (Other, Other_abstract) = + isabstracttype(Other) ? (first(system._abstract[Other]), Other) : + (Other, nothing) + throw( + ConflictWithSystemComponent( + C, + C_as === C ? nothing : C_as, + node, + Other, + Other_abstract, + reason, + ), + ) + end + for Chk in keys(checked) + if Chk <: Other + throw( + ConflictWithBroughtComponent( + C, + C_as === C ? nothing : C_as, + node, + Chk, + Chk === Other ? nothing : Other, + checked[Chk], + reason, + ), + ) + end end end @@ -154,7 +178,7 @@ function check( end end - push!(checked, C) + checked[C] = node end end @@ -169,7 +193,7 @@ function add!(system::System{V}, blueprints::Blueprint{V}...) where {V} forest = Node[] brought = BroughtList{V}() # Populated during pre-order traversal. - checked = OrderedSet{CompType{V}}() # Populated during first post-order traversal. + checked = OrderedDict{CompType{V},Node}() # Populated during first post-order traversal. #--------------------------------------------------------------------------------------- # Read-only preliminary checking. @@ -212,7 +236,7 @@ function add!(system::System{V}, blueprints::Blueprint{V}...) where {V} try # Second post-order visit: expand the blueprints. - for Chk in checked + for Chk in keys(checked) node = first(brought[Chk]) blueprint = node.blueprint @@ -325,15 +349,20 @@ end struct ConflictWithSystemComponent <: AddException comp::CompType + comp_abstract::Option{CompType} # Fill if 'comp' conflicts as this abstract type. node::Node other::CompType + other_abstract::Option{CompType} # Fill if 'other' conflicts as this abstract type. reason::Reason end struct ConflictWithBroughtComponent <: AddException comp::CompType + comp_abstract::Option{CompType} node::Node - other::Node + other::CompType + other_abstract::Option{CompType} + other_node::Node reason::Reason end @@ -470,10 +499,12 @@ function Base.showerror(io::IO, e::UnexpectedHookFailure) end function Base.showerror(io::IO, e::ConflictWithSystemComponent) - (; comp, node, other, reason) = e + (; comp, comp_abstract, node, other, other_abstract, reason) = e path = render_path(node) - header = "Blueprint would expand into $(typeof(comp)), \ - which conflicts with $other already in the system" + comp_as = isnothing(comp_abstract) ? "" : " (as a $comp_abstract)" + other_as = isnothing(other_abstract) ? "" : " (as a $other_abstract)" + header = "Blueprint would expand into $comp, \ + which$comp_as conflicts with $other$other_as already in the system" if isnothing(reason) body = "." else @@ -481,3 +512,20 @@ function Base.showerror(io::IO, e::ConflictWithSystemComponent) end print(io, "$header$body\n$path") end + +function Base.showerror(io::IO, e::ConflictWithBroughtComponent) + (; comp, comp_abstract, node, other, other_abstract, other_node, reason) = e + path = render_path(node) + other_path = render_path(other_node) + comp_as = isnothing(comp_abstract) ? "" : " (as a $comp_abstract)" + other_as = isnothing(other_abstract) ? "" : " (as a $other_abstract)" + header = "Blueprint would expand into $comp, \ + which$comp_as would conflict with $other$other_as \ + already brought by the same blueprint" + if isnothing(reason) + body = "." + else + body = ":\n $reason" + end + print(io, "$header$body\nAlready brought: $other_path---\n$path") +end diff --git a/src/Framework/component.jl b/src/Framework/component.jl index 0c3d5d54c..b0cd76e17 100644 --- a/src/Framework/component.jl +++ b/src/Framework/component.jl @@ -86,6 +86,29 @@ function vertical_guard(A::CompType, B::CompType, err_same::Function, err_diff:: err_diff(sub, sup) end +#------------------------------------------------------------------------------------------- +# The 'conflicts_' mapping entries are either abstract or concrete component, +# which makes checking information for one particular component not exactly straighforward. + +# (for some reason this is absent from Base) +function supertypes(T::Type) + S = supertype(T) + S === T ? (T,) : (T, supertypes(S)...) +end + +# Iterate over all conflicts for one particular component. +# yields (conflict_key, conflicting_component, reason) +# The yielded conflict key may be a supercomponent of the focal one. +function all_conflicts(C::CompType) + supers = ifilter(T -> T !== Any, supertypes(C)) + Iterators.flatten(imap(supers) do Sup + entries = conflicts_(Sup) + imap(entries) do (k, v) + (Sup, k, v) + end + end) +end + # ========================================================================================== # Display. diff --git a/src/Framework/component_macro.jl b/src/Framework/component_macro.jl index c4fd63d63..6f158b82e 100644 --- a/src/Framework/component_macro.jl +++ b/src/Framework/component_macro.jl @@ -115,9 +115,7 @@ function component_macro(__module__, __source__, input...) SuperComponent = $(tovalue(super, "Evaluating given supercomponent", DataType)) if !(SuperComponent <: Component) - xerr( - "Supercomponent: '$SuperComponent' does not subtype '$Component'.", - ) + xerr("Supercomponent: $SuperComponent does not subtype $Component.") end ValueType = system_value_type(SuperComponent) end, @@ -320,87 +318,3 @@ function component_macro(__module__, __source__, input...) res end - -#------------------------------------------------------------------------------------------- -# The 'conflicts_' mapping entries are either abstract or concrete component, -# which makes checking information for one particular component not exactly straighforward. - -# (for some reason this is absent from Base) -function supertypes(T::Type) - S = supertype(T) - S === T ? (T,) : (T, supertypes(S)...) -end - -# Iterate over all conflicting entries with the given component or a supercomponent of it. -super_conflict_keys(C::CompType) = - Iterators.filter(supertypes(C)) do sup - conflicts_(sup) - end - -# Iterate over all conflicts for one particular component. -# yields (conflict_key, conflicting_component, reason) -# The yielded conflict key may be a supercomponent of the focal one. -function all_conflicts(C::CompType) - Iterators.flatten(Iterators.map(super_conflict_keys(C)) do key - Iterators.map(conflicts_(key)) do (conflicting, reason) - (key, conflicting, reason) - end - end) -end - -# Guard against declaring conflicts between sub/super components. -function vertical_conflict(err) - (sub, sup) -> begin - it = sub === sup ? "itself" : "its own super-component '$sup'" - err("Component '$sub' cannot conflict with $it.") - end -end - -# Declare one particular conflict with a reason. -# Guard against redundant reasons specifications. -function declare_conflict(A::CompType, B::CompType, reason::Reason, err) - vertical_guard(A, B, vertical_conflict(err)) - for (k, c, reason) in all_conflicts(A) - isnothing(reason) && continue - if B <: c - as_K = k === A ? "" : " (as '$k')" - as_C = B === c ? "" : " (as '$c')" - err("Component '$A'$as_K already declared to conflict with '$B'$as_C \ - for the following reason:\n $(reason)") - end - end - # Append new method or override by updating value. - current = conflicts_(A) # Creates a new empty value if falling back on default impl. - if isempty(current) - # Dynamically add method to lend reference to the value lended by `conflicts_`. - eval(quote - conflicts_(::Type{$A}) = $current - end) - end - current[B] = reason -end - -# Fill up a clique, not overriding any existing reason. -function declare_conflicts_clique(err, components::Vector{<:CompType{V}}) where {V} - - function process_pair(A::CompType{V}, B::CompType{V}) - vertical_guard(A, B, vertical_conflict(err)) - # Same logic as above. - current = conflicts_(A) - if isempty(current) - eval(quote - conflicts_(::Type{$A}) = $current - end) - end - haskey(current, B) || (current[B] = nothing) - end - - # Triangular-iterate to guard against redundant items. - for (i, a) in enumerate(components) - for b in components[1:(i-1)] - process_pair(a, b) - process_pair(b, a) - end - end - -end diff --git a/src/Framework/conflicts_macro.jl b/src/Framework/conflicts_macro.jl index aba37debd..8b695a9b6 100644 --- a/src/Framework/conflicts_macro.jl +++ b/src/Framework/conflicts_macro.jl @@ -129,3 +129,78 @@ function conflicts_macro(__module__, __source__, input...) res end + +# Guard against declaring conflicts between sub/super components. +function vertical_conflict(err) + (sub, sup) -> begin + it = sub === sup ? "itself" : "its own super-component $sup" + err("Component $sub cannot conflict with $it.") + end +end + +# Declare one particular conflict with a reason. +# Guard against redundant reasons specifications. +function declare_conflict(A::CompType, B::CompType, reason::Reason, err) + vertical_guard(A, B, vertical_conflict(err)) + for (k, c, reason) in all_conflicts(A) + isnothing(reason) && continue + if B <: c + as_K = k === A ? "" : " (as $k)" + as_C = B === c ? "" : " (as $c)" + err("Component $A$as_K already declared to conflict with $B$as_C \ + for the following reason:\n $(reason)") + end + end + # Append new method or override by updating value. + current = conflicts_(A) # Creates a new empty value if falling back on default impl. + if isempty(current) + # Dynamically add method to lend reference to the value lended by `conflicts_`. + eval(quote + conflicts_(::Type{$A}) = $current + end) + end + current[B] = reason +end + +# Fill up a clique, not overriding any existing reason. +function declare_conflicts_clique(err, components::Vector{<:CompType{V}}) where {V} + + # The result of overriding methods like the above + # will not be visible from within the same function call + # because of . + # So, collect all required overrides in this collection + # to perform them only once at the end. + + changes = Dict{CompType{V},Tuple{Bool,Any}}() # {Component: (needs_override, NewConflictsDict)} + + function process_pair(A::CompType{V}, B::CompType{V}) + vertical_guard(A, B, vertical_conflict(err)) + current = if haskey(changes, A) + _, current = changes[A] + current + else + current = conflicts_(A) + changes[A] = (isempty(current), current) + current + end + haskey(current, B) || (current[B] = nothing) + end + + # Triangular-iterate to guard against redundant items. + for (i, a) in enumerate(components) + for b in components[1:(i-1)] + process_pair(a, b) + process_pair(b, a) + end + end + + # Perform all the overrides at once. + for (C, (needs_override, conflicts)) in changes + if needs_override + eval(quote + conflicts_(::Type{$C}) = $conflicts + end) + end + end + +end diff --git a/src/Framework/macro_helpers.jl b/src/Framework/macro_helpers.jl index 1dc958000..670dbff07 100644 --- a/src/Framework/macro_helpers.jl +++ b/src/Framework/macro_helpers.jl @@ -103,15 +103,15 @@ function to_component(mod, xp, value_type_var, ctx, xerr) if C isa Type if !(C <: Sup) but = C <: Component ? ", but '$(Component{system_value_type(C)})'" : "" - $xerr("$($ctx): the given type \ - does not subtype '$Sup'$but:$(xpres($qxp, C))") + $xerr("$($ctx): expression does not evaluate \ + to a subtype of $Sup$but:$(xpres($qxp, C))") end C else c = C # Actually an instance. if !(c isa Sup) but = c isa Component ? ", but for '$(system_value_type(c))'" : "" - $xerr("$($ctx): the given expression does not evaluate \ + $xerr("$($ctx): expression does not evaluate \ to a component for '$($value_type_var)'$but:$(xpres($qxp, c))") end typeof(c) @@ -126,13 +126,13 @@ function to_component(mod, xp, ctx, xerr) C = $(to_value(mod, xp, ctx, xerr, Any)) # Don't check the system value type, but infer it. if C isa Type - C <: Component || $xerr("$($ctx): the given type \ - does not subtype $Component:$(xpres($qxp, C))") + C <: Component || $xerr("$($ctx): expression does not evaluate \ + to a subtype of $Component:$(xpres($qxp, C))") C else c = C # Actually an instance. - c isa Component || $xerr("$($ctx): the given value \ - is not a component:$(xpres($qxp, c))") + c isa Component || $xerr("$($ctx): expression does not evaluate \ + to a component:$(xpres($qxp, c))") typeof(c) end end diff --git a/test/dedicated_test_failures/framework.jl b/test/dedicated_test_failures/framework.jl index 7c0a1f326..80692cd6a 100644 --- a/test/dedicated_test_failures/framework.jl +++ b/test/dedicated_test_failures/framework.jl @@ -9,6 +9,7 @@ import EcologicalNetworksDynamics.Framework: ConflictMacroExecError, ConflictMacroParseError, ConflictWithSystemComponent, + ConflictWithBroughtComponent, Framework, HookCheckFailure, ItemMacroExecError, diff --git a/test/framework/01-regular_use.jl b/test/framework/01-regular_use.jl index 839289f38..a79cbd626 100644 --- a/test/framework/01-regular_use.jl +++ b/test/framework/01-regular_use.jl @@ -243,7 +243,15 @@ end # Cannot add incompatible component. @sysfails( s + SparseMark(), - Add(ConflictWithSystemComponent, Sparse, [SparseMark], Size, nothing), + Add( + ConflictWithSystemComponent, + Sparse, + nothing, + [SparseMark], + Size, + nothing, + nothing, + ), ) # Blueprint checking may depend on other components. diff --git a/test/framework/03-component_macro.jl b/test/framework/03-component_macro.jl index b4de3a9fe..332fa61f1 100644 --- a/test/framework/03-component_macro.jl +++ b/test/framework/03-component_macro.jl @@ -287,7 +287,7 @@ end @xcompfails( (@component Kpr{Value} requires(4 + 5)), :Kpr, - "Required component: the given expression does not evaluate \ + "Required component: expression does not evaluate \ to a component for '$Value':\n\ Expression: :(4 + 5)\n\ Result: 9 ::$Int" @@ -296,7 +296,8 @@ end @xcompfails( (@component Kpr{Value} requires(Int)), :Kpr, - "Required component: the given type does not subtype '':\n\ + "Required component: expression does not evaluate \ + to a subtype of :\n\ Expression: :Int\n\ Result: $Int ::DataType" ) @@ -305,7 +306,7 @@ end @xcompfails( (@component Odv{Value} requires(Wdj)), :Odv, - "Required component: the given expression does not evaluate \ + "Required component: expression does not evaluate \ to a component for '$Value', but for '$Int':\n\ Expression: :Wdj\n\ Result: $Wdj ::" diff --git a/test/framework/04-conflicts_macro.jl b/test/framework/04-conflicts_macro.jl index eb8d522fe..7f0a636b8 100644 --- a/test/framework/04-conflicts_macro.jl +++ b/test/framework/04-conflicts_macro.jl @@ -15,10 +15,16 @@ using Test # Generate many small "markers" components just to toy with'em. for letter in 'A':'Z' - eval(:(struct $(Symbol(letter)) <: Blueprint{Value} end)) + C = Symbol(letter) + Bp = Symbol(C, :_b) + eval(quote + struct $Bp <: Blueprint{Value} end + @blueprint $Bp + @component $C{Value} blueprints(b::$Bp) + end) end -@testset "Invocations variations for @conflicts macro." begin +@testset "Invalid @conflicts macro invocations." begin #--------------------------------------------------------------------------------------- # Provide enough data for the declaration to be meaningful. @@ -43,16 +49,18 @@ end (@conflicts(A => ())), "At least two components are required to declare a conflict not only :A." ) - @xconffails((@conflicts(A, A)), "Component '$A' cannot conflict with itself.") - @xconffails( - (@conflicts(A, Invocations.A)), - "Component '$A' cannot conflict with itself." - ) + @xconffails((@conflicts(A, A)), "Component $_A cannot conflict with itself.") + @xconffails((@conflicts(A, A)), "Component $_A cannot conflict with itself.") #--------------------------------------------------------------------------------------- # No conflicts *a priori*, they are declared by the macro invocation. - confs(C) = sort(collect(Framework.conflicts(C)); by = repr) + confs(C) = sort( + collect(Iterators.map(Framework.all_conflicts(typeof(C))) do (a, b, r) + (Framework.singleton_instance(a), Framework.singleton_instance(b), r) + end); + by = repr, + ) @test confs(A) == [] @conflicts(A, B) @@ -78,37 +86,50 @@ end @xconffails( (@conflicts(4 + 5, 6)), - "First conflicting entry: expression does not evaluate to a $DataType: :(4 + 5), \ - but to a $Int64: 9.", + "First conflicting entry: expression does not evaluate to a component:\n\ + Expression: :(4 + 5)\n\ + Result: 9 ::$Int" ) @xconffails( (@conflicts(A, 6)), - "Conflicting entry: expression does not evaluate to a $DataType: 6, \ - but to a $Int64: 6.", + "Conflicting entry: expression does not evaluate \ + to a component for '$Value':\n\ + Expression: 6\n\ + Result: 6 ::$Int" ) @xconffails( - (@conflicts(Int64, Float64)), - "First conflicting entry: not a subtype of '$Blueprint': '$Int64'.", + (@conflicts(Int, Float64)), + "First conflicting entry: expression does not evaluate \ + to a subtype of $Component:\n\ + Expression: :Int\n\ + Result: $Int ::DataType", ) @xconffails( (@conflicts(A, Float64)), # 'Value' inferred from the first entry. - "Conflicting entry: '$Float64' does not subtype '$Blueprint{$Value}'.", + "Conflicting entry: expression does not evaluate \ + to a subtype of :\n\ + Expression: :Float64\n\ + Result: $Float64 ::DataType", ) a = 5 @xconffails( (@conflicts(a, a)), - "First conflicting entry: expression does not evaluate to a $DataType: :a, \ - but to a Int64: 5.", + "First conflicting entry: expression does not evaluate \ + to a component:\n\ + Expression: :a\n\ + Result: 5 ::$Int", ) @xconffails( (@conflicts(A, a)), - "Conflicting entry: expression does not evaluate to a $DataType: :a, \ - but to a Int64: 5.", + "Conflicting entry: expression does not evaluate \ + to a component for '$Value':\n\ + Expression: :a\n\ + Result: 5 ::$Int", ) #--------------------------------------------------------------------------------------- @@ -118,43 +139,57 @@ end @test confs(C) == [(C, D, nothing)] @test confs(D) == [(D, C, "D dislikes C.")] - s = System{Value}(A(), C()) - @sysfails(s + D(), Check(D), "conflicts with component '$C': D dislikes C.") + s = System{Value}(A.b(), C.b()) + @sysfails( + s + D.b(), + Add(ConflictWithSystemComponent, _D, nothing, [D.b], _C, nothing, "D dislikes C.") + ) # Invalid reasons specs. @xconffails( (@conflicts(E, (4 + 5) => (E => "ok"))), - "Conflicting entry: expression does not evaluate to a $DataType: :(4 + 5), \ - but to a Int64: 9.", + "Conflicting entry: expression does not evaluate \ + to a component for '$Value':\n\ + Expression: :(4 + 5)\n\ + Result: 9 ::$Int", ) + @pconffails((@conflicts(E, F => (4 + 5))), "Not a list of conflict reasons: :(4 + 5).") + @xconffails( (@conflicts(E, F => (E => 4 + 5))), "Reason message: expression does not evaluate to a String: :(4 + 5), \ but to a Int64: 9." ) + @xconffails( (@conflicts(E, F => (4 + 5 => "ok"))), - "Reason reference: expression does not evaluate to a $DataType: :(4 + 5), \ - but to a Int64: 9." + "Reason reference: expression does not evaluate \ + to a component for '$Value':\n\ + Expression: :(4 + 5)\n\ + Result: 9 ::$Int", ) + @xconffails( (@conflicts(E, F => (A => "A dislikes F."))), "Conflict reason does not refer to a component listed \ - in the same @conflicts invocation: $A => \"A dislikes F.\"." + in the same @conflicts invocation: $_A => \"A dislikes F.\"." ) + @xconffails( (@conflicts(E, F => (F => "F again?"))), - "Component '$F' cannot conflict with itself." + "Component $_F cannot conflict with itself." ) + @xconffails( - (@conflicts(E, F => (Invocations.F => "F again?"))), - "Component '$F' cannot conflict with itself." + (@conflicts(E, F => (F => "F again?"))), + "Component $_F cannot conflict with itself." ) + @xconffails( (@conflicts(E, F => (B => "B?"))), "Conflict reason does not refer to a component \ - listed in the same @conflicts invocation: $B => \"B?\"." + listed in the same @conflicts invocation: $_B => \"B?\"." ) # Same, but with a list of reasons. @@ -163,15 +198,19 @@ end "Reason message: expression does not evaluate to a String: :(4 + 5), \ but to a Int64: 9." ) + @xconffails( (@conflicts(E, F, G => [F => "ok", 4 + 5 => "message"])), - "Reason reference: expression does not evaluate to a $DataType: :(4 + 5), \ - but to a Int64: 9." + "Reason reference: expression does not evaluate \ + to a component for '$Value':\n\ + Expression: :(4 + 5)\n\ + Result: 9 ::$Int", ) + @xconffails( (@conflicts(E, F, G => (F => "ok", A => "A dislikes F."))), "Conflict reason does not refer to a component listed \ - in the same @conflicts invocation: $A => \"A dislikes F.\"." + in the same @conflicts invocation: $_A => \"A dislikes F.\"." ) #--------------------------------------------------------------------------------------- @@ -219,7 +258,7 @@ end # .. unless it would override the reason already specified. @xconffails( (@conflicts(B, U, V => (U => "New reason why V dislikes U."))), - "Component '$V' already declared to conflict with '$U' \ + "Component $_V already declared to conflict with $_U \ for the following reason:\n V dislikes U.", ) @@ -234,7 +273,7 @@ using Main: @sysfails, @pconffails, @xconffails using Test const S = System{Value} -comps(s) = sort(collect(components(s)); by = repr) +comps(s) = collect(components(s)) @testset "Abstract component types conflicts." begin @@ -246,80 +285,121 @@ comps(s) = sort(collect(components(s)); by = repr) # │ │ │ │ │ │ # D E F H I J # - abstract type A <: Blueprint{Value} end - abstract type G <: Blueprint{Value} end + abstract type A <: Component{Value} end + abstract type G <: Component{Value} end abstract type B <: A end abstract type C <: A end - struct D <: B end - struct E <: C end - struct F <: C end - struct H <: G end - struct I <: G end - struct J <: G end - @component D - @component E - @component F - @component H - @component I - @component J + struct D_b <: Blueprint{Value} end + struct E_b <: Blueprint{Value} end + struct F_b <: Blueprint{Value} end + struct H_b <: Blueprint{Value} end + struct I_b <: Blueprint{Value} end + struct J_b <: Blueprint{Value} end + @blueprint D_b + @blueprint E_b + @blueprint F_b + @blueprint H_b + @blueprint I_b + @blueprint J_b + @component D <: B blueprints(b::D_b) + @component E <: C blueprints(b::E_b) + @component F <: C blueprints(b::F_b) + @component H <: G blueprints(b::H_b) + @component I <: G blueprints(b::I_b) + @component J <: G blueprints(b::J_b) # Conflict between abstract and concrete component types. @conflicts(A, H => (A => "H dislikes A.")) - @test comps(S(D(), I())) == [D, I] # - @test comps(S(D(), I())) == [D, I] # (allowed combinations) - @test comps(S(J(), D())) == [D, J] # - @test comps(S(J(), D())) == [D, J] # + @test comps(S(D.b(), I.b())) == [D, I] # + @test comps(S(I.b(), D.b())) == [I, D] # (allowed combinations) + @test comps(S(D.b(), J.b())) == [D, J] # + @test comps(S(J.b(), D.b())) == [J, D] # @sysfails( - S(D(), H()), # (with an explicit reason) - Check(H), - "conflicts with component '$D' (as '$A'): H dislikes A." + S(D.b(), H.b()), # (with an explicit reason) + Add(ConflictWithSystemComponent, _H, nothing, [H_b], _D, A, "H dislikes A."), ) @sysfails( - S(H(), D()), # (without explicit reason) - Check(D), - "conflicts (as '$A') with component '$H'." + S(H.b(), D.b()), # (without explicit reason) + Add(ConflictWithSystemComponent, _D, A, [D_b], H, nothing, nothing), ) # Conflict between two abstract component types. @conflicts(C => (B => "C dislikes B."), B) - @test comps(S(E(), I())) == [E, I] # - @test comps(S(F(), I())) == [F, I] # (allowed combinations) - @test comps(S(J(), E())) == [E, J] # - @test comps(S(J(), F())) == [F, J] # + @test comps(S(E.b(), I.b())) == [E, I] # + @test comps(S(F.b(), I.b())) == [F, I] # (allowed combinations) + @test comps(S(J.b(), E.b())) == [J, E] # + @test comps(S(J.b(), F.b())) == [J, F] # @sysfails( - S(D(), E()), # (with an explicit reason) - Check(E), - "conflicts (as '$C') with component '$D' (as '$B'): C dislikes B." + S(D.b(), E.b()), # (with an explicit reason) + Add(ConflictWithSystemComponent, _E, C, [E_b], _D, B, "C dislikes B."), ) @sysfails( - S(E(), D()), # (without explicit reason) - Check(D), - "conflicts (as '$B') with component '$E' (as '$C')." + S(E.b(), D.b()), # (without explicit reason) + Add(ConflictWithSystemComponent, _D, B, [D_b], _E, C, nothing), ) # Forbid vertical conflicts. @xconffails( @conflicts(G, I), - "Component '$I' cannot conflict with its own supertype '$G'." + "Component $_I cannot conflict with its own super-component $G." ) @xconffails( @conflicts(I, G), - "Component '$I' cannot conflict with its own supertype '$G'." + "Component $_I cannot conflict with its own super-component $G." ) # Guard against redundant reason specifications. @xconffails( @conflicts(F, H => (F => "H dislikes F.")), - "Component '$H' already declared to conflict with '$F' (as '$A') \ + "Component $_H already declared to conflict with $_F (as $A) \ for the following reason:\n H dislikes A." ) @xconffails( @conflicts(D, E => (D => "E dislikes D.")), - "Component '$E' (as '$C') already declared to conflict with '$D' (as '$B') \ + "Component $_E (as $C) already declared to conflict with $_D (as $B) \ for the following reason:\n C dislikes B." ) + # Conflict with brought components. + struct Crh_b <: Blueprint{Value} + c::Brought(C) + end + Framework.implied_blueprint_for(::Crh_b, ::C) = E.b() + @blueprint Crh_b + Framework.componentsof(::Crh_b) = (_D,) + + crh = Crh_b(E.b()) + @sysfails( + S(crh), + Add( + ConflictWithBroughtComponent, + _D, + B, + [Crh_b], + _E, + C, + [E.b, false, Crh_b], + nothing, + ) + ) + + # Or implied. + crh = Crh_b(E) + @sysfails( + S(crh), + Add( + ConflictWithBroughtComponent, + _D, + B, + [Crh_b], + _E, + C, + [E.b, true, Crh_b], + nothing, + ) + ) + + end end - end diff --git a/test/framework/runtests.jl b/test/framework/runtests.jl index 08f2efaa2..091c5cafe 100644 --- a/test/framework/runtests.jl +++ b/test/framework/runtests.jl @@ -11,6 +11,7 @@ only = [ "./01-regular_use.jl", "./02-blueprint_macro.jl", "./03-component_macro.jl", + "./04-conflicts_macro.jl", ] # Unless some files are specified here, in which case only run these. if isempty(only) for (folder, _, files) in walkdir(dirname(@__FILE__))