All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gradle.api.internal.AbstractDomainObjectCollectionSpec.groovy Maven / Gradle / Ivy

There is a newer version: 8.6
Show newest version
/*
 * Copyright 2018 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.api.internal


import org.gradle.api.Action
import org.gradle.api.DomainObjectCollection
import org.gradle.api.internal.plugins.DslObject
import org.gradle.api.internal.provider.CollectionProviderInternal
import org.gradle.api.internal.provider.ProviderInternal
import org.gradle.api.internal.provider.ValueSupplier
import org.gradle.configuration.internal.DefaultUserCodeApplicationContext
import org.gradle.configuration.internal.UserCodeApplicationContext
import org.gradle.configuration.internal.UserCodeApplicationId
import org.gradle.internal.Actions
import org.gradle.internal.DisplayName
import org.gradle.internal.metaobject.ConfigureDelegate
import org.gradle.internal.operations.TestBuildOperationExecutor
import org.gradle.util.ConfigureUtil
import org.gradle.util.TestUtil
import org.hamcrest.CoreMatchers
import spock.lang.Specification
import spock.lang.Unroll

import static org.gradle.util.Matchers.hasMessage
import static org.gradle.util.WrapUtil.toList
import static org.hamcrest.CoreMatchers.startsWith
import static org.hamcrest.MatcherAssert.assertThat
import static org.junit.Assume.assumeTrue

abstract class AbstractDomainObjectCollectionSpec extends Specification {

    TestBuildOperationExecutor buildOperationExecutor = new TestBuildOperationExecutor()
    UserCodeApplicationContext userCodeApplicationContext = new DefaultUserCodeApplicationContext()
    CollectionCallbackActionDecorator callbackActionDecorator = new DefaultCollectionCallbackActionDecorator(buildOperationExecutor, userCodeApplicationContext)

    abstract boolean isSupportsBuildOperations()

    abstract DomainObjectCollection getContainer()

    abstract T getA()

    abstract T getB()

    abstract T getC()

    abstract  S getD()

    Class getType() {
        return a.class
    }

    @SuppressWarnings("GrUnnecessaryPublicModifier")
    public  Class getOtherType() {
        return d.class as Class
    }

    List iterationOrder(T... elements) {
        return elements
    }

    abstract boolean isExternalProviderAllowed()

    abstract boolean isDirectElementAdditionAllowed()

    abstract boolean isElementRemovalAllowed()

    void containerAllowsElementAddition() {
        assumeTrue("the container doesn't allow direct element addition", isDirectElementAdditionAllowed())
    }

    void containerAllowsElementRemoval() {
        assumeTrue("the container doesn't allow element removal", isElementRemovalAllowed())
    }

    void containerAllowsExternalProviders() {
        assumeTrue("the container doesn't allow external provider to be added", isExternalProviderAllowed())
    }

    void containerSupportsBuildOperations() {
        assumeTrue("the container doesn't support build operations", isSupportsBuildOperations())
    }

    protected void addToContainer(T element) {
        container.add(element)
    }

    Class> getContainerPublicType() {
        return new DslObject(container).publicType.concreteClass
    }

    def setup() {
        // Verify some assumptions
        assert !type.isAssignableFrom(otherType) && !otherType.isAssignableFrom(type)
        assert type.isInstance(a)
        assert otherType.isInstance(d)
    }

    def "can get all domain objects for empty collection"() {
        expect:
        container.isEmpty()
        container.size() == 0
    }

    def "can iterate over empty collection"() {
        expect:
        def iterator = container.iterator()
        !iterator.hasNext()
    }

    def "does not add elements with duplicate name"() {
        containerAllowsElementAddition()

        given:
        container.add(a)

        when:
        container.add(a)

        then:
        toList(container) == [a]

        when:
        container.addAll([a, b])

        then:
        toList(container) == [a, b]
    }

    def "element added using provider is not realized when added"() {
        containerAllowsExternalProviders()
        def provider = Mock(ProviderInternal)

        when:
        container.addLater(provider)

        then:
        _ * provider.type >> type
        0 * provider._

        and:
        container.size() == 1
        !container.empty
    }

    def "elements added using provider of iterable are not realized when added"() {
        containerAllowsExternalProviders()
        def provider = Mock(CollectionProviderInternal)
        _ * provider.size() >> 2

        when:
        container.addAllLater(provider)

        then:
        _ * provider.elementType >> type
        0 * provider._

        and:
        container.size() == 2
        !container.empty
    }

    def "provider of elements are not queried when another element added"() {
        containerAllowsExternalProviders()
        def provider = Mock(ProviderInternal)
        def providerOfIterable = Mock(CollectionProviderInternal)
        _ * providerOfIterable.size() >> 2

        given:
        addToContainer(a)
        container.addLater(provider)
        container.addAllLater(providerOfIterable)

        when:
        addToContainer(b)

        then:
        0 * provider._
        0 * providerOfIterable._

        and:
        container.size() == 5
        !container.empty
    }

    def "can check for membership"() {
        given:
        addToContainer(b)
        addToContainer(a)

        expect:
        !container.contains(c)
        container.contains(a)
    }

    def "provider for element is queried when membership checked"() {
        containerAllowsExternalProviders()
        def provider = Mock(ProviderInternal)

        given:
        addToContainer(b)
        container.addLater(provider)

        when:
        def result = container.contains(c)

        then:
        !result

        and:
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of(a)
        0 * _
    }

    def "provider for iterable of elements is queried when membership checked"() {
        containerAllowsExternalProviders()
        def provider = Mock(CollectionProviderInternal)

        given:
        addToContainer(b)
        container.addAllLater(provider)

        when:
        def result = container.contains(c)

        then:
        !result

        and:
        _ * provider.size() >> 2
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of([a, d])
        0 * _
    }

    def "can get all domain objects ordered by order added"() {
        given:
        addToContainer(b)
        addToContainer(a)
        addToContainer(c)

        expect:
        toList(container) == iterationOrder(b, a, c)
    }

    def "can iterate over domain objects ordered by order added"() {
        addToContainer(b)
        addToContainer(a)
        addToContainer(c)

        expect:
        def seen = []
        def iterator = container.iterator()
        seen << iterator.next()
        seen << iterator.next()
        seen << iterator.next()
        !iterator.hasNext()
        seen == iterationOrder(b, a, c)
    }

    def "provider for element is queried when elements iterated and insertion order is retained"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        addToContainer(b)
        container.addLater(provider1)
        container.addLater(provider2)
        addToContainer(c)

        when:
        def result = toList(container)

        then:
        result == iterationOrder(b, a, d, c)

        and:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        0 * _
    }

    def "provider for iterable of elements is queried when elements iterated and insertion order is retained"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(CollectionProviderInternal)

        given:
        addToContainer(b)
        container.addAllLater(provider1)
        addToContainer(c)

        when:
        def result = toList(container)

        then:
        result == iterationOrder(b, a, d, c)

        and:
        _ * provider1.size() >> 2
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([a, d])
        0 * _
    }

    def "can execute action for all elements in a collection"() {
        def action = Mock(Action)

        addToContainer(c)
        addToContainer(d)

        when:
        container.all(action)

        then:
        1 * action.execute(c)
        1 * action.execute(d)
        0 * action._

        when:
        addToContainer(a)

        then:
        1 * action.execute(a)
        0 * action._
    }

    def "can add action to execute only when object added"() {
        def action = Mock(Action)

        addToContainer(c)

        when:
        container.whenObjectAdded(action)

        then:
        // Does not fire for existing elements
        0 * action._

        when:
        addToContainer(a)

        then:
        1 * action.execute(a)
        0 * action._

        when:
        addToContainer(a)

        then:
        0 * action._

        when:
        containerAllowsElementAddition()
        containerAllowsElementRemoval()

        and:
        container.remove(c)
        container.addAll(a, b, c, d)

        then:
        1 * action.execute(b)
        1 * action.execute(c)
        1 * action.execute(d)
        0 * action._
    }

    def "can add action to execute only when object removed"() {
        containerAllowsElementRemoval()

        def action = Mock(Action)

        addToContainer(c)

        when:
        container.whenObjectRemoved(action)

        then:
        // Does not fire for existing elements
        0 * action._

        when:
        addToContainer(a)
        container.remove(c)
        container.remove(a)

        then:
        1 * action.execute(c)
        1 * action.execute(a)
        0 * action._

        when:
        container.remove(d)

        then:
        // Not fired when unknown object removed
        0 * action._
    }

    def "queries provider for element when registering action for all elements in a collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        container.addLater(provider1)

        when:
        container.all(action)

        then:
        1 * action.execute(c)
        _ * provider1.type >> type
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(c)
        0 * _

        when:
        container.addLater(provider2)

        then:
        1 * action.execute(a)
        _ * provider2.type >> type
        1 * provider2.get() >> a
        0 * _
    }

    def "queries provider for iterable of elements when registering action for all elements in a collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        container.addAllLater(provider1)

        when:
        container.all(action)

        then:
        1 * action.execute(c)
        _ * provider1.elementType >> type
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([c])
        _ * provider1.size() >> 1
        0 * _

        when:
        container.addAllLater(provider2)

        then:
        1 * action.execute(a)
        1 * action.execute(b)
        _ * provider2.elementType >> type
        1 * provider2.get() >> [a, b]
        0 * _
    }

    def "can get filtered collection containing all objects which have type"() {
        addToContainer(c)
        addToContainer(a)
        addToContainer(d)

        expect:
        toList(container.withType(type)) == iterationOrder(c, a)
        toList(container.withType(otherType)) == iterationOrder(d)
    }

    def "provider for element is queried when filtered collection with matching type created"() {
        containerAllowsExternalProviders()
        def provider = Mock(ProviderInternal)

        addToContainer(c)
        container.addLater(provider)
        addToContainer(d)

        when:
        def filtered = container.withType(type)

        then:
        0 * provider._

        when:
        def result = toList(filtered)

        then:
        result == iterationOrder(c, a)
        _ * provider.type >> type
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of(a)
        0 * _

        when:
        def result2 = toList(container)

        then:
        result2 == iterationOrder(c, a, d)
        0 * _
    }

    def "provider for iterable of elements is queried when filtered collection with matching type created"() {
        containerAllowsExternalProviders()
        def provider = Mock(CollectionProviderInternal)
        _ * provider.elementType >> type

        addToContainer(c)
        container.addAllLater(provider)
        addToContainer(d)

        when:
        def filtered = container.withType(type)

        then:
        0 * provider._

        when:
        def result = toList(filtered)

        then:
        result == iterationOrder(c, a, b)
        _ * provider.size() >> 2
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of([a, b])
        0 * _

        when:
        def result2 = toList(container)

        then:
        result2 == iterationOrder(c, a, b, d)
        0 * _
    }

    def "provider for element is not queried when filtered collection with non matching type created"() {
        containerAllowsExternalProviders()
        def provider = Mock(ProviderInternal)
        _ * provider.type >> type

        addToContainer(c)
        container.addLater(provider)
        addToContainer(d)

        when:
        def filtered = container.withType(otherType)

        then:
        0 * _

        when:
        def result = toList(filtered)

        then:
        result == iterationOrder(d)
        0 * _

        when:
        def result2 = toList(container)

        then:
        result2 == iterationOrder(c, a, d)
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of(a)
        0 * _
    }

    def "provider for iterable of elements is not queried when filtered collection with non matching type created"() {
        containerAllowsExternalProviders()
        def provider = Mock(CollectionProviderInternal)
        _ * provider.elementType >> type

        addToContainer(c)
        container.addAllLater(provider)
        addToContainer(d)

        when:
        def filtered = container.withType(otherType)

        then:
        0 * provider._

        when:
        def result = toList(filtered)

        then:
        result == iterationOrder(d)
        _ * provider.size() >> 2
        0 * provider._

        when:
        def result2 = toList(container)

        then:
        result2 == iterationOrder(c, a, b, d)
        _ * provider.size() >> 2
        1 * provider.calculateValue(_) >> ValueSupplier.Value.of([a, b])
        0 * _
    }

    def "can execute action for all elements in a type filtered collection"() {
        def action = Mock(Action)

        addToContainer(c)
        addToContainer(d)

        when:
        container.withType(type, action)

        then:
        1 * action.execute(c)
        0 * action._

        when:
        addToContainer(a)

        then:
        1 * action.execute(a)
        0 * action._
    }

    def "can execute closure for all elements in a type filtered collection"() {
        def seen = []
        def closure = { seen << it }

        addToContainer(c)
        addToContainer(d)

        when:
        container.withType(type, closure)

        then:
        seen == [c]

        when:
        addToContainer(a)

        then:
        seen == [c, a]
    }

    def "action for all elements in a type filtered collection can add more elements"() {
        containerAllowsElementAddition()

        def action = Mock(Action)

        container.add(c)

        when:
        container.withType(type, action)

        then:
        1 * action.execute(c) >> {
            container.add(d)
            container.add(a)
        }
        1 * action.execute(a)
        0 * action._
    }

    def "provider for element is queried and action executed for filtered collection with matching type"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        container.addLater(provider1)
        addToContainer(d)

        when:
        container.withType(type, action)

        then:
        _ * provider1.type >> type
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(c)
        1 * action.execute(c)
        0 * _

        when:
        container.addLater(provider2)

        then:
        _ * provider2.type >> type
        1 * provider2.get() >> a
        1 * action.execute(a)
        0 * _
    }

    def "provider for iterable of elements is queried and action executed for filtered collection with matching type"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        container.addAllLater(provider1)
        addToContainer(d)

        when:
        container.withType(type, action)

        then:
        _ * provider1.elementType >> type
        _ * provider1.size() >> 1
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([c])
        1 * action.execute(c)
        0 * _

        when:
        container.addAllLater(provider2)

        then:
        _ * provider2.elementType >> type
        1 * provider2.get() >> [a, b]
        1 * action.execute(a)
        1 * action.execute(b)
        0 * _
    }

    def "provider for element is not queried and action executed for filtered collection with non matching type"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        _ * provider1.type >> type

        container.addLater(provider1)
        addToContainer(d)

        when:
        container.withType(otherType, action)

        then:
        1 * action.execute(d)
        0 * _

        when:
        container.addLater(provider2)

        then:
        _ * provider2.type >> type
        0 * _
    }

    def "provider for iterable of elements is not queried and action executed for filtered collection with non matching type"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)
        _ * provider1.elementType >> type

        container.addAllLater(provider1)
        addToContainer(d)

        when:
        container.withType(otherType, action)

        then:
        1 * action.execute(d)
        _ * provider1.size() >> 1
        0 * _

        when:
        container.addAllLater(provider2)

        then:
        _ * provider2.elementType >> type
        0 * _
    }

    def "can execute action to configure element when element is realized"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def provider3 = Mock(CollectionProviderInternal)

        given:
        container.addLater(provider1)

        when:
        container.configureEach(action)

        then:
        0 * _

        when:
        toList(container)

        then:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * action.execute(a)
        0 * _

        when:
        container.addLater(provider2)

        then:
        _ * provider2.type >> type
        0 * _

        when:
        toList(container)

        then:
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(c)
        1 * action.execute(c)
        0 * _

        when:
        container.addAllLater(provider3)

        then:
        _ * provider3.elementType >> type
        0 * _

        when:
        toList(container)

        then:
        _ * provider3.size() >> 2
        1 * provider3.calculateValue(_) >> ValueSupplier.Value.of([b, d])
        1 * action.execute(b)
        1 * action.execute(d)
        0 * _
    }

    def "runs configure element action immediately when element already realized"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider = Mock(ProviderInternal)
        def providerOfIterable = Mock(CollectionProviderInternal)

        given:
        _ * provider.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * providerOfIterable.calculateValue(_) >> ValueSupplier.Value.of([b, c])
        container.addLater(provider)
        container.addAllLater(providerOfIterable)
        toList(container)

        when:
        container.configureEach(action)

        then:
        _ * providerOfIterable.size() >> 2
        1 * action.execute(a)
        1 * action.execute(b)
        1 * action.execute(c)
        0 * _
    }

    def "runs configure element action immediately when element added directly"() {
        containerAllowsElementAddition()

        def action = Mock(Action)

        given:
        container.configureEach(action)

        when:
        container.add(a)

        then:
        1 * action.execute(a)
        0 * _
    }

    def "can execute action to configure element of given type when element is realized"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        container.addLater(provider1)
        container.addLater(provider2)

        when:
        container.withType(type).configureEach(action)

        then:
        0 * _

        when:
        toList(container)

        then:
        _ * provider1.type >> type
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * action.execute(a)
        _ * provider2.type >> otherType
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        0 * _

        when:
        toList(container)

        then:
        0 * _
    }

    def "can execute action to configure elements of given type when iterable of elements is realized"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        given:
        container.addAllLater(provider1)
        container.addAllLater(provider2)

        when:
        container.withType(type).configureEach(action)

        then:
        _ * provider1.size() >> 2
        _ * provider2.size() >> 1
        0 * _

        when:
        toList(container)

        then:
        _ * provider1.elementType >> type
        _ * provider1.size() >> 2
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([a, c])
        1 * action.execute(a)
        1 * action.execute(c)
        _ * provider2.elementType >> otherType
        _ * provider2.size() >> 1
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of([d])
        0 * _

        when:
        toList(container)

        then:
        0 * _
    }

    def "provider is not queried or element configured until collection is realized when lazy action is registered on type-filtered collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)
        def filtered = container.withType(type)

        when:
        filtered.configureEach(action)

        then:
        0 * _

        when:
        def result = toList(filtered)

        then:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        and:
        result == iterationOrder(a, b)

        and:
        1 * action.execute(a)
        1 * action.execute(b)
    }

    def "provider of iterable is not queried or elements configured until collection is realized when lazy action is registered on type-filtered collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        given:
        _ * provider1.elementType >> type
        _ * provider2.elementType >> type
        container.addAllLater(provider1)
        container.addAllLater(provider2)
        def filtered = container.withType(type)

        when:
        filtered.configureEach(action)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        0 * _

        when:
        def result = toList(filtered)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([a])
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of([c, b])
        0 * _

        and:
        result == iterationOrder(a, c, b)

        and:
        1 * action.execute(a)
        1 * action.execute(c)
        1 * action.execute(b)
    }

    def "only realized elements of given type are configured when lazy action is registered on type-filtered collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> otherType
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)
        def filtered = container.withType(type)

        when:
        filtered.configureEach(action)

        then:
        0 * _

        when:
        def result = toList(filtered)

        then:
        0 * provider1.get() >> a
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        and:
        result == iterationOrder(b)

        and:
        0 * action.execute(a)
        1 * action.execute(b)
    }

    def "only realized elements of iterable with given type are configured when lazy action is registered on type-filtered collection"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        given:
        _ * provider1.elementType >> otherType
        _ * provider2.type >> type
        container.addAllLater(provider1)
        container.addAllLater(provider2)
        def filtered = container.withType(type)

        when:
        filtered.configureEach(action)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        0 * _

        when:
        def result = toList(filtered)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        0 * provider1.get()
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of([b, c])
        0 * _

        and:
        result == iterationOrder(b, c)

        and:
        0 * action.execute(a)
        1 * action.execute(b)
        1 * action.execute(c)
    }

    def "provider is queried but element not configured when lazy action is registered on non-matching filter"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)
        def filtered = container.matching { it == b }

        when:
        filtered.configureEach(action)

        then:
        0 * _

        when:
        def result = toList(filtered)

        then:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        and:
        result == iterationOrder(b)

        and:
        0 * action.execute(a)
        1 * action.execute(b)
    }

    def "can remove external providers without realizing them"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)

        when:
        def didRemoved = container.remove(provider1)

        then:
        didRemoved

        and:
        0 * _

        when:
        def result = toList(container)

        then:
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        and:
        result == iterationOrder(b)
    }

    def "returns false when removing external providers a second time"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider1.present >> true
        container.addLater(provider1)

        when:
        def didRemovedFirstTime = container.remove(provider1)

        then:
        didRemovedFirstTime
        toList(container) == []

        and:
        _ * provider1.type >> type
        0 * _

        when:
        def didRemovedSecondTime = container.remove(provider1)

        then:
        !didRemovedSecondTime
        toList(container) == []

        and:
        _ * provider1.type >> type
        1 * provider1.present >> false
        0 * _
    }

    def "can remove realized external providers without realizing more providers"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def provider3 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider1.present >> true
        _ * provider2.type >> type
        _ * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        _ * provider2.present >> true
        _ * provider3.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)
        container.addLater(provider3)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        def didRemoved1 = container.remove(provider1)

        then:
        didRemoved1

        and:
        _ * provider1.type >> type
        1 * provider1.present >> true
        1 * provider1.get() >> a
        0 * _

        when:
        def didRemoved2 = container.remove(provider2)

        then:
        didRemoved2

        and:
        _ * provider2.type >> type
        1 * provider2.present >> true
        1 * provider2.get() >> b
        0 * _
    }

    def "can remove realized external elements via instance"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider2.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)

        // Realize all object of type `type`
        def element = container.withType(type).iterator().next()

        when:
        def didRemoved = container.remove(element)

        then:
        didRemoved

        and:
        0 * _
    }

    def "will execute remove action when removing external provider only for realized elements"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def action = Mock(Action)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider1.get() >> a
        _ * provider1.present >> true
        _ * provider2.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)
        container.whenObjectRemoved(action)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        def didRemoved1 = container.remove(provider1)

        then:
        didRemoved1

        and:
        _ * provider1.type >> type
        1 * provider1.present >> true
        1 * provider1.get() >> a

        and:
        1 * action.execute(a)
        0 * _

        when:
        def didRemoved2 = container.remove(provider2)

        then:
        didRemoved2

        and:
        0 * _
    }

    def "will execute remove action when clearing the container only for realized external providers"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def action = Mock(Action)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider2.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)
        container.whenObjectRemoved(action)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        container.clear()

        then:
        1 * action.execute(a)
        0 * _

        when:
        def result = toList(container)

        then:
        result == iterationOrder()
    }

    def "will not query external provider when clearing"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)

        when:
        container.clear()

        then:
        0 * _

        when:
        def result = toList(container)

        then:
        0 * _

        and:
        result == iterationOrder()
    }

    def "will execute remove action when not retaining external providers for all elements"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def action = Mock(Action)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider2.type >> otherType
        _ * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        container.addLater(provider1)
        container.addLater(provider2)
        container.whenObjectRemoved(action)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        def didRetained = container.retainAll([])

        then:
        didRetained

        and:
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        1 * action.execute(a)
        1 * action.execute(d)
        0 * _

        when:
        def result = toList(container)

        then:
        result == iterationOrder()

        and:
        0 * _
    }

    def "will query external providers when not retaining them"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> type
        container.addLater(provider1)
        container.addLater(provider2)

        when:
        def didRetained = container.retainAll([b])

        then:
        didRetained

        and:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        when:
        def result = toList(container)

        then:
        0 * _

        and:
        result == iterationOrder(b)
    }

    def "will query retaining provider when retaining realized external provider"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider2.type >> otherType
        _ * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        container.addLater(provider1)
        container.addLater(provider2)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        def didRetained = container.retainAll([a])

        then:
        didRetained

        and:
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(d)
        0 * _

        when:
        def result = toList(container)

        then:
        0 * _

        and:
        result == iterationOrder(a)
    }

    def "provider of iterable is queried but elements not configured when lazy action is registered on non-matching filter"() {
        containerAllowsExternalProviders()
        def action = Mock(Action)
        def provider1 = Mock(CollectionProviderInternal)
        def provider2 = Mock(CollectionProviderInternal)

        given:
        _ * provider1.elementType >> type
        _ * provider2.elementType >> type
        container.addAllLater(provider1)
        container.addAllLater(provider2)
        def filtered = container.matching { it == b }

        when:
        filtered.configureEach(action)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        0 * _

        when:
        def result = toList(filtered)

        then:
        _ * provider1.size() >> 1
        _ * provider2.size() >> 2
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of([a])
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of([b, c])
        0 * _

        and:
        result == iterationOrder(b)

        and:
        0 * action.execute(a)
        0 * action.execute(c)
        1 * action.execute(b)
    }

    def "will realize all external provider when querying the iterator"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)

        given:
        _ * provider1.type >> type
        _ * provider2.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)

        when:
        container.withType(type).iterator()

        then:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        0 * _

        when:
        def result = toList(container)

        then:
        0 * provider1.get()
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        0 * _

        and:
        result == iterationOrder(a, b)
    }

    def "will execute remove action when removing realized external provider using iterator"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def action = Mock(Action)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider2.type >> type
        _ * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        container.addLater(provider1)
        container.addLater(provider2)
        container.whenObjectRemoved(action)

        when:
        def iterator = container.iterator()
        iterator.next()
        iterator.remove()

        then:
        def result = toList(container)
        result == iterationOrder(b)

        and:
        1 * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        1 * provider2.calculateValue(_) >> ValueSupplier.Value.of(b)
        1 * action.execute(a)
        0 * _
    }

    def "will execute remove action when removing a collection of external provider only for realized elements"() {
        containerAllowsExternalProviders()
        def provider1 = Mock(ProviderInternal)
        def provider2 = Mock(ProviderInternal)
        def action = Mock(Action)

        given:
        _ * provider1.type >> type
        _ * provider1.calculateValue(_) >> ValueSupplier.Value.of(a)
        _ * provider1.get() >> a
        _ * provider1.present >> true
        _ * provider2.type >> otherType
        container.addLater(provider1)
        container.addLater(provider2)
        container.whenObjectRemoved(action)

        // Realize all object of type `type`
        toList(container.withType(type))

        when:
        def didRemoved = container.removeAll([provider1, provider2])

        then:
        didRemoved
        container.empty

        and:
        _ * provider1.type >> type
        1 * provider1.present >> true
        1 * provider1.get() >> a
        1 * action.execute(a)
        0 * _
    }

    void setupContainerDefaults() {}

    @Unroll
    def "disallow mutating from common methods when #mutatingMethods.key"() {
        setupContainerDefaults()
        addToContainer(a)
        String methodUnderTest = mutatingMethods.key
        Closure method = bind(mutatingMethods.value)

        when:
        container.configureEach(method)
        then:
        def ex = thrown(Throwable)
        assertDoesNotAllowMethod(ex, methodUnderTest)

        when:
        container.withType(container.type).configureEach(method)
        then:
        ex = thrown(Throwable)
        assertDoesNotAllowMethod(ex, methodUnderTest)

        when:
        container.matching({ it in container.type }).configureEach(method)
        then:
        ex = thrown(Throwable)
        assertDoesNotAllowMethod(ex, methodUnderTest)

        where:
        mutatingMethods << getMutatingMethods()
    }

    protected Map getMutatingMethods() {
        // TODO:
        // "addLater(Provider)"    | { it.addLater(Providers.of(b)) }
        // "addAllLater(Provider)" | { it.addAllLater(Providers.collectionOf(b)) }
        def methods = [:]

        if (isDirectElementAdditionAllowed()) {
            methods += [
                "add(T)": { container.add(b) },
                "addAll(Collection)": { container.addAll([b]) }
            ]
        }

        if (isElementRemovalAllowed()) {
            methods += [
                "clear()": { container.clear() },
                "remove(Object)": { container.remove(b) },
                "removeAll(Collection)": { container.removeAll([b]) },
                "retainAll(Collection)": { container.retainAll([b]) },
                "iterator().remove()": { def iter = container.iterator(); iter.next(); iter.remove() },
                "configureEach(Action)": { container.configureEach(Actions.doNothing()) }
            ]
        }

        return methods + [
            "whenObjectAdded(Action)": { container.whenObjectAdded(Actions.doNothing()) },
            "withType(Class, Action)": { container.withType(type, Actions.doNothing()) },
            "all(Action)": { container.all(Actions.doNothing()) },
        ]
    }

    protected void assertDoesNotAllowMethod(Throwable exception, String methodUnderTest) {
        String message = "${containerPublicType.simpleName}#${methodUnderTest} on ${container.toString()} cannot be executed in the current context."
        List causes = new ArrayList()
        while (exception != null) {
            causes.add(exception)
            exception = exception.cause
        }
        assertThat(causes, CoreMatchers.hasItem(hasMessage(startsWith(message))))
    }

    @Unroll
    def "allow common querying methods when #queryMethods.key"() {
        setupContainerDefaults()
        addToContainer(a)
        Closure method = bind(queryMethods.value)

        when:
        container.configureEach(method)
        then:
        noExceptionThrown()

        when:
        container.withType(container.type).configureEach(method)
        then:
        noExceptionThrown()

        when:
        container.matching({ it in container.type }).configureEach(method)
        then:
        noExceptionThrown()

        where:
        queryMethods << getQueryMethods()
    }

    @Unroll
    def "allow common querying and mutating methods when #methods.key"() {
        setupContainerDefaults()
        addToContainer(a)
        Closure method = bind(methods.value)

        when:
        container.all(noReentry(method))
        then:
        noExceptionThrown()

        where:
        methods << getQueryMethods() + getMutatingMethods()
    }

    @Unroll
    def "allow common querying and mutating methods when #methods.key on filtered container by type"() {
        setupContainerDefaults()
        addToContainer(a)
        Closure method = bind(methods.value)

        when:
        container.withType(container.type).all(noReentry(method))
        then:
        noExceptionThrown()

        where:
        methods << getQueryMethods() + getMutatingMethods()
    }

    @Unroll
    def "allow common querying and mutating methods when #methods.key on filtered container by spec"() {
        setupContainerDefaults()
        addToContainer(a)
        Closure method = bind(methods.value)

        when:
        container.matching({ it in container.type }).all(noReentry(method))
        then:
        noExceptionThrown()

        where:
        methods << getQueryMethods() + getMutatingMethods()
    }

    def "fires build operation when emitting added callback and reestablishes user code context"() {
        given:
        containerSupportsBuildOperations()

        UserCodeApplicationId id1 = null
        userCodeApplicationContext.apply(Stub(DisplayName)) {
            id1 = it
            container.whenObjectAdded {
                assert userCodeApplicationContext.current().id == id1
            }
        }

        when:
        addToContainer(a)

        then:
        def callbacks1 = buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType)
        callbacks1.size() == 1
        callbacks1.first().details.applicationId == id1.longValue()

        when:
        UserCodeApplicationId id2 = null
        userCodeApplicationContext.apply(Stub(DisplayName)) {
            id2 = it
            container.whenObjectAdded {
                assert userCodeApplicationContext.current().id == id2
            }
        }

        and:
        addToContainer(b)

        then:
        def callbacks = buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType)
        callbacks.size() == 3
        callbacks[1].details.applicationId == id1.longValue()
        callbacks[2].details.applicationId == id2.longValue()
    }

    def "does not fire build operation if callback is filtered out by type"() {
        given:
        containerSupportsBuildOperations()

        userCodeApplicationContext.apply(Stub(DisplayName)) {
            container.withType(otherType).whenObjectAdded {
                throw new IllegalStateException()
            }
        }

        when:
        addToContainer(a)

        then:
        buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType).empty
    }

    def "does not fire build operation if callback is filtered out by condition"() {
        given:
        containerSupportsBuildOperations()

        userCodeApplicationContext.apply(Stub(DisplayName)) {
            container.matching { !it.is(a) }.whenObjectAdded {
                throw new IllegalStateException()
            }
        }

        when:
        addToContainer(a)

        then:
        buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType).empty
    }

    def "fires build operation for existing elements"() {
        given:
        containerSupportsBuildOperations()

        addToContainer(a)
        addToContainer(b)

        when:
        UserCodeApplicationId id = null
        List ids = []
        userCodeApplicationContext.apply(Stub(DisplayName)) {
            id = it
            container.matching { !it.is(a) }.all {
                ids << userCodeApplicationContext.current().id
            }
        }

        then:
        ids.size() == 1
        ids.first() == id
        def ops = buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType)
        ops.size() == 1
        ops.first().details.applicationId == id.longValue()
    }

    def "does not fire op if no user code application id"() {
        given:
        containerSupportsBuildOperations()

        when:
        def ids = []
        container.all {
            ids << userCodeApplicationContext.current()
        }
        addToContainer(a)

        then:
        ids.size() == 1
        ids.first() == null
        buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType).empty
    }

    def "handles nested listener registration"() {
        given:
        containerSupportsBuildOperations()

        when:
        UserCodeApplicationId id1 = null
        UserCodeApplicationId id2 = null
        List ids = []
        userCodeApplicationContext.apply(Stub(DisplayName)) {
            id1 = it
            container.all {
                ids << userCodeApplicationContext.current()
                if (it.is(a)) {
                    userCodeApplicationContext.apply(Stub(DisplayName)) {
                        id2 = it
                        container.all {
                            ids << userCodeApplicationContext.current()
                        }
                    }
                }
            }
        }
        addToContainer(a)
        addToContainer(b)

        then:
        def ops = buildOperationExecutor.log.all(ExecuteDomainObjectCollectionCallbackBuildOperationType)
        ops.size() == 4
        ops[0].details.applicationId == id1.longValue()
        ops[1].details.applicationId == id2.longValue()
        ops[2].details.applicationId == id1.longValue()
        ops[3].details.applicationId == id2.longValue()
    }

    def "can add list properties to container"() {
        containerAllowsExternalProviders()

        given:
        def property = TestUtil.objectFactory().listProperty(type)

        when:
        container.addAllLater(property)
        property.add(a)
        property.addAll([b, c])

        then:
        toList(container) == [a, b, c]
    }


    def "can add set properties to container"() {
        containerAllowsExternalProviders()

        given:
        def property = TestUtil.objectFactory().setProperty(type)

        when:
        container.addAllLater(property)
        property.add(a)
        property.addAll([b, c])

        then:
        toList(container) == [a, b, c]
    }

    protected Map getQueryMethods() {
        return [
            "contains(Object)": { container.contains(b) },
            "iterator().next()": { def iter = container.iterator(); iter.next() },
        ]
    }

    protected Closure bind(Closure delegateClosure) {
        def thiz = this
        return {
            ConfigureUtil.configureSelf(delegateClosure, it, new ConfigureDelegate(delegateClosure, thiz))
        }
    }

    protected Closure noReentry(Closure delegateClosure) {
        boolean entryAllowed = true
        return {
            if (entryAllowed) {
                boolean oldEntryAllowed = entryAllowed
                entryAllowed = false
                try {
                    ConfigureUtil.configure(delegateClosure, it)
                } finally {
                    entryAllowed = oldEntryAllowed
                }
            }
        }
    }
}