org.gradle.model.internal.registry.DefaultModelRegistryTest.groovy Maven / Gradle / Ivy
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.model.internal.registry
import org.gradle.api.Action
import org.gradle.api.Transformer
import org.gradle.internal.Actions
import org.gradle.internal.BiAction
import org.gradle.internal.BiActions
import org.gradle.model.*
import org.gradle.model.internal.core.*
import org.gradle.model.internal.core.rule.describe.SimpleModelRuleDescriptor
import org.gradle.model.internal.fixture.ModelRegistryHelper
import org.gradle.model.internal.type.ModelType
import org.gradle.model.internal.type.ModelTypes
import org.gradle.util.TextUtil
import spock.lang.Specification
import spock.lang.Unroll
import static org.gradle.model.internal.core.NodePredicate.allDescendants
import static org.gradle.model.internal.core.NodePredicate.allLinks
import static org.gradle.util.TextUtil.normaliseLineSeparators
class DefaultModelRegistryTest extends Specification {
def registry = new ModelRegistryHelper()
def "can maybe get non existing"() {
when:
registry.realize("foo")
then:
thrown IllegalStateException
when:
def modelElement = registry.find("foo", ModelType.untyped())
then:
noExceptionThrown()
and:
modelElement == null
}
def "can get element for which a registration has been registered"() {
given:
registry.registerInstance("foo", "value")
expect:
registry.realize("foo", Object) == "value"
registry.realize("foo", String) == "value"
}
def "can get root node"() {
expect:
registry.realizeNode(ModelPath.ROOT) != null
}
def "cannot get element for which registration by-path input does not exist"() {
given:
registry.register("foo") { it.descriptor("foo creator").unmanaged(String, "other", null, Stub(Transformer)) }
when:
registry.realize("foo")
then:
UnboundModelRulesException e = thrown()
normaliseLineSeparators(e.message).contains '''
foo creator
inputs:
- other Object [*]
'''
}
def "cannot get element for which registration by-type input does not exist"() {
given:
registry.register("foo") { it.descriptor("foo creator").unmanaged(String, Long, Stub(Transformer)) }
when:
registry.realize("foo")
then:
UnboundModelRulesException e = thrown()
normaliseLineSeparators(e.message).contains '''
foo creator
inputs:
- Long [*]
'''
}
def "cannot register when by-type input is ambiguous"() {
given:
registry.registerInstance("other-1", 11)
registry.registerInstance("other-2", 12)
when:
registry.register("foo") { it.descriptor("foo creator").unmanaged(String, Number, Stub(Transformer)) }
registry.bindAllReferences()
then:
InvalidModelRuleException e = thrown()
e.cause instanceof ModelRuleBindingException
normaliseLineSeparators(e.cause.message) == """Type-only model reference of type java.lang.Number is ambiguous as multiple model elements are available for this type:
- other-1 (created by: other-1 creator)
- other-2 (created by: other-2 creator)"""
}
def "cannot register already known element"() {
given:
registry.register("foo") { it.descriptor("create foo as String").unmanaged("value") }
when:
registry.register("foo") { it.descriptor("create foo as Integer").unmanaged(12.toInteger()) }
then:
DuplicateModelException e = thrown()
e.message == /Cannot create 'foo' using creation rule 'create foo as Integer' as the rule 'create foo as String' is already registered to create this model element./
}
def "cannot register when element already closed"() {
given:
registry.register("foo") { it.descriptor("create foo as String").unmanaged("value") }
registry.realize("foo")
when:
registry.register("foo") { it.descriptor("create foo as Integer").unmanaged(12.toInteger()) }
then:
DuplicateModelException e = thrown()
e.message == /Cannot create 'foo' using creation rule 'create foo as Integer' as the rule 'create foo as String' has already been used to create this model element./
}
def "cannot register when sibling with same type used as by-type input"() {
given:
registry.registerInstance("other-1", 12)
registry.register("foo") { it.descriptor("foo creator").unmanaged(String, Number, Stub(Transformer)) }
registry.registerInstance("other-2", 11)
when:
registry.bindAllReferences()
then:
InvalidModelRuleException e = thrown()
e.cause instanceof ModelRuleBindingException
normaliseLineSeparators(e.cause.message) == """Type-only model reference of type java.lang.Number is ambiguous as multiple model elements are available for this type:
- other-1 (created by: other-1 creator)
- other-2 (created by: other-2 creator)"""
}
def "rule cannot add link when element already known"() {
def mutatorAction = Mock(Action)
given:
registry.register("foo") { it.descriptor("create foo as Integer").unmanaged(12.toInteger()) }
registry.mutate { it.path "foo" type Integer descriptor "mutate foo as Integer" node mutatorAction }
mutatorAction.execute(_) >> { MutableModelNode node ->
node.addLink("foo.bar") { it.descriptor("create foo.bar as String").unmanaged("12") }
node.addLink("foo.bar") { it.descriptor("create foo.bar as Integer").unmanaged(12) }
}
when:
registry.realize("foo")
then:
ModelRuleExecutionException e = thrown()
e.message == /Exception thrown while executing model rule: mutate foo as Integer/
e.cause instanceof DuplicateModelException
e.cause.message == /Cannot create 'foo.bar' using creation rule 'create foo.bar as Integer' as the rule 'create foo.bar as String' is already registered to create this model element./
}
def "inputs for creation are bound when inputs already closed"() {
def action = Mock(Transformer)
given:
registry.registerInstance("foo", 12.toInteger())
registry.realize("foo")
registry.register("bar") { it.unmanaged String, Integer, action }
action.transform(12) >> "[12]"
expect:
registry.realize("bar") == "[12]"
}
def "inputs for creation are bound when inputs already known"() {
def action = Mock(Transformer)
given:
registry.registerInstance("foo", 12.toInteger())
registry.register("bar") { it.unmanaged String, Integer, action }
action.transform(12) >> "[12]"
expect:
registry.realize("bar") == "[12]"
}
def "inputs for creation are bound as inputs become known"() {
def action = Mock(Transformer)
given:
registry.register("bar") { it.unmanaged String, Integer, action }
registry.registerInstance("foo", 12.toInteger())
action.transform(12) >> "[12]"
expect:
registry.realize("bar") == "[12]"
}
def "parent of input is implicitly closed when input is not known"() {
given:
registry.register("bar") { it.unmanaged(String, "foo.child", { input -> "[$input]" }) }
registry.registerInstance("foo", "foo")
registry.mutate {
it.path "foo" type String node {
node -> node.addLinkInstance("foo.child", 12)
}
}
expect:
registry.realize("bar") == "[12]"
}
def "input path can point to a reference"() {
given:
registry.registerInstance("target", "value")
def target = registry.node("target")
registry.register("ref") { parentBuilder ->
parentBuilder.unmanagedNode(Object) { node ->
node.addReference("direct", String, target)
}
}
registry.register("foo") { it.unmanaged(String, "ref.direct") { it } }
expect:
registry.realize("foo", String) == "value"
}
def "input path can traverse a reference"() {
given:
registry.register("parent") { parentBuilder ->
parentBuilder.unmanagedNode(Object) { node ->
node.addLinkInstance("parent.child", "value")
}
}
def parent = registry.node("parent")
registry.register("ref") { parentBuilder ->
parentBuilder.unmanagedNode(Object) { node ->
node.addReference("indirect", String, parent)
}
}
registry.register("foo") { it.unmanaged(String, "ref.indirect.child") { it } }
expect:
registry.realize("foo", String) == "value"
}
def "child reference can be null when parent is realized"() {
given:
registry.register("parent") { parentBuilder ->
parentBuilder.unmanagedNode(String) { node ->
node.addReference("child", String, null)
}
}
when:
registry.realize("parent", String)
then:
noExceptionThrown()
}
def "reference can point to ancestor of node"() {
given:
registry.register("parent") { parentBuilder ->
parentBuilder.unmanagedNode(String) { node ->
node.addReference("child", String, node)
node.applyToSelf(ModelActionRole.Mutate) { it.path("parent").node { it.setPrivateData(String, "value") }}
}
}
expect:
registry.realize("parent.child", String) == "value"
}
def "child can be made known"() {
given:
registry.register("parent") { parentBuilder ->
parentBuilder.unmanagedNode(String) { node ->
node.addLinkInstance("parent.child", "child")
}
}
when:
registry.realize("parent.child", String)
then:
noExceptionThrown()
}
def "cannot change a reference after it has been discovered"() {
given:
registry.registerInstance("target", "value")
def target = registry.node("target")
registry.root.addReference("ref", String, target)
def ref = registry.atState("ref", ModelNode.State.Discovered)
when:
ref.setTarget(newTarget)
then:
IllegalStateException e = thrown()
e.message == "Cannot set target for model element 'ref' as this element is not mutable."
where:
newTarget << [null, Stub(MutableModelNode)]
}
def "rules are invoked in order before element is closed"() {
def action = Mock(Action)
given:
registry
.register("foo") { it.unmanaged(new Bean(), action) }
.configure(ModelActionRole.Defaults) { it.path("foo").type(Bean).action(action) }
.configure(ModelActionRole.Initialize) { it.path("foo").type(Bean).action(action) }
.configure(ModelActionRole.Mutate) { it.path("foo").type(Bean).action(action) }
.configure(ModelActionRole.Finalize) { it.path("foo").type(Bean).action(action) }
.configure(ModelActionRole.Validate) { it.path("foo").type(Bean).action(action) }
when:
def value = registry.realize("foo", Bean).value
then:
value == "create > defaults > initialize > mutate > finalize"
and:
1 * action.execute(_) >> { Bean bean ->
assert bean.value == null
bean.value = "create"
}
1 * action.execute(_) >> { Bean bean ->
bean.value += " > defaults"
}
1 * action.execute(_) >> { Bean bean ->
bean.value += " > initialize"
}
1 * action.execute(_) >> { Bean bean ->
bean.value += " > mutate"
}
1 * action.execute(_) >> { Bean bean ->
bean.value += " > finalize"
}
1 * action.execute(_) >> { Bean bean ->
assert bean.value == "create > defaults > initialize > mutate > finalize"
}
0 * action._
when:
registry.realize("foo", Bean)
then:
0 * action._
}
def "registration for linked element invoked before element is closed"() {
def action = Mock(Action)
given:
registry.registerInstance("foo", new Bean())
registry.mutate { it.path "foo" type Bean node action }
when:
registry.realize("foo", Bean)
then:
1 * action.execute(_) >> { MutableModelNode node -> node.addLink("foo.bar") { it.unmanaged("value", action) } }
1 * action.execute(_)
0 * action._
}
def "inputs for mutator are bound when inputs already closed"() {
def action = Mock(BiAction)
given:
registry.registerInstance("foo", 12.toInteger())
registry.realize("foo")
registry.registerInstance("bar", new Bean())
registry.mutate { it.path("bar").type(Bean).action(Integer, action) }
action.execute(_, 12) >> { bean, value -> bean.value = "[12]" }
expect:
registry.realize("bar", Bean).value == "[12]"
}
def "inputs for mutator are bound when inputs already known"() {
def action = Mock(BiAction)
given:
registry.registerInstance("foo", 12.toInteger())
registry.registerInstance("bar", new Bean())
registry.mutate { it.path("bar").type(Bean).action(Integer, action) }
action.execute(_, 12) >> { bean, value -> bean.value = "[12]" }
expect:
registry.realize("bar", Bean).value == "[12]"
}
def "inputs for mutator are bound as inputs become known"() {
def action = Mock(BiAction)
given:
registry.registerInstance("bar", new Bean())
registry.mutate { it.path("bar").type(Bean).action(Integer, action) }
registry.registerInstance("foo", 12.toInteger())
action.execute(_, 12) >> { bean, value -> bean.value = "[12]" }
expect:
registry.realize("bar", Bean).value == "[12]"
}
def "transitions elements that depend on a particular state of an element when the target element leaves target state"() {
given:
registry.registerInstance("a", new Bean())
registry.registerInstance("b", new Bean())
registry.configure(ModelActionRole.Finalize) {
it.path("b").action(ModelReference.of(ModelPath.path("a"), ModelType.of(Bean), ModelNode.State.DefaultsApplied)) { Bean b, Bean a ->
b.value = "$b.value $a.value"
}
}
registry.configure(ModelActionRole.Mutate) {
it.path("b").action { Bean b ->
b.value = "b-mutate"
}
}
registry.configure(ModelActionRole.Mutate) {
it.path("a").action { Bean a ->
a.value = "a-mutate"
}
}
registry.configure(ModelActionRole.Defaults) {
it.path("a").action { Bean a ->
a.value = "a-defaults"
}
}
expect:
registry.realize("a", Bean).value == "a-mutate"
registry.realize("b", Bean).value == "b-mutate a-defaults"
}
def "transitions input elements to target state"() {
given:
registry.registerInstance("a", new Bean())
registry.registerInstance("b", new Bean())
registry.configure(ModelActionRole.Finalize) {
it.path("b").descriptor("b-finalize").action(ModelReference.of(ModelPath.path("a"), ModelType.of(Bean), ModelNode.State.DefaultsApplied)) { Bean b, Bean a ->
b.value = "$b.value $a.value"
}
}
registry.configure(ModelActionRole.Mutate) {
it.path("b").descriptor("b-mutate").action { Bean b ->
b.value = "b-mutate"
}
}
registry.configure(ModelActionRole.Mutate) {
it.path("a").descriptor("a-mutate").action { Bean a ->
a.value = "a-mutate"
}
}
registry.configure(ModelActionRole.Defaults) {
it.path("a").descriptor("a-defaults").action { Bean a ->
a.value = "a-defaults"
}
}
expect:
registry.realize("b", Bean).value == "b-mutate a-defaults"
registry.realize("a", Bean).value == "a-mutate"
}
def "can attach a mutator with inputs to all elements linked from an element"() {
given:
registry.register("parent") { it.unmanagedNode Integer, { MutableModelNode node ->
node.applyTo(allLinks(), ModelActionRole.Mutate) {
it.type(Bean).action(String) { Bean bean, String prefix ->
bean.value = "$prefix: $bean.value"
}
}
node.addLinkInstance("parent.foo", new Bean(value: "foo"))
node.addLinkInstance("parent.bar", new Bean(value: "bar"))
}
}
registry.registerInstance("prefix", "prefix")
expect:
registry.realize("parent.foo", Bean).value == "prefix: foo"
registry.realize("parent.bar", Bean).value == "prefix: bar"
}
def "can attach a mutator to all elements with specific type linked from an element"() {
def creatorAction = Mock(Action)
def mutatorAction = Mock(Action)
given:
registry.register("parent") { it.unmanagedNode Integer, creatorAction }
creatorAction.execute(_) >> { MutableModelNode node ->
node.applyTo(allLinks(), ModelActionRole.Mutate) { it.type(Bean).action(mutatorAction) }
node.addLinkInstance("parent.foo", "ignore me")
node.addLinkInstance("parent.bar", new Bean(value: "bar"))
}
registry.registerInstance("other", new Bean(value: "ignore me"))
mutatorAction.execute(_) >> { Bean bean -> bean.value = "prefix: $bean.value" }
registry.realize("parent") // TODO - should not need this: parent mutations should be applied before mutating element
expect:
registry.realize("parent.bar", Bean).value == "prefix: bar"
registry.realize("parent.foo", String) == "ignore me"
and:
registry.realize("other", Bean).value == "ignore me"
}
def "can attach a mutator to all elements with specific type transitively linked from an element"() {
def creatorAction = Mock(Action)
def mutatorAction = Mock(Action)
given:
registry.register("parent") { it.unmanagedNode Integer, creatorAction }
creatorAction.execute(_) >> { MutableModelNode node ->
node.applyTo(allDescendants(), ModelActionRole.Mutate) { it.type(Bean).action(mutatorAction) }
node.addLinkInstance("parent.foo", "ignore me")
node.addLinkInstance("parent.bar", new Bean(value: "bar"))
node.applyToLink(ModelActionRole.Mutate) { it.path("parent.bar").node { MutableModelNode bar ->
bar.addLinkInstance("parent.bar.child1", new Bean(value: "baz"))
bar.addLinkInstance("parent.bar.child2", "ignore me too")
}}
}
registry.registerInstance("other", new Bean(value: "ignore me"))
mutatorAction.execute(_) >> { Bean bean -> bean.value = "prefix: $bean.value" }
registry.realize("parent") // TODO - should not need this: parent mutations should be applied before mutating element
expect:
registry.realize("parent.bar", Bean).value == "prefix: bar"
registry.realize("parent.foo", String) == "ignore me"
registry.realize("parent.bar.child1", Bean).value == "prefix: baz"
registry.realize("parent.bar.child2", String) == "ignore me too"
and:
registry.realize("other", Bean).value == "ignore me"
}
def "can attach a mutator with inputs to element linked from another element"() {
def creatorAction = Mock(Action)
def mutatorAction = Mock(BiAction)
given:
registry.register("parent") { it.unmanagedNode Integer, creatorAction }
creatorAction.execute(_) >> { MutableModelNode node ->
node.applyToLink(ModelActionRole.Mutate) { it.path("parent.foo").type(Bean).action(String, mutatorAction) }
node.addLinkInstance("parent.foo", new Bean(value: "foo"))
node.addLinkInstance("parent.bar", new Bean(value: "bar"))
}
mutatorAction.execute(_, _) >> { Bean bean, String prefix ->
bean.value = "$prefix: $bean.value"
}
registry.registerInstance("prefix", "prefix")
registry.realize("parent") // TODO - should not need this: parent mutations should be applied before mutating element
expect:
registry.realize("parent.foo", Bean).value == "prefix: foo"
registry.realize("parent.bar", Bean).value == "bar"
}
def "cannot attach link when element is not mutable"() {
def action = Stub(Action)
given:
registry.registerInstance("thing", "value")
registry.configure(ModelActionRole.Validate) { it.path "thing" type Object node action }
action.execute(_) >> { MutableModelNode node -> node.addLink("thing.child") { it.descriptor("create thing.child as String").unmanaged("value") } }
when:
registry.realize("thing")
then:
ModelRuleExecutionException e = thrown()
e.cause instanceof IllegalStateException
e.cause.message == "Cannot create 'thing.child' using creation rule 'create thing.child as String' as model element 'thing' is no longer mutable."
}
def "cannot set value when element is not mutable"() {
def action = Stub(Action)
given:
registry.registerInstance("thing", "value")
registry.configure(ModelActionRole.Validate) { it.path("thing").type(Object).node(action) }
action.execute(_) >> { MutableModelNode node -> node.setPrivateData(ModelType.of(String), "value 2") }
when:
registry.realize("thing")
then:
ModelRuleExecutionException e = thrown()
e.cause instanceof IllegalStateException
e.cause.message == "Cannot set value for model element 'thing' as this element is not mutable."
}
def "can replace an element that has not been used as input by a rule"() {
given:
registry.registerInstance("thing", new Bean(value: "old"))
registry.configure(ModelActionRole.Mutate) { it.path("thing").action { it.value = "${it.value} path" } }
registry.configure(ModelActionRole.Mutate) { it.type(Bean).action { it.value = "${it.value} type" } }
registry.realize("thing")
registry.remove(ModelPath.path("thing"))
registry.registerInstance("thing", new Bean(value: "new"))
expect:
registry.realize("thing", Bean).value == "new path type"
}
def "cannot remove an element that has already been used as input by a rule"() {
given:
def action = Mock(BiAction)
registry.registerInstance("foo", 12.toInteger())
registry.registerInstance("bar", new Bean())
registry.mutate { it.path("bar").type(Bean).action(Integer, action) }
registry.realize("bar", Bean).value == "[12]"
when:
registry.remove(ModelPath.path("foo"))
then:
def ex = thrown IllegalStateException
ex.message == "Tried to remove model 'foo' but it is depended on by: 'bar'"
}
def "can remove an element with children that has not been used as input by a rule"() {
given:
registry.register("parent") { it.unmanagedNode (Integer) { MutableModelNode node ->
node.addLinkInstance("parent.foo", 12.toInteger())
}}
registry.realize("parent")
expect:
registry.atStateOrLater("parent", ModelNode.State.Registered).path == ModelPath.path("parent")
registry.atStateOrLater("parent.foo", ModelNode.State.Registered).path == ModelPath.path("parent.foo")
registry.remove(ModelPath.path("parent"))
when:
registry.atStateOrLater("parent", ModelNode.State.Registered) == null
then:
def exParent = thrown IllegalStateException
exParent.message == "No model node at 'parent'"
when:
registry.atStateOrLater("parent.foo", ModelNode.State.Registered) == null
then:
def exFoo = thrown IllegalStateException
exFoo.message == "No model node at 'parent.foo'"
}
def "cannot remove an element whose child has already been used as input by a rule"() {
given:
registry.register("parent") { it.unmanagedNode (Integer) { MutableModelNode node ->
node.addLinkInstance("parent.foo", 12.toInteger())
}}
registry.registerInstance("bar", new Bean())
registry.mutate { it.path("bar").action("parent.foo", Integer, BiActions.doNothing()) }
registry.realize("bar", Bean).value == "[12]"
when:
registry.remove(ModelPath.path("parent"))
then:
def ex = thrown IllegalStateException
ex.message == "Tried to remove model 'parent.foo' but it is depended on by: 'bar'"
}
@Unroll
def "cannot bind action targeting type for role #targetRole where type is not available"() {
when:
registry.configure(targetRole, ModelReference.of("thing", Bean), Actions.doNothing())
then:
def ex = thrown IllegalStateException
ex.message == "Cannot bind subject 'ModelReference{path=thing, scope=null, type=${Bean.name}, state=GraphClosed}' to role '${targetRole}' because it is targeting a type and subject types are not yet available in that role"
where:
targetRole << ModelActionRole.values().findAll { !it.subjectViewAvailable }
}
@Unroll
def "cannot execute action with role #targetRole where view is not available"() {
registry.configure(targetRole, new AbstractModelActionWithView(ModelReference.of("thing"), new SimpleModelRuleDescriptor(targetRole.name()), []) {
@Override
protected void execute(MutableModelNode modelNode, Bean view, List> inputs) {
}
})
when:
registry.registerInstance("thing", new Bean(value: "thing"))
registry.atStateOrLater(ModelPath.path("thing"), targetRole.targetState)
then:
def ex = thrown ModelRuleExecutionException
ex.cause instanceof IllegalStateException
ex.cause.message == "Cannot get view for node thing in state ${targetRole.targetState.previous()}"
where:
targetRole << ModelActionRole.values().findAll { !it.subjectViewAvailable }
}
@Unroll
def "cannot add action for #targetRole mutation when in later #fromRole mutation"() {
def action = Stub(Action)
given:
registry.registerInstance("thing", "value")
.configure(fromRole) { it.path("thing").node(action) }
action.execute(_) >> { MutableModelNode node -> registry
.configure(targetRole) { it.path("thing").type(String).descriptor("X").action {} }
}
when:
registry.realize("thing")
then:
ModelRuleExecutionException e = thrown()
e.cause instanceof IllegalStateException
e.cause.message == "Cannot add rule X for model element 'thing' at state ${targetRole.targetState.previous()} as this element is already at state ${fromRole.targetState.previous()}."
where:
[fromRole, targetRole] << ModelActionRole.values().collectMany { fromRole ->
return ModelActionRole.values().findAll { it.ordinal() < fromRole.ordinal() && it.subjectViewAvailable }.collect { targetRole ->
[ fromRole, targetRole ]
}
}
}
@Unroll
def "cannot add action for #targetRole mutation when in later #fromState state"() {
def action = Stub(Action)
given:
registry.registerInstance("thing", "value")
.registerInstance("another", "value")
.configure(ModelActionRole.Mutate) {
it.path("another").node(action)
}
action.execute(_) >> {
MutableModelNode node -> registry.configure(targetRole) { it.path("thing").descriptor("X").action {} }
}
when:
registry.atState(ModelPath.path("thing"), fromState)
registry.realize("another")
then:
ModelRuleExecutionException e = thrown()
e.cause instanceof IllegalStateException
e.cause.message == "Cannot add rule X for model element 'thing' at state ${targetRole.targetState.previous()} as this element is already at state ${fromState}."
where:
[fromState, targetRole] << ModelNode.State.values().collectMany { fromState ->
return ModelActionRole.values().findAll { it.targetState.ordinal() <= fromState.ordinal() }.collect { targetRole ->
[ fromState, targetRole ]
}
}
}
@Unroll
def "can add action for #targetRole when in #fromRole action"() {
given:
registry.configure(fromRole) {
it.path("thing").node { MutableModelNode node ->
registry.configure(targetRole) {
it.path("thing").type(Bean).action {
it.value = "mutated"
}
}
}
}
registry.registerInstance("thing", new Bean(value: "initial"))
when:
def thing = registry.realize("thing", Bean)
then:
thing.value == "mutated"
where:
[fromRole, targetRole] << ModelActionRole.values().collectMany { fromRole ->
return ModelActionRole.values().findAll { it.subjectViewAvailable && it.ordinal() >= fromRole.ordinal() }.collect { targetRole ->
return [ fromRole, targetRole ]
}
}
}
@Unroll
def "closes inputs for mutation discovered after running action with role #targetRole"() {
given:
registry.registerInstance("thing", new Bean(value: "initial"))
.configure(targetRole) {
it.path("thing").descriptor("outside").node { MutableModelNode node ->
registry.configure(targetRole) {
it.path("thing").type(Bean).descriptor("inside").action("other", ModelType.of(Bean), action)
}
}
}
// Include a dependency
registry.registerInstance("other", new Bean())
.mutate { it.path("other").type(Bean).action { it.value = "input value" } }
when:
def thing = registry.realize("thing", Bean)
then:
thing.value == expected
where:
[targetRole, action, expected] << ModelActionRole.values().findAll { it.subjectViewAvailable }.collect { role ->
return [role, { subject, dep -> subject.value = dep.value }, "input value"]
}
}
@Unroll
def "can add action for #targetRole mutation when in earlier #fromState state"() {
def action = Stub(Action)
given:
registry.registerInstance("thing", "value")
.registerInstance("another", "value")
.configure(ModelActionRole.Mutate) {
it.path("another").node(action)
}
action.execute(_) >> {
MutableModelNode node -> registry.configure(targetRole) {
it.path("thing").descriptor("X").action {}
}
}
when:
registry.atState(ModelPath.path("thing"), fromState)
registry.realize("another")
then:
noExceptionThrown()
where:
[fromState, targetRole] << (ModelNode.State.values() - ModelNode.State.Registered).collectMany { fromState ->
return ModelActionRole.values().findAll { it.targetState.ordinal() > fromState.ordinal() }.collect { targetRole ->
[ fromState, targetRole ]
}
}
}
@Unroll
def "can get node at state #state"() {
given:
ModelActionRole.values().each { role ->
registry.configure(role, { builder ->
builder.path "thing"
if (role.subjectViewAvailable) {
builder.type Bean
return builder.action({
it.value = role.name()
})
} else {
return builder.node(Actions.doNothing())
}
})
}
registry.registerInstance("thing", new Bean(value: "created"))
expect:
registry.atState(ModelPath.path("thing"), state).getPrivateData(ModelType.of(Bean))?.value == expected
where:
state | expected
ModelNode.State.Registered | null
ModelNode.State.Discovered | null
ModelNode.State.Created | "created"
ModelNode.State.DefaultsApplied | ModelActionRole.Defaults.name()
ModelNode.State.Initialized | ModelActionRole.Initialize.name()
ModelNode.State.Mutated | ModelActionRole.Mutate.name()
ModelNode.State.Finalized | ModelActionRole.Finalize.name()
ModelNode.State.SelfClosed | ModelActionRole.Validate.name()
ModelNode.State.GraphClosed | ModelActionRole.Validate.name()
}
def "can get node at state Known"() {
registry.registerInstance("thing", new Bean(value: "created"))
expect:
registry.atState(ModelPath.path("thing"), ModelNode.State.Registered).path.toString() == "thing"
}
@Unroll
def "asking for element at state #state does not create node"() {
given:
def events = []
registry.register("thing") { it.unmanaged(new Bean()) { events << "created" } }
when:
registry.atState(ModelPath.path("thing"), state)
then:
events == []
when:
registry.atState(ModelPath.path("thing"), ModelNode.State.Created)
then:
events == ["created"]
where:
state << [ModelNode.State.Registered, ModelNode.State.Discovered]
}
@Unroll
def "asking for unknown element at state #state fails"() {
when:
registry.atState(ModelPath.path("thing"), state)
then:
IllegalStateException e = thrown()
e.message == "No model node at 'thing'"
where:
state << ModelNode.State.values()
}
def "getting self closed collection defines all links but does not realise them until graph closed"() {
given:
def events = []
def mmType = ModelTypes.modelMap(Bean)
registry
.registerModelMap("things", Bean) { it.registerFactory(Bean) { new Bean(name: it) } }
.mutate {
it.path "things" type mmType action { c ->
events << "collection mutated"
c.create("c1") { events << "$it.name created" }
}
}
when:
def cbNode = registry.atState(ModelPath.path("things"), ModelNode.State.SelfClosed)
then:
events == ["collection mutated"]
cbNode.getLinkNames(ModelType.of(Bean)).toList() == ["c1"]
when:
registry.atState(ModelPath.path("things"), ModelNode.State.GraphClosed)
then:
events == ["collection mutated", "c1 created"]
}
@Unroll
def "cannot request model node at earlier state #targetState when at #fromState"() {
given:
registry.registerInstance("thing", new Bean())
registry.atState(ModelPath.path("thing"), fromState)
when:
registry.atState(ModelPath.path("thing"), targetState)
then:
def e = thrown IllegalStateException
e.message == "Cannot lifecycle model node 'thing' to state ${targetState.name()} as it is already at ${fromState.name()}"
where:
[fromState, targetState] << ModelNode.State.values().collectMany { fromState ->
return ModelNode.State.values().findAll { it.ordinal() < fromState.ordinal() }.collect { targetState ->
[ fromState, targetState ]
}
}
}
@Unroll
def "is benign to request element at current state #state"() {
given:
registry.registerInstance("thing", new Bean())
when:
// not in loop to get different stacktrace line numbers
registry.atState(ModelPath.path("thing"), state)
registry.atState(ModelPath.path("thing"), state)
registry.atState(ModelPath.path("thing"), state)
then:
noExceptionThrown()
where:
state << ModelNode.State.values() - ModelNode.State.Registered
}
@Unroll
def "is benign to request element at prior state #state"() {
given:
registry.registerInstance("thing", new Bean())
when:
registry.atState(ModelPath.path("thing"), state)
ModelNode.State.values().findAll { it.ordinal() <= state.ordinal() }.each {
registry.atStateOrLater(ModelPath.path("thing"), state)
}
then:
noExceptionThrown()
where:
state << ModelNode.State.values() - ModelNode.State.Registered
}
@Unroll
def "requesting at state #state does not reinvoke actions"() {
given:
def events = []
def uptoRole = ModelActionRole.values().findAll { it.ordinal() <= role.ordinal() }
uptoRole.each { r -> configureAction(r, "thing", Bean, { events << r.name() }) }
registry.registerInstance("thing", new Bean())
when:
registry.atState(ModelPath.path("thing"), state)
then:
events == uptoRole*.name()
when:
registry.atState(ModelPath.path("thing"), state)
then:
events == uptoRole*.name()
where:
[state, role] << ModelActionRole.values().collect { role -> [role.targetState, role]}
}
private void configureAction(ModelActionRole role, String path, def type, Action super MutableModelNode> nodeAction, Action super T> viewAction = null) {
registry.configure(role) {
it.path path
if (role.subjectViewAvailable) {
it.type type
if (viewAction != null) {
it.action viewAction
} else {
it.node nodeAction
}
} else {
it.node nodeAction
}
}
}
def "reports unbound subjects"() {
given:
registry.mutate { it.path("a.b").descriptor("by-path").action() {} }
registry.mutate { it.type(Long).descriptor("by-type").action() {} }
registry.mutate { it.path("missing").type(String).descriptor("by-path-and-type").action() {} }
when:
registry.bindAllReferences()
then:
UnboundModelRulesException e = thrown()
normaliseLineSeparators(e.message).contains '''
by-path
subject:
- a.b Object [*]
by-path-and-type
subject:
- missing String [*]
by-type
subject:
- Long [*]
'''
}
def "reports unbound inputs"() {
given:
registry.register("foo") { it.descriptor("creator").unmanaged(Long, "a.b") {} }
registry.mutate { it.path("foo").descriptor("by-path").action(ModelPath.path("other.thing"), ModelType.of(String)) {} }
registry.mutate { it.type(Runnable).descriptor("by-type").action(String) {} }
when:
registry.bindAllReferences()
then:
UnboundModelRulesException e = thrown()
normaliseLineSeparators(e.message).contains '''
by-path
subject:
- foo Object
inputs:
- other.thing String (java.lang.String) [*]
by-type
subject:
- Runnable [*]
inputs:
- String (java.lang.String) [*]
creator
inputs:
- a.b Object (a.b) [*]
'''
}
def "closes elements as required to bind all subjects and inputs"() {
given:
registry.mutate { it.path("a.1.2").action(ModelPath.path("b.1.2"), ModelType.of(String)) {} }
registry.register("a") { it.unmanaged("a") }
registry.mutate {
it.path("a").node {
it.addLinkInstance("a.1", "a.1")
it.applyToLink(ModelActionRole.Finalize) { it.path("a.1").node {
it.addLinkInstance("a.1.2", "a.1.2")
}}
}
}
registry.register("b") { it.unmanaged("b") }
registry.mutate {
it.path("b").node {
it.addLinkInstance("b.1", "b.1")
it.applyToLink(ModelActionRole.Finalize) { it.path("b.1").node {
it.addLinkInstance("b.1.2", "b.1.2")
}}
}
}
when:
registry.bindAllReferences()
then:
noExceptionThrown()
}
def "only rules that actually have unbound inputs are reported as unbound"() {
given:
def mmType = ModelTypes.modelMap(Bean)
registry
.registerInstance("foo", new Bean())
.mutate {
it.descriptor("non-bindable").path("foo").type(Bean).action("emptyBeans.element", ModelType.of(Bean), null, {})
}
.mutate {
it.descriptor("bindable").path("foo").type(Bean).action("beans.element", ModelType.of(Bean)) {
}
}
.registerModelMap("beans", Bean) { it.registerFactory(Bean) { new Bean(name: it) } }
.mutate {
it.path "beans" type mmType action { c ->
c.create("element")
}
}
.registerModelMap("emptyBeans", Bean) { it.registerFactory(Bean) { new Bean(name: it) } }
when:
registry.bindAllReferences()
then:
UnboundModelRulesException e = thrown()
normaliseLineSeparators(e.message).contains '''
non-bindable
subject:
- foo DefaultModelRegistryTest.Bean
inputs:
- emptyBeans.element DefaultModelRegistryTest.Bean [*]
'''
}
def "does not report unbound actions applied at registration as unbound after the nodes is removed"() {
given:
def registration = ModelRegistrations.of(ModelPath.path("unused")).descriptor("unused")
ModelActionRole.values().each { role ->
registration.action(role, [ModelReference.of("unknown")], BiActions.doNothing())
}
registry.register(registration.build())
registry.remove(ModelPath.path("unused"))
when:
registry.bindAllReferences()
then:
noExceptionThrown()
}
def "two element mutation rule based configuration cycles are detected"() {
given:
registry.registerInstance("foo", "foo")
.registerInstance("bar", "bar")
.mutate { it.path("foo").descriptor("foo mutator").type(String).action("bar", ModelType.of(String), "parameter 1", {}) }
.mutate { it.path("bar").descriptor("bar mutator").type(String).action("foo", ModelType.of(String), null, {}) }
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
foo
\\- foo mutator
\\- bar
\\- bar mutator
\\- foo""")
}
def "multiple element configuration cycles are detected"() {
registry.register("foo") { it.unmanaged(String, "bar") { "foo" } }
.register("bar") { it.unmanaged(String, "fizz") { "bar" } }
.registerInstance("fizz", "fizz")
.mutate { it.path("fizz").descriptor("fizz mutator").type(String).action("buzz", ModelType.of(String), {}) }
.registerInstance("buzz", "buzz")
.mutate { it.path("buzz").descriptor("buzz mutator").type(String).action("foo", ModelType.of(String), {}) }
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
foo
\\- foo creator
\\- bar
\\- bar creator
\\- fizz
\\- fizz mutator
\\- buzz
\\- buzz mutator
\\- foo""")
}
def "one element configuration cycles are detected"() {
given:
registry.registerInstance("foo", "foo")
.mutate { it.path("foo").descriptor("foo mutator").type(String).action(String) {} }
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
foo
\\- foo mutator
\\- foo""")
}
def "only the elements actually forming the cycle are reported when configuration cycles are detected"() {
given:
registry.register("foo") { it.unmanaged(Long, "bar") { 12 } }
.register("bar") { it.unmanaged(String, "fizz") { "bar" } }
.mutate { it.path("foo").action(String) {} }
.register("fizz") { it.unmanaged(Boolean, "buzz") { "buzz" } }
.mutate { it.path("fizz").descriptor("fizz mutator").action("bar", ModelType.of(String), {}) }
.registerInstance("buzz", Long)
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
bar
\\- bar creator
\\- fizz
\\- fizz mutator
\\- bar""")
}
def "implicit cycle when node depends on parent is detected"() {
given:
registry.registerInstance("foo", "foo")
.mutate { it.path("foo").descriptor("foo mutator").node { it.addLink("foo.bar") { it.unmanaged(Number, 12) } } }
.mutate { it.path("foo.bar").descriptor("bar mutator").action(String) {} }
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
foo
\\- foo.bar
\\- bar mutator
\\- foo""")
}
def "implicit cycle when node depends on ancestor is detected"() {
given:
registry.registerInstance("foo", "foo")
.mutate { it.path("foo").descriptor("foo mutator").node { it.addLink("foo.bar") { it.unmanaged(Number, 12) } } }
.mutate { it.path("foo.bar").descriptor("bar mutator").node { it.addLink("foo.bar.baz") { it.unmanaged(Number, 107) } } }
.mutate { it.path("foo.bar.baz").descriptor("baz mutator").action(String) {} }
when:
registry.get("foo")
then:
ConfigurationCycleException e = thrown()
e.message == TextUtil.toPlatformLineSeparators("""A cycle has been detected in model rule dependencies. References forming the cycle:
foo
\\- foo.bar
\\- foo.bar.baz
\\- baz mutator
\\- foo""")
}
def "node can be viewed via projection registered via projector"() {
registry.configure(ModelActionRole.Discover) { it.path "foo" descriptor "project" node { node ->
node.addProjection UnmanagedModelProjection.of(BeanInternal)
} }
registry
.register("foo") { it.unmanaged(Bean, new AdvancedBean(name: "foo")) }
.mutate (BeanInternal) { bean ->
bean.internal = "internal"
}
expect:
def bean = registry.realize("foo")
assert bean instanceof AdvancedBean
bean.internal == "internal"
}
def "can register projection after node is registered"() {
registry
.register("foo") { it.unmanaged(Bean, new AdvancedBean(name: "foo")) }
.mutate (BeanInternal) { bean ->
bean.internal = "internal"
}
registry.configure(ModelActionRole.Discover) { it.path "foo" descriptor "project" node { node ->
node.addProjection UnmanagedModelProjection.of(BeanInternal)
} }
expect:
def bean = registry.realize("foo")
assert bean instanceof AdvancedBean
bean.internal == "internal"
}
def "cannot register projection after node has been discovered"() {
given:
registry.register("foo") { it.unmanaged(Bean, new AdvancedBean(name: "foo")) }
registry.atState("foo", ModelNode.State.Discovered)
when:
registry.configure(ModelActionRole.Discover) { it.path "foo" descriptor "project" node {} }
then:
def ex = thrown IllegalStateException
ex.message == "Cannot add rule project for model element 'foo' at state ${ModelNode.State.Registered} as this element is already at state ${ModelNode.State.Discovered}."
}
def "discover children of scope when defining scope when node matching input type is not already discovered"() {
registry.register("dep") { it.unmanaged(Bean, new Bean()) }
registry.register("target") { it.unmanaged(String) }
registry.register("childA") { it.unmanaged(String) }
registry.register("childB") { it.unmanaged(String) }
registry.configure(ModelActionRole.Mutate) { it.path("target").action(Bean, BiActions.doNothing())}
when:
registry.realize("target")
then:
registry.state("dep") == ModelNode.State.GraphClosed
registry.state("target") == ModelNode.State.GraphClosed
registry.state("childA") == ModelNode.State.Discovered
registry.state("childB") == ModelNode.State.Discovered
}
def "does not discover children of scope when node matching input type is already in discovered"() {
registry.register(ModelRegistrations.bridgedInstance(ModelReference.of("dep", Bean), new Bean()).descriptor("dep").build())
registry.atState("dep", ModelNode.State.Discovered)
registry.register("target") { it.unmanaged(String) }
registry.register("childA") { it.unmanaged(String) }
registry.register("childB") { it.unmanaged(String) }
registry.configure(ModelActionRole.Mutate) { it.path("target").action(Bean, BiActions.doNothing()) }
when:
registry.realize("target")
then:
registry.state("dep") == ModelNode.State.GraphClosed
registry.state("target") == ModelNode.State.GraphClosed
registry.state("childA") == ModelNode.State.Registered
registry.state("childB") == ModelNode.State.Registered
}
def "fails when another child in scope with matching bound rule's target type is discovered"() {
registry.register(ModelRegistrations.bridgedInstance(ModelReference.of("dep", Bean), new Bean()).descriptor("dep creator").build())
registry.atState("dep", ModelNode.State.Discovered)
registry.register("target") { it.unmanaged(String) }
registry.register("childA") { it.unmanaged(String) }
registry.register("childB") { it.unmanaged(String) }
registry.configure(ModelActionRole.Mutate) { it.path("target").action(Bean, BiActions.doNothing()) }
when:
registry.register(ModelRegistrations.bridgedInstance(ModelReference.of("dep2", Bean), new Bean()).descriptor("dep2 creator").build())
then:
noExceptionThrown()
when:
registry.bindAllReferences()
then:
def ex = thrown InvalidModelRuleException
ex.cause instanceof ModelRuleBindingException
ex.cause.message == TextUtil.toPlatformLineSeparators("""Type-only model reference of type $Bean.name ($Bean.name) is ambiguous as multiple model elements are available for this type:
- dep (created by: dep creator)
- dep2 (created by: dep2 creator)""")
}
def "can apply RuleSource to node"() {
when:
registry.registerInstance("bean", new Bean())
registry.configure(ModelActionRole.Create) { it.path("bean").node { node -> node.applyToSelf(BeanRules) } }
then:
def bean = registry.realize("bean", Bean)
bean.name == 'bean'
bean.value == '12'
}
def "can apply RuleSource to node via reference"() {
when:
registry.registerInstance("target", new Bean())
registry.root.addReference("bean", Bean, registry.root.getLink("target"))
registry.configure(ModelActionRole.Defaults) { it.path("bean").node { node -> node.applyToSelf(BeanRules) } }
// TODO - should not need this: target of the reference should be transitioned
registry.realize("target", Bean)
then:
def bean = registry.realize("bean", Bean)
bean.name == 'bean'
bean.value == '12'
}
@Unroll
def "can apply #description to each element matching type in root scope"() {
def mmType = ModelTypes.modelMap(Bean)
registry.registerInstance("foo", "foo")
registry.registerInstance("bean1", new Bean(name: "bean1 unmodified"))
registry.registerModelMap("beans", Bean) { it.registerFactory(Bean) { new Bean(name: it + " unmodified") } }
registry.mutate {
it.path "beans" type mmType action { beans ->
beans.create("bean2")
}
}
registry.root.applyToSelf(rules)
expect:
registry.realize("bean1", Bean).name == "bean"
registry.realize("beans.bean2", Bean).name == "bean"
where:
rules | description
EachBeanViaDirectRule | "direct rule"
EachBeanViaRuleSource | "rule source"
}
@Unroll
def "#description is not applied to descendants accessible only via references"() {
def mmType = ModelTypes.modelMap(Bean)
registry.registerInstance("bean1", new Bean(name: "bean1 unmodified"))
registry.registerInstance("beans", new Bean(name: "beans"))
registry.registerModelMap("otherBeans", Bean) { it.registerFactory(Bean) { new Bean(name: it + " unmodified") } }
registry.mutate {
it.path "otherBeans" type mmType action { beans ->
beans.create("bean3")
}
}
registry.mutate {
it.path "beans" node {
it.addLinkInstance("beans.bean2", new Bean(name: "bean2 unmodified"))
it.addReference("beanRef", Bean, registry.root.getLink("bean1"))
it.addReference("otherBeansRef", Bean, registry.root.getLink("otherBeans"))
}
}
def scope = registry.root.getLink("beans")
scope.applyToSelf(rules)
expect:
// Rule gets applied to node in scope
registry.realize("beans.bean2", Bean).name == "bean"
// Rule doesn't get applied to node outside scope
registry.realize("bean1", Bean).name == "bean1 unmodified"
// Rule is not applied to referenced node
registry.realize("beans.beanRef", Bean).name == "bean1 unmodified"
// Rule is not applied to descendant node of referenced node (via "beans.otherBeans")
registry.realize("otherBeans.bean3", Bean).name == "bean3 unmodified"
where:
rules | description
EachBeanViaDirectRule | "direct rule"
EachBeanViaRuleSource | "rule source"
}
@Unroll
def "#description is not applied to scope element"() {
registry.registerInstance("bean1", new Bean(name: "bean1 unmodified"))
registry.mutate {
it.path "bean1" node {
it.addLinkInstance("bean1.bean2", new Bean(name: "bean2 unmodified"))
}
}
registry.root.getLink("bean1").applyToSelf(rules)
expect:
// Rule is not applied to scope node
registry.realize("bean1", Bean).name == "bean1 unmodified"
// Rule is applied to child of scope node
registry.realize("bean1.bean2", Bean).name == "bean"
where:
rules | description
EachBeanViaDirectRule | "direct rule"
EachBeanViaRuleSource | "rule source"
}
static class Bean {
String name
String value
}
static interface BeanInternal {
String getInternal()
void setInternal(String internal)
}
static class AdvancedBean extends Bean implements BeanInternal {
String internal
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy