This is an automated email from the ASF dual-hosted git repository.

iuliana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 4605c16  be smarter about picking which component is referenced by DSL
     new 8e5b5bf  Merge pull request #1253 from ahgittin/dsl-id-search-smarter
4605c16 is described below

commit 4605c160292bc7ca0c6cd5f5e36c8e015903c663
Author: Alex Heneveld <[email protected]>
AuthorDate: Wed Sep 8 16:35:09 2021 +0100

    be smarter about picking which component is referenced by DSL
    
    prefer those:
    - crossing fewer application boundaries
    - closer
---
 .../camp/brooklyn/spi/dsl/AppGroupTraverser.java   | 139 +++++++++++++++++++
 .../spi/dsl/methods/BrooklynDslCommon.java         |   5 +-
 .../brooklyn/spi/dsl/methods/DslComponent.java     |  81 ++++++-----
 .../brooklyn/spi/dsl/AppGroupTraverserTest.java    | 154 +++++++++++++++++++++
 .../camp/brooklyn/spi/dsl/DslYamlTest.java         |   2 +-
 5 files changed, 337 insertions(+), 44 deletions(-)

diff --git 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverser.java
 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverser.java
new file mode 100644
index 0000000..6f1bdeb
--- /dev/null
+++ 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverser.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.brooklyn.camp.brooklyn.spi.dsl;
+
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.util.collections.MutableSet;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+
+// progressively look up and down to application (topology) boundaries
+
+// ie first look subject to application boundaries, ie up to ancestor which is 
an application, and through descendants which are applications
+// then look past all of those to next app boundary
+
+// TODO would be nice to have tests which look at parent, children, 
grandchildren, older and younger nephews
+
+// TODO ideally would be enhanced to look at the "depth in ancestor" tag, to 
work based on entity definition boundaries rather than app boundaries
+
+/**
+ *
+ * Progressively expands groups based on "application" boundaries, returning 
each distal group.
+ * Useful e.g. to find nodes matching an ID included in a particular 
application definition and not children.
+ <code>
+ - APP1
+   - ENT1
+   - APP2
+     - ENT2
+     - ENT3
+   - APP3
+     - ENT4
+       - ENT5
+       - APP4
+          - ENT6
+       - ENT7
+     - APP5
+       - ENT8
+ </code>
+
+ if this is initialized with ENT4, it will start with that in {@link 
#visitedThisTime} and {@link #visited}.
+ one invocation of {@link #next()} will return an instance where {@link 
#visitedThisTime} is {APP3,ENT5,APP4,ENT7,APP5}, and {@link #visited} contains 
that and ENT4;
+ it will not go below APP4 or APP5 because those are {@link Application} 
boundaries, nor will it go above APP3, on that pass.
+ an invocation of {@link #next()} on that instance will then return {@link 
#visitedThisTime} containing {APP1,ENT1,APP2,ENT6,ENT8},
+ i.e. the items above APP3 (but not above the next highest ancestor 
implementing {@link Application},
+ and its {@link #visited} will (as it always does) contain those items plus 
the items previously visited.
+ an invocation of {@link #next()} on that instance will then return {@link 
#visitedThisTime} empty.
+ <p>
+ see {@link #findFirstGroupOfMatches(Entity, Predicate)}.
+*/
+public class AppGroupTraverser {
+
+    int depth = -1;
+    Set<Entity> visited = MutableSet.of();
+    Set<Entity> visitedThisTime = MutableSet.of();
+    // _parent_ of the last node we have visited;
+    // after first iteration this should have at least (exactly?) one visited 
child which is a topology template (not a node)
+    Entity ancestorBound = null;
+    // children whom we have not yet visited, because they are part of a new 
topology template
+    Set<Entity> descendantBounds = MutableSet.of();
+
+    protected AppGroupTraverser() {
+    }
+
+    AppGroupTraverser(Entity source) {
+        this.visitedThisTime.add(source);
+        this.visited.add(source);
+        this.ancestorBound = source.getParent();
+        this.descendantBounds.addAll(source.getChildren());
+    }
+
+    AppGroupTraverser next() {
+        AppGroupTraverser result = new AppGroupTraverser();
+        result.visited.addAll(visited);
+        result.depth = depth + 1;
+        descendantBounds.forEach(c -> result.visitDescendants(c, true));
+        if (ancestorBound != null) 
result.visitAncestorsAndTheirDescendants(ancestorBound);
+        return result;
+    }
+
+    protected void visitAncestorsAndTheirDescendants(Entity ancestor) {
+        // go to the top of the containing topology template / application 
boundary
+        Entity appAncestor = ancestor;
+        while (!(appAncestor instanceof Application) && 
appAncestor.getParent() != null)
+            appAncestor = appAncestor.getParent();
+        visitDescendants(appAncestor, true);
+        ancestorBound = appAncestor.getParent() != null ? 
appAncestor.getParent() : null;
+    }
+
+    protected void visitDescendants(Entity node, boolean isFirst) {
+        if (!isFirst && !visited.add(node)) {
+            // already visited
+            return;
+        }
+        // normal entity
+        visitedThisTime.add(node);
+
+        if (!isFirst && node instanceof Application) {
+            descendantBounds.add(node);
+        } else {
+            node.getChildren().forEach(c -> this.visitDescendants(c, false));
+        }
+    }
+
+    boolean hasNext() {
+        return ancestorBound != null || !descendantBounds.isEmpty();
+    }
+
+    AppGroupTraverser expandUntilMatchesFound(Predicate<Entity> test) {
+        if (visitedThisTime.stream().anyMatch(test) || 
visitedThisTime.isEmpty()) return this;
+        return next().expandUntilMatchesFound(test);
+    }
+
+    /** Progressively expands across {@link 
org.apache.brooklyn.api.entity.Application} boundaries until one or more 
matching entities are found. */
+    public static List<Entity> findFirstGroupOfMatches(Entity source, 
Predicate<Entity> test) {
+        AppGroupTraverser traversed = new 
AppGroupTraverser(source).expandUntilMatchesFound(test);
+        return 
traversed.visitedThisTime.stream().filter(test).collect(Collectors.toList());
+    }
+
+}
diff --git 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
index 91b85a1..7e22521 100644
--- 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
+++ 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
@@ -138,11 +138,14 @@ public class BrooklynDslCommon {
     public static DslComponent root() {
         return new DslComponent(Scope.ROOT);
     }
+
     @DslAccessible
     public static DslComponent scopeRoot() {
         return new DslComponent(Scope.SCOPE_ROOT);
     }
-    // prefer the syntax above to the below now, but not deprecating the below
+    // above is within the current definition
+    // whereas the below _prefers_ in the current definition
+
     @DslAccessible
     public static DslComponent component(Object id) {
         return component("global", id);
diff --git 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java
 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java
index 01c68e1..f9e3b5b 100644
--- 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java
+++ 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java
@@ -18,18 +18,26 @@
  */
 package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import java.util.Set;
-import java.util.function.Function;
-import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
-
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Converter;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Callables;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
-
+import java.util.function.Function;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.mgmt.ExecutionContext;
@@ -38,9 +46,11 @@ import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.api.sensor.Sensor;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.AppGroupTraverser;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslFunctionSource;
+import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.Entities;
@@ -48,7 +58,6 @@ import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.EntityPredicates;
 import org.apache.brooklyn.core.location.Locations;
 import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
-import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal;
 import org.apache.brooklyn.core.sensor.DependentConfiguration;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.util.JavaGroovyEquivalents;
@@ -64,19 +73,6 @@ import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.Strings;
 
-import com.google.common.base.CaseFormat;
-import com.google.common.base.Converter;
-import com.google.common.base.Objects;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.common.util.concurrent.Callables;
-import com.thoughtworks.xstream.annotations.XStreamConverter;
-
 public class DslComponent extends BrooklynDslDeferredSupplier<Entity> 
implements DslFunctionSource {
 
     private static final long serialVersionUID = -7715984495268724954L;
@@ -278,15 +274,15 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                 return 
Maybe.<Entity>ofDisallowingNull(entity()).or(Maybe.<Entity>absent("Context 
entity not available when trying to evaluate Brooklyn DSL"));
             }
         }
-        
+
         protected Maybe<Entity> callImpl(boolean immediate) throws Exception {
             Maybe<Entity> entityMaybe = getEntity(immediate);
             if (immediate && entityMaybe.isAbsent()) {
                 return entityMaybe;
             }
             EntityInternal entity = (EntityInternal) entityMaybe.get();
-            
-            Iterable<Entity> entitiesToSearch = null;
+
+            java.util.function.Predicate<Entity> acceptableEntity = x -> true;
             Predicate<Entity> notSelfPredicate = 
Predicates.not(Predicates.<Entity>equalTo(entity));
 
             switch (scope) {
@@ -297,8 +293,9 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                 case GLOBAL:
                     if (Entities.isManaged(entity())) {
                         // use management context if entity is managed (usual 
case, more efficient)
-                        entitiesToSearch = ((EntityManagerInternal) 
entity.getManagementContext().getEntityManager())
-                                
.getAllEntitiesInApplication(entity().getApplication());
+                        String appId = entity().getApplicationId();
+                        acceptableEntity = ee -> appId!=null && 
appId.equals(ee.getApplicationId());
+
                     } else {
                         // otherwise traverse the application
                         if (entity()!=null && entity().getApplication()!=null) 
{
@@ -312,7 +309,7 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                                     });
                                 });
                             }
-                            entitiesToSearch = visited;
+                            acceptableEntity = visited::contains;
                         } else {
                             // nothing to do
                         }
