brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hadr...@apache.org
Subject [04/33] incubator-brooklyn git commit: [BROOKLYN-162] Refactor package in ./core/management
Date Sat, 15 Aug 2015 13:33:06 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/ha/TestEntityFailingRebind.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/ha/TestEntityFailingRebind.java b/core/src/test/java/org/apache/brooklyn/core/management/ha/TestEntityFailingRebind.java
new file mode 100644
index 0000000..974f5d7
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/ha/TestEntityFailingRebind.java
@@ -0,0 +1,55 @@
+/*
+ * 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.core.management.ha;
+
+import org.apache.brooklyn.test.entity.TestApplicationImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TestEntityFailingRebind extends TestApplicationImpl {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(TestEntityFailingRebind.class);
+
+    public static class RebindException extends RuntimeException {
+        private static final long serialVersionUID = 1L;
+
+        public RebindException(String message) {
+            super(message);
+        }
+    }
+    
+    private static boolean throwOnRebind = true;
+    
+    public static void setThrowOnRebind(boolean state) {
+        throwOnRebind = state;
+    }
+    
+    public static boolean getThrowOnRebind() {
+        return throwOnRebind;
+    }
+
+    @Override
+    public void rebind() {
+        if (throwOnRebind) {
+            LOG.warn("Throwing intentional exception to simulate failure of rebinding " + this);
+            throw new RebindException("Intentional exception thrown when rebinding " + this);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/ha/WarmStandbyTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/ha/WarmStandbyTest.java b/core/src/test/java/org/apache/brooklyn/core/management/ha/WarmStandbyTest.java
new file mode 100644
index 0000000..8324c16
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/ha/WarmStandbyTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.core.management.ha;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.management.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.management.ha.ManagementNodeState;
+import org.apache.brooklyn.api.management.ha.ManagementPlaneSyncRecordPersister;
+import org.apache.brooklyn.core.management.ha.HighAvailabilityManagerImpl;
+import org.apache.brooklyn.core.management.ha.ManagementPlaneSyncRecordPersisterToObjectStore;
+import org.apache.brooklyn.core.management.internal.ManagementContextInternal;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
+import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
+import brooklyn.entity.rebind.persister.InMemoryObjectStore;
+import brooklyn.entity.rebind.persister.ListeningObjectStore;
+import brooklyn.entity.rebind.persister.PersistMode;
+import brooklyn.entity.rebind.persister.PersistenceObjectStore;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+@Test
+public class WarmStandbyTest {
+
+    private static final Logger log = LoggerFactory.getLogger(WarmStandbyTest.class);
+    
+    private List<HaMgmtNode> nodes = new MutableList<WarmStandbyTest.HaMgmtNode>();
+    Map<String,String> sharedBackingStore = MutableMap.of();
+    Map<String,Date> sharedBackingStoreDates = MutableMap.of();
+    private ClassLoader classLoader = getClass().getClassLoader();
+    
+    public class HaMgmtNode {
+        // TODO share with HotStandbyTest and SplitBrainTest and a few others (minor differences but worth it ultimately)
+        
+        private ManagementContextInternal mgmt;
+        private String ownNodeId;
+        private String nodeName;
+        private ListeningObjectStore objectStore;
+        private ManagementPlaneSyncRecordPersister persister;
+        private HighAvailabilityManagerImpl ha;
+
+        @BeforeMethod(alwaysRun=true)
+        public void setUp() throws Exception {
+            nodeName = "node "+nodes.size();
+            mgmt = newLocalManagementContext();
+            ownNodeId = mgmt.getManagementNodeId();
+            objectStore = new ListeningObjectStore(newPersistenceObjectStore());
+            objectStore.injectManagementContext(mgmt);
+            objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
+            persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).preferRemoteTimestampInMemento();
+            BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
+            mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
+            ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())
+                .setPollPeriod(Duration.PRACTICALLY_FOREVER)
+                .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
+                .setPersister(persister);
+            log.info("Created "+nodeName+" "+ownNodeId);
+        }
+        
+        public void tearDown() throws Exception {
+            if (ha != null) ha.stop();
+            if (mgmt != null) Entities.destroyAll(mgmt);
+            if (objectStore != null) objectStore.deleteCompletely();
+        }
+        
+        @Override
+        public String toString() {
+            return nodeName+" "+ownNodeId;
+        }
+    }
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        nodes.clear();
+        sharedBackingStore.clear();
+    }
+    
+    public HaMgmtNode newNode() throws Exception {
+        HaMgmtNode node = new HaMgmtNode();
+        node.setUp();
+        nodes.add(node);
+        return node;
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        for (HaMgmtNode n: nodes)
+            n.tearDown();
+    }
+
+    protected ManagementContextInternal newLocalManagementContext() {
+        return new LocalManagementContextForTests();
+    }
+
+    protected PersistenceObjectStore newPersistenceObjectStore() {
+        return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
+    }
+
+    // TODO refactor above -- routines above this line are shared among HotStandbyTest and SplitBrainTest
+    
+    @Test
+    public void testWarmStandby() throws Exception {
+        HaMgmtNode n1 = newNode();
+        n1.ha.start(HighAvailabilityMode.AUTO);
+        assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
+        
+        TestApplication app = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app.start(MutableList.<Location>of());
+        
+        n1.mgmt.getRebindManager().forcePersistNow(false, null);
+
+        HaMgmtNode n2 = newNode();
+        n2.ha.start(HighAvailabilityMode.STANDBY);
+        assertEquals(n2.ha.getNodeState(), ManagementNodeState.STANDBY);
+
+        assertEquals(n2.mgmt.getApplications().size(), 0);
+    }
+    
+    // TODO support forcible demotion, and check that a master forcibly demoted 
+    // to warm standby clears its apps, policies, and locations  
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/internal/AccessManagerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/internal/AccessManagerTest.java b/core/src/test/java/org/apache/brooklyn/core/management/internal/AccessManagerTest.java
new file mode 100644
index 0000000..891c3a4
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/internal/AccessManagerTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.core.management.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.core.management.internal.LocalManagementContext;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.collect.ImmutableSet;
+
+public class AccessManagerTest {
+
+    private LocalManagementContext managementContext;
+    private TestApplication app;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+        app = null;
+    }
+
+    @Test
+    public void testEntityManagementAllowed() throws Exception {
+        // default is allowed
+        TestEntity e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+
+        // when forbidden, should give error trying to create+manage new entity
+        managementContext.getAccessManager().setEntityManagementAllowed(false);
+        try {
+            app.createAndManageChild(EntitySpec.create(TestEntity.class));
+            fail();
+        } catch (Exception e) {
+            // expect it to be forbidden
+            if (Exceptions.getFirstThrowableOfType(e, IllegalStateException.class) == null) {
+                throw e;
+            }
+        }
+
+        // when forbidden, should refuse to create new app
+        try {
+            ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+            fail();
+        } catch (Exception e) {
+            // expect it to be forbidden
+            if (Exceptions.getFirstThrowableOfType(e, IllegalStateException.class) == null) {
+                throw e;
+            }
+        }
+
+        // but when forbidden, still allowed to create locations
+        managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        
+        // when re-enabled, can create entities again
+        managementContext.getAccessManager().setEntityManagementAllowed(true);
+        TestEntity e3 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        assertEquals(ImmutableSet.copyOf(managementContext.getEntityManager().getEntities()), ImmutableSet.of(app, e1, e3));
+    }
+    
+    @Test
+    public void testLocationManagementAllowed() throws Exception {
+        // default is allowed
+        Location loc1 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+
+        // when forbidden, should give error
+        managementContext.getAccessManager().setLocationManagementAllowed(false);
+        try {
+            managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+            fail();
+        } catch (Exception e) {
+            // expect it to be forbidden
+            if (Exceptions.getFirstThrowableOfType(e, IllegalStateException.class) == null) {
+                throw e;
+            }
+        }
+
+        // but when forbidden, still allowed to create entity
+        ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        
+        // when re-enabled, can create entities again
+        managementContext.getAccessManager().setLocationManagementAllowed(true);
+        Location loc3 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        
+        assertEquals(ImmutableSet.copyOf(managementContext.getLocationManager().getLocations()), ImmutableSet.of(loc1, loc3));
+    }
+    
+    @Test
+    public void testLocationProvisioningAllowed() throws Exception {
+        SimulatedLocation loc = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        
+        // default is allowed
+        assertTrue(managementContext.getAccessController().canProvisionLocation(loc).isAllowed());
+
+        // when forbidden, should say so
+        managementContext.getAccessManager().setLocationProvisioningAllowed(false);
+        assertFalse(managementContext.getAccessController().canProvisionLocation(loc).isAllowed());
+
+        // but when forbidden, still allowed to create locations
+        managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        
+        // when re-enabled, can create entities again
+        managementContext.getAccessManager().setLocationProvisioningAllowed(true);
+        assertTrue(managementContext.getAccessController().canProvisionLocation(loc).isAllowed());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/internal/EntityExecutionManagerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/internal/EntityExecutionManagerTest.java b/core/src/test/java/org/apache/brooklyn/core/management/internal/EntityExecutionManagerTest.java
new file mode 100644
index 0000000..7f35bba
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/internal/EntityExecutionManagerTest.java
@@ -0,0 +1,478 @@
+/*
+ * 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.core.management.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.management.ExecutionManager;
+import org.apache.brooklyn.api.management.Task;
+import org.apache.brooklyn.core.management.internal.BrooklynGarbageCollector;
+import org.apache.brooklyn.core.management.internal.LocalManagementContext;
+import org.apache.brooklyn.core.management.internal.ManagementContextInternal;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.BrooklynTaskTags.WrappedEntity;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.repeat.Repeater;
+import brooklyn.util.task.BasicExecutionManager;
+import brooklyn.util.task.ExecutionListener;
+import brooklyn.util.task.TaskBuilder;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Callables;
+
+/** Includes many tests for {@link BrooklynGarbageCollector} */
+public class EntityExecutionManagerTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(EntityExecutionManagerTest.class);
+    
+    private static final Duration TIMEOUT_MS = Duration.TEN_SECONDS;
+    
+    private ManagementContextInternal mgmt;
+    private TestApplication app;
+    private TestEntity e;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+        app = null;
+        if (mgmt != null) Entities.destroyAll(mgmt);
+    }
+
+    @Test
+    public void testOnDoneCallback() throws InterruptedException {
+        mgmt = LocalManagementContextForTests.newInstance();
+        ExecutionManager em = mgmt.getExecutionManager();
+        BasicExecutionManager bem = (BasicExecutionManager)em;
+        final Map<Task<?>,Duration> completedTasks = MutableMap.of();
+        final Semaphore sema4 = new Semaphore(-1);
+        bem.addListener(new ExecutionListener() {
+            @Override
+            public void onTaskDone(Task<?> task) {
+                Assert.assertTrue(task.isDone());
+                Assert.assertEquals(task.getUnchecked(), "foo");
+                completedTasks.put(task, Duration.sinceUtc(task.getEndTimeUtc()));
+                sema4.release();
+            }
+        });
+        Task<String> t1 = em.submit( Tasks.<String>builder().name("t1").dynamic(false).body(Callables.returning("foo")).build() );
+        t1.getUnchecked();
+        Task<String> t2 = em.submit( Tasks.<String>builder().name("t2").dynamic(false).body(Callables.returning("foo")).build() );
+        sema4.acquire();
+        Assert.assertEquals(completedTasks.size(), 2, "completed tasks are: "+completedTasks);
+        completedTasks.get(t1).isShorterThan(Duration.TEN_SECONDS);
+        completedTasks.get(t2).isShorterThan(Duration.TEN_SECONDS);
+    }
+    
+    protected void forceGc() {
+        ((LocalManagementContext)app.getManagementContext()).getGarbageCollector().gcIteration();
+    }
+
+    protected static Task<?> runEmptyTaskWithNameAndTags(Entity target, String name, Object ...tags) {
+        TaskBuilder<Object> tb = newEmptyTask(name);
+        for (Object tag: tags) tb.tag(tag);
+        Task<?> task = ((EntityInternal)target).getExecutionContext().submit(tb.build());
+        task.getUnchecked();
+        return task;
+    }
+
+    protected static TaskBuilder<Object> newEmptyTask(String name) {
+        return Tasks.builder().name(name).dynamic(false).body(Callables.returning(null));
+    }
+    
+    protected void assertTaskCountForEntitySoon(final Entity entity, final int expectedCount) {
+        // Dead task (and initialization task) should have been GC'd on completion.
+        // However, the GC'ing happens in a listener, executed in a different thread - the task.get()
+        // doesn't block for it. Therefore can't always guarantee it will be GC'ed by now.
+        Repeater.create().backoff(Duration.millis(10), 2, Duration.millis(500)).limitTimeTo(Duration.TEN_SECONDS).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                forceGc();
+                Collection<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(((EntityInternal)entity).getManagementContext().getExecutionManager(), entity);
+                Assert.assertEquals(tasks.size(), expectedCount, "Tasks were "+tasks);
+                return true;
+            }
+        }).runRequiringTrue();
+    }
+
+    @Test
+    public void testGetTasksAndGcBoringTags() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        final Task<?> task = runEmptyTaskWithNameAndTags(e, "should-be-kept", ManagementContextInternal.NON_TRANSIENT_TASK_TAG);
+        runEmptyTaskWithNameAndTags(e, "should-be-gcd", ManagementContextInternal.TRANSIENT_TASK_TAG);
+        
+        assertTaskCountForEntitySoon(e, 1);
+        Collection<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(app.getManagementContext().getExecutionManager(), e);
+        assertEquals(tasks, ImmutableList.of(task), "Mismatched tasks, got: "+tasks);
+    }
+
+    @Test
+    public void testGcTaskAtNormalTagLimit() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_TAG, 2);
+
+        for (int count=0; count<5; count++)
+            runEmptyTaskWithNameAndTags(e, "task"+count, ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+
+        assertTaskCountForEntitySoon(e, 2);
+    }
+    
+    @Test
+    public void testGcTaskAtEntityLimit() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_ENTITY, 2);
+        
+        for (int count=0; count<5; count++)
+            runEmptyTaskWithNameAndTags(e, "task-e-"+count, ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        for (int count=0; count<5; count++)
+            runEmptyTaskWithNameAndTags(app, "task-app-"+count, ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        
+        assertTaskCountForEntitySoon(app, 2);
+        assertTaskCountForEntitySoon(e, 2);
+    }
+    
+    @Test
+    public void testGcTaskWithTagAndEntityLimit() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_ENTITY, 6);
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_TAG, 2);
+
+        int count=0;
+        
+        runEmptyTaskWithNameAndTags(app, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        Time.sleep(Duration.ONE_MILLISECOND);
+        // should keep the 2 below, because all the other borings get grace, but delete the ones above
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag");
+        
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag", "another-tag-e");
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "boring-tag", "another-tag-e");
+        // should keep both the above
+        
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag");
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag");
+        Time.sleep(Duration.ONE_MILLISECOND);
+        runEmptyTaskWithNameAndTags(app, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag");
+        // should keep the below since they have unique tags, but remove one of the e tasks above 
+        runEmptyTaskWithNameAndTags(e, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag", "and-another-tag");
+        runEmptyTaskWithNameAndTags(app, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag-app", "another-tag");
+        runEmptyTaskWithNameAndTags(app, "task-"+(count++), ManagementContextInternal.NON_TRANSIENT_TASK_TAG, "another-tag-app", "another-tag");
+        
+        assertTaskCountForEntitySoon(e, 6);
+        assertTaskCountForEntitySoon(app, 3);
+        
+        // now with a lowered limit, we should remove one more e
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_ENTITY, 5);
+        assertTaskCountForEntitySoon(e, 5);
+    }
+    
+    @Test
+    public void testGcDynamicTaskAtNormalTagLimit() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        ((BrooklynProperties)app.getManagementContext().getConfig()).put(
+            BrooklynGarbageCollector.MAX_TASKS_PER_TAG, 2);
+
+        for (int count=0; count<5; count++) {
+            TaskBuilder<Object> tb = Tasks.builder().name("task-"+count).dynamic(true).body(new Runnable() { @Override public void run() {}})
+                .tag(ManagementContextInternal.NON_TRANSIENT_TASK_TAG).tag("foo");
+            ((EntityInternal)e).getExecutionContext().submit(tb.build()).getUnchecked();
+        }
+
+        // might need an eventually here, if the internal job completion and GC is done in the background
+        // (if there are no test failures for a few months, since Sept 2014, then we can remove this comment)
+        assertTaskCountForEntitySoon(e, 2);
+    }
+    
+    @Test
+    public void testUnmanagedEntityCanBeGcedEvenIfPreviouslyTagged() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        e = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        String eId = e.getId();
+        
+        e.invoke(TestEntity.MY_EFFECTOR, ImmutableMap.<String,Object>of()).get();
+        Set<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(app.getManagementContext().getExecutionManager(), e);
+        Task<?> task = Iterables.get(tasks, 0);
+        assertTrue(task.getTags().contains(BrooklynTaskTags.tagForContextEntity(e)));
+
+        Set<Object> tags = app.getManagementContext().getExecutionManager().getTaskTags();
+        assertTrue(tags.contains(BrooklynTaskTags.tagForContextEntity(e)), "tags="+tags);
+        
+        Entities.destroy(e);
+        forceGc();
+        
+        Set<Object> tags2 = app.getManagementContext().getExecutionManager().getTaskTags();
+        for (Object tag : tags2) {
+            if (tag instanceof Entity && ((Entity)tag).getId().equals(eId)) {
+                fail("tags contains unmanaged entity "+tag);
+            }
+            if ((tag instanceof WrappedEntity) && ((WrappedEntity)tag).entity.getId().equals(eId) 
+                    && ((WrappedEntity)tag).wrappingType.equals(BrooklynTaskTags.CONTEXT_ENTITY)) {
+                fail("tags contains unmanaged entity (wrapped) "+tag);
+            }
+        }
+        return;
+    }
+    
+    @Test(groups="Integration")
+    public void testSubscriptionAndEffectorTasksGced() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        BasicExecutionManager em = (BasicExecutionManager) app.getManagementContext().getExecutionManager();
+        // allow background enrichers to complete
+        Time.sleep(Duration.ONE_SECOND);
+        forceGc();
+        List<Task<?>> t1 = em.getAllTasks();
+        
+        TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        entity.setAttribute(TestEntity.NAME, "bob");
+        entity.invoke(TestEntity.MY_EFFECTOR, ImmutableMap.<String,Object>of()).get();
+        Entities.destroy(entity);
+        Time.sleep(Duration.ONE_SECOND);
+        forceGc();
+        List<Task<?>> t2 = em.getAllTasks();
+        
+        Assert.assertEquals(t1.size(), t2.size(), "lists are different:\n"+t1+"\n"+t2+"\n");
+    }
+
+    /**
+     * Invoke effector many times, where each would claim 10MB because it stores the return value.
+     * If it didn't gc the tasks promptly, it would consume 10GB ram (so would OOME before that).
+     */
+    @Test(groups="Integration")
+    public void testEffectorTasksGcedSoNoOome() throws Exception {
+        
+        BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(BrooklynGarbageCollector.GC_PERIOD, Duration.ONE_MILLISECOND);
+        brooklynProperties.put(BrooklynGarbageCollector.MAX_TASKS_PER_TAG, 2);
+        
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, LocalManagementContextForTests.newInstance(brooklynProperties));
+        TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        for (int i = 0; i < 1000; i++) {
+            if (i%100==0) LOG.info(JavaClassNames.niceClassAndMethod()+": iteration "+i);
+            try {
+                LOG.debug("testEffectorTasksGced: iteration="+i);
+                entity.invoke(TestEntity.IDENTITY_EFFECTOR, ImmutableMap.of("arg", new BigObject(10*1000*1000))).get();
+                
+                Time.sleep(Duration.ONE_MILLISECOND); // Give GC thread a chance to run
+                forceGc();
+            } catch (OutOfMemoryError e) {
+                LOG.warn(JavaClassNames.niceClassAndMethod()+": OOME at iteration="+i);
+                throw e;
+            }
+        }
+    }
+    
+    @Test(groups="Integration")
+    public void testUnmanagedEntityGcedOnUnmanageEvenIfEffectorInvoked() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        
+        BasicAttributeSensor<Object> byteArrayAttrib = new BasicAttributeSensor<Object>(Object.class, "test.byteArray", "");
+
+        for (int i = 0; i < 1000; i++) {
+            if (i<100 && i%10==0 || i%100==0) LOG.info(JavaClassNames.niceClassAndMethod()+": iteration "+i);
+            try {
+                LOG.debug(JavaClassNames.niceClassAndMethod()+": iteration="+i);
+                TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+                entity.setAttribute(byteArrayAttrib, new BigObject(10*1000*1000));
+                entity.invoke(TestEntity.MY_EFFECTOR, ImmutableMap.<String,Object>of()).get();
+                
+                // we get exceptions because tasks are still trying to publish after deployment;
+                // this should prevent them
+//                ((LocalEntityManager)app.getManagementContext().getEntityManager()).stopTasks(entity, Duration.ONE_SECOND);
+//                Entities.destroy(entity);
+                
+                // alternatively if we 'unmanage' instead of destroy, there are usually not errors
+                // (the errors come from the node transitioning to a 'stopping' state on destroy, 
+                // and publishing lots of info then)
+                Entities.unmanage(entity);
+                
+                forceGc();
+                // previously we did an extra GC but it was crazy slow, shouldn't be needed
+//                System.gc(); System.gc();
+            } catch (OutOfMemoryError e) {
+                LOG.warn(JavaClassNames.niceClassAndMethod()+": OOME at iteration="+i);
+                ExecutionManager em = app.getManagementContext().getExecutionManager();
+                Collection<Task<?>> tasks = ((BasicExecutionManager)em).getAllTasks();
+                LOG.info("TASKS count "+tasks.size()+": "+tasks);
+                throw e;
+            }
+        }
+    }
+
+    @Test(groups={"Integration"})
+    public void testEffectorTasksGcedForMaxPerTag() throws Exception {
+        int maxNumTasks = 2;
+        BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(BrooklynGarbageCollector.GC_PERIOD, Duration.ONE_SECOND);
+        brooklynProperties.put(BrooklynGarbageCollector.MAX_TASKS_PER_TAG, 2);
+        
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, LocalManagementContextForTests.newInstance(brooklynProperties));
+        final TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        List<Task<?>> tasks = Lists.newArrayList();
+        
+        for (int i = 0; i < (maxNumTasks+1); i++) {
+            Task<?> task = entity.invoke(TestEntity.MY_EFFECTOR, ImmutableMap.<String,Object>of());
+            task.get();
+            tasks.add(task);
+            
+            // TASKS_OLDEST_FIRST_COMPARATOR is based on comparing EndTimeUtc; but two tasks executed in
+            // rapid succession could finish in same millisecond
+            // (especially when using System.currentTimeMillis, which can return the same time for several millisconds).
+            Thread.sleep(10);
+        }
+        
+        // Should initially have all tasks
+        Set<Task<?>> storedTasks = app.getManagementContext().getExecutionManager().getTasksWithAllTags(
+                ImmutableList.of(BrooklynTaskTags.tagForContextEntity(entity), ManagementContextInternal.EFFECTOR_TAG));
+        assertEquals(storedTasks, ImmutableSet.copyOf(tasks), "storedTasks="+storedTasks+"; expected="+tasks);
+        
+        // Then oldest should be GC'ed to leave only maxNumTasks
+        final List<Task<?>> recentTasks = tasks.subList(tasks.size()-maxNumTasks, tasks.size());
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                Set<Task<?>> storedTasks2 = app.getManagementContext().getExecutionManager().getTasksWithAllTags(
+                       ImmutableList.of(BrooklynTaskTags.tagForContextEntity(entity), ManagementContextInternal.EFFECTOR_TAG));
+                List<String> storedTasks2Str = FluentIterable
+                        .from(storedTasks2)
+                        .transform(new Function<Task<?>, String>() {
+                            @Override public String apply(Task<?> input) {
+                                return taskToVerboseString(input);
+                            }})
+                        .toList();
+                assertEquals(storedTasks2, ImmutableSet.copyOf(recentTasks), "storedTasks="+storedTasks2Str+"; expected="+recentTasks);
+            }});
+    }
+    
+    private String taskToVerboseString(Task t) {
+        return Objects.toStringHelper(t)
+                .add("id", t.getId())
+                .add("displayName", t.getDisplayName())
+                .add("submitTime", t.getSubmitTimeUtc())
+                .add("startTime", t.getStartTimeUtc())
+                .add("endTime", t.getEndTimeUtc())
+                .add("status", t.getStatusSummary())
+                .add("tags", t.getTags())
+                .toString();
+    }
+            
+    @Test(groups="Integration")
+    public void testEffectorTasksGcedForAge() throws Exception {
+        Duration maxTaskAge = Duration.millis(100);
+        Duration maxOverhead = Duration.millis(250);
+        Duration earlyReturnGrace = Duration.millis(10);
+        BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(BrooklynGarbageCollector.GC_PERIOD, Duration.ONE_MILLISECOND);
+        brooklynProperties.put(BrooklynGarbageCollector.MAX_TASK_AGE, maxTaskAge);
+        
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, LocalManagementContextForTests.newInstance(brooklynProperties));
+        final TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        Task<?> oldTask = entity.invoke(TestEntity.MY_EFFECTOR, ImmutableMap.<String,Object>of());
+        oldTask.get();
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                Set<Task<?>> storedTasks = app.getManagementContext().getExecutionManager().getTasksWithAllTags(ImmutableList.of(
+                        BrooklynTaskTags.tagForTargetEntity(entity), 
+                        ManagementContextInternal.EFFECTOR_TAG));
+                assertEquals(storedTasks, ImmutableSet.of(), "storedTasks="+storedTasks);
+            }});
+
+        Duration timeToGc = Duration.of(stopwatch);
+        assertTrue(timeToGc.isLongerThan(maxTaskAge.subtract(earlyReturnGrace)), "timeToGc="+timeToGc+"; maxTaskAge="+maxTaskAge);
+        assertTrue(timeToGc.isShorterThan(maxTaskAge.add(maxOverhead)), "timeToGc="+timeToGc+"; maxTaskAge="+maxTaskAge);
+    }
+    
+    private static class BigObject implements Serializable {
+        private static final long serialVersionUID = -4021304829674972215L;
+        private final int sizeBytes;
+        private final byte[] data;
+        
+        BigObject(int sizeBytes) {
+            this.sizeBytes = sizeBytes;
+            this.data = new byte[sizeBytes];
+        }
+        
+        @Override
+        public String toString() {
+            return "BigObject["+sizeBytes+"/"+data.length+"]";
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextInstancesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextInstancesTest.java b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextInstancesTest.java
new file mode 100644
index 0000000..973b795
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextInstancesTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.core.management.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.apache.brooklyn.core.management.internal.LocalManagementContext;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Tests the {@link LocalManagementContext#terminateAll()} and {@link LocalManagementContext#getInstances()} behaviour.
+ * Note this test must NEVER be run in parallel with other tests, as it will terminate the ManagementContext of those
+ * other tests.
+ * 
+ * @author pveentjer
+ */
+public class LocalManagementContextInstancesTest {
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDown(){
+         LocalManagementContext.terminateAll();
+    }
+
+    /** WIP group because other threads may be running in background, 
+     * creating management contexts at the same time as us (slim chance, but observed once);
+     * they shouldn't be, but cleaning that up is another matter! */
+    @Test(groups="WIP")
+    public void testGetInstances(){
+        LocalManagementContext.terminateAll();
+        LocalManagementContext context1 = new LocalManagementContext();
+        LocalManagementContext context2 = new LocalManagementContext();
+        LocalManagementContext context3 = new LocalManagementContext();
+
+        assertEquals(LocalManagementContext.getInstances(), ImmutableSet.of(context1, context2, context3));
+    }
+
+    /** WIP group because other threads may be running in background;
+     * they shouldn't be, but cleaning that up is another matter! */
+    @Test
+    public void terminateAll(){
+        LocalManagementContext.terminateAll();
+        
+        LocalManagementContext context1 = new LocalManagementContext();
+        LocalManagementContext context2 = new LocalManagementContext();
+
+        LocalManagementContext.terminateAll();
+
+        assertTrue(LocalManagementContext.getInstances().isEmpty());
+        assertFalse(context1.isRunning());
+        assertFalse(context2.isRunning());
+    }
+
+    @Test
+    public void terminateExplicitContext(){
+        LocalManagementContext context1 = new LocalManagementContext();
+        LocalManagementContext context2 = new LocalManagementContext();
+        LocalManagementContext context3 = new LocalManagementContext();
+
+        context2.terminate();
+
+        Assert.assertFalse(LocalManagementContext.getInstances().contains(context2));
+        Assert.assertTrue(LocalManagementContext.getInstances().contains(context1));
+        Assert.assertTrue(LocalManagementContext.getInstances().contains(context3));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextTest.java b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextTest.java
new file mode 100644
index 0000000..d88765c
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalManagementContextTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.core.management.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.management.ManagementContext.PropertiesReloadListener;
+import org.apache.brooklyn.core.management.internal.LocalManagementContext;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.config.BrooklynProperties.Factory.Builder;
+import brooklyn.util.os.Os;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+public class LocalManagementContextTest {
+    
+    private LocalManagementContext context; 
+    private File globalPropertiesFile;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        context = null;
+        globalPropertiesFile = Os.newTempFile(getClass(), "global.properties");
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (context!=null) context.terminate();
+        if (globalPropertiesFile != null) globalPropertiesFile.delete();
+    }
+    
+    @Test
+    public void testReloadPropertiesFromBuilder() throws IOException {
+        String globalPropertiesContents = "brooklyn.location.localhost.displayName=myname";
+        Files.write(globalPropertiesContents, globalPropertiesFile, Charsets.UTF_8);
+        Builder propsBuilder = new BrooklynProperties.Factory.Builder()
+            .globalPropertiesFile(globalPropertiesFile.getAbsolutePath());
+        // no builder support in LocalManagementContextForTests (we are testing that the builder's files are reloaded so we need it here)
+        context = new LocalManagementContext(propsBuilder);
+        Location location = context.getLocationRegistry().resolve("localhost");
+        assertEquals(location.getDisplayName(), "myname");
+        String newGlobalPropertiesContents = "brooklyn.location.localhost.displayName=myname2";
+        Files.write(newGlobalPropertiesContents, globalPropertiesFile, Charsets.UTF_8);
+        context.reloadBrooklynProperties();
+        Location location2 = context.getLocationRegistry().resolve("localhost");
+        assertEquals(location.getDisplayName(), "myname");
+        assertEquals(location2.getDisplayName(), "myname2");
+    }
+    
+    @Test
+    public void testReloadPropertiesFromProperties() throws IOException {
+        String globalPropertiesContents = "brooklyn.location.localhost.displayName=myname";
+        Files.write(globalPropertiesContents, globalPropertiesFile, Charsets.UTF_8);
+        BrooklynProperties brooklynProperties = new BrooklynProperties.Factory.Builder()
+            .globalPropertiesFile(globalPropertiesFile.getAbsolutePath())
+            .build();
+        context = LocalManagementContextForTests.builder(true).useProperties(brooklynProperties).build();
+        Location location = context.getLocationRegistry().resolve("localhost");
+        assertEquals(location.getDisplayName(), "myname");
+        String newGlobalPropertiesContents = "brooklyn.location.localhost.displayName=myname2";
+        Files.write(newGlobalPropertiesContents, globalPropertiesFile, Charsets.UTF_8);
+        context.reloadBrooklynProperties();
+        Location location2 = context.getLocationRegistry().resolve("localhost");
+        assertEquals(location.getDisplayName(), "myname");
+        assertEquals(location2.getDisplayName(), "myname");
+    }
+    
+    @Test
+    public void testPropertiesModified() throws IOException {
+        BrooklynProperties properties = BrooklynProperties.Factory.newEmpty();
+        properties.put("myname", "myval");
+        context = LocalManagementContextForTests.builder(true).useProperties(properties).build();
+        assertEquals(context.getBrooklynProperties().get("myname"), "myval");
+        properties.put("myname", "newval");
+        assertEquals(properties.get("myname"), "newval");
+        // TODO: Should changes in the 'properties' collection be reflected in context.getBrooklynProperties()?
+        assertNotEquals(context.getBrooklynProperties().get("myname"), "newval");
+    }
+    
+    @Test
+    public void testAddAndRemoveReloadListener() {
+        final AtomicInteger reloadedCallbackCount = new AtomicInteger(0);
+        BrooklynProperties properties = BrooklynProperties.Factory.newEmpty();
+        properties.put("myname", "myval");
+        context = LocalManagementContextForTests.builder(true).useProperties(properties).build();
+        PropertiesReloadListener listener = new PropertiesReloadListener() {
+            public void reloaded() {
+                reloadedCallbackCount.incrementAndGet();
+            }
+        };
+        assertEquals(reloadedCallbackCount.get(), 0);
+        context.addPropertiesReloadListener(listener);
+        context.reloadBrooklynProperties();
+        assertEquals(reloadedCallbackCount.get(), 1);
+        context.removePropertiesReloadListener(listener);
+        context.reloadBrooklynProperties();
+        assertEquals(reloadedCallbackCount.get(), 1);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalSubscriptionManagerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalSubscriptionManagerTest.java b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalSubscriptionManagerTest.java
new file mode 100644
index 0000000..635e562
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/internal/LocalSubscriptionManagerTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.core.management.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.management.SubscriptionHandle;
+import org.apache.brooklyn.api.management.SubscriptionManager;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.basic.BasicGroup;
+import brooklyn.entity.basic.Entities;
+
+/**
+ * testing the {@link SubscriptionManager} and associated classes.
+ */
+public class LocalSubscriptionManagerTest extends BrooklynAppUnitTestSupport {
+    
+    private static final int TIMEOUT_MS = 5000;
+    
+    private TestEntity entity;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+    }
+
+    private void manage(Entity ...entities) {
+        for (Entity e: entities)
+            Entities.manage(e);
+    }
+
+    @Test
+    public void testSubscribeToEntityAttributeChange() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        app.subscribe(entity, TestEntity.SEQUENCE, new SensorEventListener<Object>() {
+                @Override public void onEvent(SensorEvent<Object> event) {
+                    latch.countDown();
+                }});
+        entity.setSequenceValue(1234);
+        if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout waiting for Event on TestEntity listener");
+        }
+    }
+    
+    @Test
+    public void testSubscribeToEntityWithAttributeWildcard() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        app.subscribe(entity, null, new SensorEventListener<Object>() {
+            @Override public void onEvent(SensorEvent<Object> event) {
+                latch.countDown();
+            }});
+        entity.setSequenceValue(1234);
+        if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout waiting for Event on TestEntity listener");
+        }
+    }
+    
+    @Test
+    public void testSubscribeToAttributeChangeWithEntityWildcard() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        app.subscribe(null, TestEntity.SEQUENCE, new SensorEventListener<Object>() {
+                @Override public void onEvent(SensorEvent<Object> event) {
+                    latch.countDown();
+                }});
+        entity.setSequenceValue(1234);
+        if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout waiting for Event on TestEntity listener");
+        }
+    }
+    
+    @Test
+    public void testSubscribeToChildAttributeChange() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        app.subscribeToChildren(app, TestEntity.SEQUENCE, new SensorEventListener<Object>() {
+            @Override public void onEvent(SensorEvent<Object> event) {
+                latch.countDown();
+            }});
+        entity.setSequenceValue(1234);
+        if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout waiting for Event on child TestEntity listener");
+        }
+    }
+    
+    @Test
+    public void testSubscribeToMemberAttributeChange() throws Exception {
+        BasicGroup group = app.createAndManageChild(EntitySpec.create(BasicGroup.class));
+        TestEntity member = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        manage(group, member);
+        
+        group.addMember(member);
+
+        final List<SensorEvent<Integer>> events = new CopyOnWriteArrayList<SensorEvent<Integer>>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        app.subscribeToMembers(group, TestEntity.SEQUENCE, new SensorEventListener<Integer>() {
+            @Override public void onEvent(SensorEvent<Integer> event) {
+                events.add(event);
+                latch.countDown();
+            }});
+        member.setAttribute(TestEntity.SEQUENCE, 123);
+
+        if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout waiting for Event on parent TestEntity listener");
+        }
+        assertEquals(events.size(), 1);
+        assertEquals(events.get(0).getValue(), (Integer)123);
+        assertEquals(events.get(0).getSensor(), TestEntity.SEQUENCE);
+        assertEquals(events.get(0).getSource().getId(), member.getId());
+    }
+    
+    // Regression test for ConcurrentModificationException in issue #327
+    @Test(groups="Integration")
+    public void testConcurrentSubscribingAndPublishing() throws Exception {
+        final AtomicReference<Exception> threadException = new AtomicReference<Exception>();
+        TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        // Repeatedly subscribe and unsubscribe, so listener-set constantly changing while publishing to it.
+        // First create a stable listener so it is always the same listener-set object.
+        Thread thread = new Thread() {
+            public void run() {
+                try {
+                    SensorEventListener<Object> noopListener = new SensorEventListener<Object>() {
+                        @Override public void onEvent(SensorEvent<Object> event) {
+                        }
+                    };
+                    app.subscribe(null, TestEntity.SEQUENCE, noopListener);
+                    while (!Thread.currentThread().isInterrupted()) {
+                        SubscriptionHandle handle = app.subscribe(null, TestEntity.SEQUENCE, noopListener);
+                        app.unsubscribe(null, handle);
+                    }
+                } catch (Exception e) {
+                    threadException.set(e);
+                }
+            }
+        };
+        
+        try {
+            thread.start();
+            for (int i = 0; i < 10000; i++) {
+                entity.setAttribute(TestEntity.SEQUENCE, i);
+            }
+        } finally {
+            thread.interrupt();
+        }
+
+        if (threadException.get() != null) throw threadException.get();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiPathTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiPathTest.java b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiPathTest.java
new file mode 100644
index 0000000..8dcbec2
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiPathTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.core.management.osgi;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.brooklyn.core.management.internal.LocalManagementContext;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.osgi.framework.BundleException;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.config.BrooklynServerConfig;
+import brooklyn.config.BrooklynServerPaths;
+import brooklyn.entity.basic.Entities;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.Identifiers;
+
+
+/** 
+ * Tests that OSGi entities load correctly and have the right catalog information set.
+ * Note further tests done elsewhere using CAMP YAML (referring to methods in this class).
+ */
+public class OsgiPathTest {
+   
+    protected LocalManagementContext mgmt = null;
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws BundleException, IOException, InterruptedException {
+        if (mgmt!=null) Entities.destroyAll(mgmt);
+    }
+    
+    @Test(groups="Integration") // integration only because OSGi takes ~200ms
+    public void testOsgiPathDefault() {
+        mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build();
+        String path = BrooklynServerPaths.getOsgiCacheDir(mgmt).getAbsolutePath();
+        Assert.assertTrue(path.startsWith(BrooklynServerPaths.getMgmtBaseDir(mgmt)), path);
+        Assert.assertTrue(path.contains(mgmt.getManagementNodeId()), path);
+        
+        assertExistsThenIsCleaned(path);
+    }
+
+    @Test(groups="Integration") // integration only because OSGi takes ~200ms
+    public void testOsgiPathCustom() {
+        BrooklynProperties bp = BrooklynProperties.Factory.newEmpty();
+        String randomSeg = "osgi-test-"+Identifiers.makeRandomId(4);
+        bp.put(BrooklynServerConfig.OSGI_CACHE_DIR, "${brooklyn.os.tmpdir}"+"/"+randomSeg+"/"+"${brooklyn.mgmt.node.id}");
+        mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).useProperties(bp).build();
+        String path = BrooklynServerPaths.getOsgiCacheDir(mgmt).getAbsolutePath();
+        Os.deleteOnExitRecursivelyAndEmptyParentsUpTo(new File(path), new File(Os.tmp()+"/"+randomSeg));
+        
+        Assert.assertTrue(path.startsWith(Os.tmp()), path);
+        Assert.assertTrue(path.contains(mgmt.getManagementNodeId()), path);
+        
+        assertExistsThenIsCleaned(path);
+    }
+
+    @Test(groups="Integration") // integration only because OSGi takes ~200ms
+    public void testOsgiPathCustomWithoutNodeIdNotCleaned() {
+        BrooklynProperties bp = BrooklynProperties.Factory.newEmpty();
+        String randomSeg = "osgi-test-"+Identifiers.makeRandomId(4);
+        bp.put(BrooklynServerConfig.OSGI_CACHE_DIR, "${brooklyn.os.tmpdir}"+"/"+randomSeg+"/"+"sample");
+        mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).useProperties(bp).build();
+        String path = BrooklynServerPaths.getOsgiCacheDir(mgmt).getAbsolutePath();
+        Os.deleteOnExitRecursivelyAndEmptyParentsUpTo(new File(path), new File(Os.tmp()+"/"+randomSeg));
+        
+        Assert.assertTrue(path.startsWith(Os.tmp()), path);
+        Assert.assertFalse(path.contains(mgmt.getManagementNodeId()), path);
+        
+        assertExistsThenCorrectCleanedBehaviour(path, false);
+    }
+
+    private void assertExistsThenIsCleaned(String path) {
+        assertExistsThenCorrectCleanedBehaviour(path, true);
+    }
+    private void assertExistsThenCorrectCleanedBehaviour(String path, boolean shouldBeCleanAfterwards) {
+        Assert.assertTrue(new File(path).exists(), "OSGi cache "+path+" should exist when in use");
+        Entities.destroyAll(mgmt);
+        mgmt = null;
+        if (shouldBeCleanAfterwards)
+            Assert.assertFalse(new File(path).exists(), "OSGi cache "+path+" should be cleaned after");
+        else
+            Assert.assertTrue(new File(path).exists(), "OSGi cache "+path+" should NOT be cleaned after");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiStandaloneTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiStandaloneTest.java b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiStandaloneTest.java
new file mode 100644
index 0000000..7f1cc48
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiStandaloneTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.core.management.osgi;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarInputStream;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+
+import brooklyn.util.exceptions.Exceptions;
+
+import org.apache.commons.io.FileUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.launch.Framework;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.maven.MavenArtifact;
+import brooklyn.util.maven.MavenRetriever;
+import brooklyn.util.net.Urls;
+import brooklyn.util.os.Os;
+import brooklyn.util.osgi.Osgis;
+import brooklyn.util.osgi.Osgis.ManifestHelper;
+import brooklyn.util.stream.Streams;
+
+/** 
+ * Tests some assumptions about OSGi behaviour, in standalone mode (not part of brooklyn).
+ * See {@link OsgiTestResources} for description of test resources.
+ */
+public class OsgiStandaloneTest {
+
+    private static final Logger log = LoggerFactory.getLogger(OsgiStandaloneTest.class);
+
+    public static final String BROOKLYN_OSGI_TEST_A_0_1_0_PATH = OsgiTestResources.BROOKLYN_OSGI_TEST_A_0_1_0_PATH;
+    public static final String BROOKLYN_OSGI_TEST_A_0_1_0_URL = "classpath:"+BROOKLYN_OSGI_TEST_A_0_1_0_PATH;
+
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_PATH = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_PATH;
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_URL = "classpath:"+BROOKLYN_TEST_OSGI_ENTITIES_PATH;
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_NAME = "org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities";
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_VERSION = "0.1.0";
+
+    protected Framework framework = null;
+    private File storageTempDir;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        storageTempDir = Os.newTempDir("osgi-standalone");
+        framework = Osgis.newFrameworkStarted(storageTempDir.getAbsolutePath(), true, null);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws BundleException, IOException, InterruptedException {
+        tearDownOsgiFramework(framework, storageTempDir);
+    }
+
+    public static void tearDownOsgiFramework(Framework framework, File storageTempDir) throws BundleException, InterruptedException, IOException {
+        if (framework!=null) {
+            framework.stop();
+            Assert.assertEquals(framework.waitForStop(1000).getType(), FrameworkEvent.STOPPED);
+            framework = null;
+        }
+        if (storageTempDir!=null) {
+            FileUtils.deleteDirectory(storageTempDir);
+            storageTempDir = null;
+        }
+    }
+
+    protected Bundle install(String url) throws BundleException {
+        try {
+            return Osgis.install(framework, url);
+        } catch (Exception e) {
+            throw new IllegalStateException("test resources not available; may be an IDE issue, so try a mvn rebuild of this project", e);
+        }
+    }
+
+    protected Bundle installFromClasspath(String resourceName) throws BundleException {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), resourceName);
+        try {
+            return Osgis.install(framework, String.format("classpath:%s", resourceName));
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Test
+    public void testInstallBundle() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle, 3, 6);
+    }
+
+    @Test
+    public void testBootBundle() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        Class<?> bundleCls = bundle.loadClass("brooklyn.osgi.tests.SimpleEntity");
+        Assert.assertEquals(Entity.class,  bundle.loadClass(Entity.class.getName()));
+        Assert.assertEquals(Entity.class, bundleCls.getClassLoader().loadClass(Entity.class.getName()));
+    }
+
+    @Test
+    public void testDuplicateBundle() throws Exception {
+        MavenArtifact artifact = new MavenArtifact("org.apache.brooklyn", "brooklyn-api", "jar", "0.8.0-SNAPSHOT"); // BROOKLYN_VERSION
+        String localUrl = MavenRetriever.localUrl(artifact);
+        if ("file".equals(Urls.getProtocol(localUrl))) {
+            helperDuplicateBundle(localUrl);
+        } else {
+            log.warn("Skipping test OsgiStandaloneTest.testDuplicateBundle due to " + artifact + " not available in local repo.");
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testRemoteDuplicateBundle() throws Exception {
+        helperDuplicateBundle(MavenRetriever.hostedUrl(new MavenArtifact("org.apache.brooklyn", "brooklyn-api", "jar", "0.8.0-SNAPSHOT"))); // BROOKLYN_VERSION
+    }
+
+    public void helperDuplicateBundle(String url) throws Exception {
+        //The bundle is already installed from the boot path.
+        //Make sure that we still get the initially loaded
+        //bundle after trying to install the same version.
+        Bundle bundle = install(url);
+        Assert.assertTrue(Osgis.isExtensionBundle(bundle));
+    }
+
+    @Test
+    public void testAMultiplier() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle, 3, 6);
+        setAMultiplier(bundle, 5);
+        checkMath(bundle, 3, 15);
+    }
+
+    /** run two multiplier tests to ensure that irrespective of order the tests run in, 
+     * on a fresh install the multiplier is reset */
+    @Test
+    public void testANOtherMultiple() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle, 3, 6);
+        setAMultiplier(bundle, 14);
+        checkMath(bundle, 3, 42);
+    }
+
+    @Test
+    public void testGetBundle() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        setAMultiplier(bundle, 3);
+
+        // can look it up based on the same location string (no other "location identifier" reference string seems to work here, however) 
+        Bundle bundle2 = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle2, 3, 9);
+    }
+
+    @Test
+    public void testUninstallAndReinstallBundle() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle, 3, 6);
+        setAMultiplier(bundle, 3);
+        checkMath(bundle, 3, 9);
+        bundle.uninstall();
+        
+        Bundle bundle2 = installFromClasspath(BROOKLYN_OSGI_TEST_A_0_1_0_PATH);
+        checkMath(bundle2, 3, 6);
+    }
+
+    protected void checkMath(Bundle bundle, int input, int output) throws Exception {
+        Assert.assertNotNull(bundle);
+        Class<?> aClass = bundle.loadClass("brooklyn.test.osgi.TestA");
+        Object aInst = aClass.newInstance();
+        Object result = aClass.getMethod("times", int.class).invoke(aInst, input);
+        Assert.assertEquals(result, output);
+    }
+
+    protected void setAMultiplier(Bundle bundle, int newMultiplier) throws Exception {
+        Assert.assertNotNull(bundle);
+        Class<?> aClass = bundle.loadClass("brooklyn.test.osgi.TestA");
+        aClass.getField("multiplier").set(null, newMultiplier);
+    }
+
+    @Test
+    public void testReadAManifest() throws Exception {
+        Enumeration<URL> manifests = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
+        log.info("Bundles and exported packages:");
+        MutableSet<String> allPackages = MutableSet.of();
+        while (manifests.hasMoreElements()) {
+            ManifestHelper mf = Osgis.ManifestHelper.forManifestContents(Streams.readFullyString( manifests.nextElement().openStream() ));
+            List<String> mfPackages = mf.getExportedPackages();
+            log.info("  "+mf.getSymbolicNameVersion()+": "+mfPackages);
+            allPackages.addAll(mfPackages);
+        }
+        log.info("Total export package count: "+allPackages.size());
+        Assert.assertTrue(allPackages.size()>20, "did not find enough packages"); // probably much larger
+        Assert.assertTrue(allPackages.contains(Osgis.class.getPackage().getName()));
+    }
+    
+    @Test
+    public void testReadKnownManifest() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        InputStream in = this.getClass().getResourceAsStream(BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        JarInputStream jarIn = new JarInputStream(in);
+        ManifestHelper helper = Osgis.ManifestHelper.forManifest(jarIn.getManifest());
+        jarIn.close();
+        Assert.assertEquals(helper.getVersion().toString(), "0.1.0");
+        Assert.assertTrue(helper.getExportedPackages().contains("brooklyn.osgi.tests"));
+    }
+    
+    @Test
+    public void testLoadOsgiBundleDependencies() throws Exception {
+        Bundle bundle = installFromClasspath(BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        Assert.assertNotNull(bundle);
+        Class<?> aClass = bundle.loadClass("brooklyn.osgi.tests.SimpleApplicationImpl");
+        Object aInst = aClass.newInstance();
+        Assert.assertNotNull(aInst);
+    }
+    
+    @Test
+    public void testLoadAbsoluteWindowsResourceWithInstalledOSGi() {
+        //Felix installs an additional URL to the system classloader
+        //which throws an IllegalArgumentException when passed a
+        //windows path. See ExtensionManager.java static initializer.
+        String context = "mycontext";
+        String dummyPath = "C:\\dummypath";
+        ResourceUtils utils = ResourceUtils.create(this, context);
+        try {
+            utils.getResourceFromUrl(dummyPath);
+            Assert.fail("Non-reachable, should throw an exception for non-existing resource.");
+        } catch (RuntimeException e) {
+            Assert.assertTrue(e.getMessage().startsWith("Error getting resource '"+dummyPath+"' for "+context));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiTestResources.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiTestResources.java b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiTestResources.java
new file mode 100644
index 0000000..b44da4d
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/management/osgi/OsgiTestResources.java
@@ -0,0 +1,74 @@
+/*
+ * 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.core.management.osgi;
+
+/**
+ * Many OSGi tests require OSGi bundles (of course). Test bundles have been collected here
+ * for convenience and clarity. Available bundles (on the classpath, with source code
+ * either embedded or in /src/dependencies) are described by the constants in this class.
+ * <p>
+ * Some of these bundles are also used in REST API tests, as that stretches catalog further
+ * (using CAMP) and that is one area where OSGi is heavily used. 
+ */
+public class OsgiTestResources {
+
+
+    /**
+     * brooklyn-osgi-test-a_0.1.0 -
+     * defines TestA which has a "times" method and a static multiplier field;
+     * we set the multiplier to determine when we are sharing versions and when not
+     */
+    public static final String BROOKLYN_OSGI_TEST_A_0_1_0_PATH = "/brooklyn/osgi/brooklyn-osgi-test-a_0.1.0.jar";
+
+    /**
+     * brooklyn-test-osgi-entities (v 0.1.0) -
+     * defines an entity and an application, to confirm it can be read and used by brooklyn
+     */
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_PATH = "/brooklyn/osgi/brooklyn-test-osgi-entities.jar";
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_APPLICATION = "brooklyn.osgi.tests.SimpleApplication";
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY = "brooklyn.osgi.tests.SimpleEntity";
+    public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY = "brooklyn.osgi.tests.SimplePolicy";
+
+    /**
+     * brooklyn-test-osgi-more-entities_0.1.0 -
+     * another bundle with a minimal sayHi effector, used to test versioning and dependencies
+     * (this one has no dependencies, but see also {@value #BROOKLYN_TEST_MORE_ENTITIES_V2_PATH})
+     */
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART = "brooklyn-test-osgi-more-entities";
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL = 
+        "org.apache.brooklyn.test.resources.osgi."+BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART;
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V1_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_0.1.0.jar";
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY = "brooklyn.osgi.tests.more.MoreEntity";
+    
+    /**
+     * brooklyn-test-osgi-more-entities_0.2.0 -
+     * similar to {@link #BROOKLYN_TEST_MORE_ENTITIES_V1_PATH} but saying "HI NAME" rather than "Hi NAME",
+     * and declaring an explicit dependency on SimplePolicy from {@link #BROOKLYN_TEST_OSGI_ENTITIES_PATH}
+     */
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_0.2.0.jar";
+    
+    /**
+     * bundle with identical metadata (same symbolic name and version -- hence being an evil twin) 
+     * as {@link #BROOKLYN_TEST_MORE_ENTITIES_V2_PATH},
+     * but slightly different behaviour -- saying "HO NAME" -- in order to make sure we can differentiate two two
+     * at runtime.
+     */
+    public static final String BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_PATH = "/brooklyn/osgi/" + BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FINAL_PART + "_evil-twin_0.2.0.jar";
+    
+}


Mime
View raw message