@@ -323,17 +320,16 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                 case SCOPE_ROOT:
                     return 
Maybe.<Entity>of(Entities.catalogItemScopeRoot(entity));
                 case DESCENDANT:
-                    entitiesToSearch = Entities.descendantsWithoutSelf(entity);
+                    acceptableEntity = 
MutableSet.copyOf(Entities.descendantsWithoutSelf(entity))::contains;
                     break;
                 case ANCESTOR:
-                    entitiesToSearch = Entities.ancestorsWithoutSelf(entity);
+                    acceptableEntity = 
MutableSet.copyOf(Entities.ancestorsWithoutSelf(entity))::contains;
                     break;
                 case SIBLING:
-                    entitiesToSearch = entity.getParent().getChildren();
-                    entitiesToSearch = Iterables.filter(entitiesToSearch, 
notSelfPredicate);
+                    acceptableEntity = 
MutableSet.copyOf(Iterables.filter(entity.getParent().getChildren(), 
notSelfPredicate))::contains;
                     break;
                 case CHILD:
-                    entitiesToSearch = entity.getChildren();
+                    acceptableEntity = 
MutableSet.copyOf(entity.getChildren())::contains;
                     break;
                 default:
                     throw new IllegalStateException("Unexpected scope "+scope);
@@ -359,9 +355,10 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                 }
                 
                 // Support being passed an explicit entity via the DSL
-                if (maybeComponentId.get() instanceof BrooklynObject) {
-                    if (Iterables.contains(entitiesToSearch, 
maybeComponentId.get())) {
-                        return Maybe.of((Entity)maybeComponentId.get());
+                Object candidate = maybeComponentId.get();
+                if (candidate instanceof BrooklynObject) {
+                    if (acceptableEntity.test((Entity)candidate)) {
+                        return Maybe.of((Entity)candidate);
                     } else {
                         throw new IllegalStateException("Resolved component " 
+ maybeComponentId.get() + " is not in scope '" + scope + "' wrt " + entity);
                     }
@@ -376,15 +373,15 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
             } else {
                 desiredComponentId = componentId;
             }
-            
-            Optional<Entity> result = Iterables.tryFind(entitiesToSearch, 
EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, 
desiredComponentId));
-            if (result.isPresent()) {
-                return Maybe.of(result.get());
+
+            List<Entity> firstGroupOfMatches = 
AppGroupTraverser.findFirstGroupOfMatches(entity,
+                    
Predicates.and(EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, 
desiredComponentId), acceptableEntity::test)::apply);
+            if (firstGroupOfMatches.isEmpty()) {
+                firstGroupOfMatches = 
AppGroupTraverser.findFirstGroupOfMatches(entity,
+                        
Predicates.and(EntityPredicates.idEqualTo(desiredComponentId), 
acceptableEntity::test)::apply);
             }
-            
-            result = Iterables.tryFind(entitiesToSearch, 
EntityPredicates.idEqualTo(desiredComponentId));
-            if (result.isPresent()) {
-                return Maybe.of(result.get());
+            if (!firstGroupOfMatches.isEmpty()) {
+                return Maybe.of(firstGroupOfMatches.get(0));
             }
             
             // could be nice if DSL has an extra .block() method to allow it 
to wait for a matching entity.
diff --git 
a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverserTest.java
 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverserTest.java
new file mode 100644
index 0000000..b1d01f4
--- /dev/null
+++ 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/AppGroupTraverserTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.brooklyn.camp.brooklyn.spi.dsl;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest;
+import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
+import org.apache.brooklyn.core.entity.EntityPredicates;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import org.testng.annotations.Test;
+
+public class AppGroupTraverserTest extends AbstractYamlTest {
+
+    @Test
+    public void testComplexTreeTraversal() throws Exception {
+        Application app = createAndStartApplication("\n" +
+                "#- APP1\n" +
+                "#   - ENT1\n" +
+                "#   - APP2\n" +
+                "#     - ENT2\n" +
+                "#     - ENT3\n" +
+                "#   - APP3\n" +
+                "#     - ENT4\n" +
+                "#       - ENT5\n" +
+                "#       - APP4\n" +
+                "#          - ENT6\n" +
+                "#       - ENT7\n" +
+                "#     - APP5\n" +
+                "#       - ENT8\n" +
+                "\n" +
+                "id: APP1\n" +
+                "services:\n" +
+                "  - type: org.apache.brooklyn.entity.stock.BasicEntity\n" +
+                "    id: ENT1\n" +
+                "  - type: 
org.apache.brooklyn.entity.stock.BasicApplication\n" +
+                "    id: APP2\n" +
+                "    brooklyn.children:\n" +
+                "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" 
+
+                "        id: ENT2\n" +
+                "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" 
+
+                "        id: ENT3\n" +
+                "  - type: 
org.apache.brooklyn.entity.stock.BasicApplication\n" +
+                "    id: APP3\n" +
+                "    brooklyn.children:\n" +
+                "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" 
+
+                "        id: ENT4\n" +
+                "        brooklyn.children:\n" +
+                "          - type: 
org.apache.brooklyn.entity.stock.BasicEntity\n" +
+                "            id: ENT5\n" +
+                "          - type: 
org.apache.brooklyn.entity.stock.BasicApplication\n" +
+                "            id: APP4\n" +
+                "            brooklyn.children:\n" +
+                "              - type: 
org.apache.brooklyn.entity.stock.BasicEntity\n" +
+                "                id: ENT6\n" +
+                "          - type: 
org.apache.brooklyn.entity.stock.BasicEntity\n" +
+                "            id: ENT7\n" +
+                "\n" +
+                "      - type: 
org.apache.brooklyn.entity.stock.BasicApplication\n" +
+                "        id: APP5\n" +
+                "        brooklyn.children:\n" +
+                "          - type: 
org.apache.brooklyn.entity.stock.BasicEntity\n" +
+                "            id: ENT8\n");
+
+        String[] IDs = {"ENT1", "ENT2", "ENT3", "ENT4", "ENT5", "ENT6",
+                "ENT7", "ENT8", "APP1", "APP2", "APP3", "APP4",
+                "APP5"};
+
+        Stream.of(IDs).forEach(s -> {
+            Entity entity = findEntity(app, s);
+            assertCanTraverseToEachEntityFromHere(entity, IDs);
+        });
+    }
+
+    static final String APP_findIDISameApp = "id: APP1\n" +
+            "services:\n" +
+            "  - type: org.apache.brooklyn.entity.stock.BasicApplication\n" +
+            "    id: APP2\n" +
+            "    brooklyn.children:\n" +
+            "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" +
+            "        id: ENT1\n" +
+            "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" +
+            "        id: ENT2\n" +
+            "        name: SAME_APP\n" +
+            "  - type: org.apache.brooklyn.entity.stock.BasicApplication\n" +
+            "    id: APP3\n" +
+            "    brooklyn.children:\n" +
+            "      - type: org.apache.brooklyn.entity.stock.BasicEntity\n" +
+            "        id: ENT2\n" +
+            "        name: DIFFERENT_APP\n";
+
+    @Test void testFindIDISameApp__SharedParent() throws Exception {
+        Application app = createAndStartApplication(APP_findIDISameApp);
+
+        Entity ent1 = findEntity(app, "ENT1");
+        Entity ent2 = AppGroupTraverser.findFirstGroupOfMatches(ent1, 
EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, 
"ENT2")::apply).get(0);
+
+        assertEquals(ent2.getDisplayName(), "SAME_APP");
+
+    }
+
+    @Test void testFindIDISameApp__Child() throws Exception {
+        Application app = createAndStartApplication(APP_findIDISameApp);
+
+        Entity ent1 = findEntity(app, "APP2");
+        Entity ent2 = AppGroupTraverser.findFirstGroupOfMatches(ent1, 
EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, 
"ENT2")::apply).get(0);
+
+        assertEquals(ent2.getDisplayName(), "SAME_APP");
+
+    }
+
+    private void assertCanTraverseToEachEntityFromHere(Entity startEntity, 
String[] allNodes) {
+        Set<String> visitedNodes = traverseFullTreeRecordingNodes(startEntity);
+
+        assertEquals(visitedNodes.size(), allNodes.length);
+        Stream.of(allNodes).forEach(s -> assertTrue(visitedNodes.contains(s), 
s + " was not found in traversal"));
+    }
+
+    private Set<String> traverseFullTreeRecordingNodes(Entity startEntity) {
+        Set<String> myIDs = new HashSet<>();
+        AppGroupTraverser.findFirstGroupOfMatches(startEntity, entity -> {
+            myIDs.add(entity.getConfig(BrooklynCampConstants.PLAN_ID));
+            return false; //Traverse all nodes
+        });
+        return myIDs;
+    }
+
+    private Entity findEntity(Application app, String s) {
+        return AppGroupTraverser.findFirstGroupOfMatches(app, entity -> {
+            String campId = entity.getConfig(BrooklynCampConstants.PLAN_ID);
+            return s.equals(campId);
+        }).get(0);
+    }
+}
\ No newline at end of file
diff --git 
a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java
 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java
index d0128f6..f83343c 100644
--- 
a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java
+++ 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java
@@ -689,7 +689,7 @@ public class DslYamlTest extends AbstractYamlTest {
         assertEquals(getConfigEventually(app, DEST), Boolean.TRUE);
     }
 
-    @Test
+    @Test(groups="Integration")  // because takes 3 seconds ... not sure why!?
     public void testDeferredDslObjectAsFirstArgument() throws Exception {
         final Entity app = createAndStartApplication(
                 "services:",

Reply via email to