http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessAndChildrenIntegrationTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessAndChildrenIntegrationTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessAndChildrenIntegrationTest.java
new file mode 100644
index 0000000..3298a2f
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessAndChildrenIntegrationTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.entity.software.base;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import 
org.apache.brooklyn.entity.software.base.SoftwareProcess.ChildStartableMode;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+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 com.google.common.base.Stopwatch;
+
+public class VanillaSoftwareProcessAndChildrenIntegrationTest {
+
+    // TODO Why are these tests so slow? Even when the sleep time was 3 
seconds instead of 10, they would still take about 10 seconds.
+    
+    // Note that tests run by jenkins can be extremely time-sensitive.
+    // e.g. 
http://brooklyn.builds.cloudsoftcorp.com/job/Brooklyn-Master-Integration/io.brooklyn$brooklyn-software-base/217/testReport/junit/brooklyn.entity.basic/VanillaSoftwareProcessAndChildrenIntegrationTest/testModeBackground/
+    //      shows a 5 second difference when in background mode, whereas the 
test originally asserted a difference of <= 1.
+    // Therefore increasing time that tests will take, but decreasing the 
sensitivity so we don't get such false-negatives.
+    
+    private static final Logger log = 
LoggerFactory.getLogger(VanillaSoftwareProcessAndChildrenIntegrationTest.class);
+
+    private static final int PARENT_TASK_SLEEP_LENGTH_SECS = 10;
+    private static final int CHILD_TASK_SLEEP_LENGTH_SECS = 10;
+    private static final int CONCURRENT_MAX_ACCEPTABLE_DIFF_SECS = 
PARENT_TASK_SLEEP_LENGTH_SECS - 1;
+    private static final int SEQUENTIAL_MIN_ACCEPTABLE_DIFF_SECS = 
PARENT_TASK_SLEEP_LENGTH_SECS - 1;
+    private static final int EARLY_RETURN_GRACE_MS = 20;
+    
+    private TestApplication app;
+    private Location localhost;
+
+    private VanillaSoftwareProcess p1;
+    private VanillaSoftwareProcess p2;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setup() {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        localhost = 
app.getManagementContext().getLocationRegistry().resolve("localhost");
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void shutdown() {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test(groups = "Integration")
+    public void testModeNone() {
+        prep(ChildStartableMode.NONE);
+        long startTime = startApp();
+
+        Assert.assertNotNull(p1.getAttribute(SoftwareProcess.RUN_DIR));
+        Assert.assertNull(p2.getAttribute(SoftwareProcess.RUN_DIR));
+        Assert.assertTrue(startTime >= PARENT_TASK_SLEEP_LENGTH_SECS*1000 - 
EARLY_RETURN_GRACE_MS, "startTime="+Time.makeTimeStringRounded(startTime));
+    }
+
+    @Test(groups = "Integration")
+    public void testModeForeground() {
+        prep(ChildStartableMode.FOREGROUND);        
+        long startTime = startApp();
+
+        long timediff = timediff();
+        Assert.assertTrue( Math.abs(timediff) <= 
CONCURRENT_MAX_ACCEPTABLE_DIFF_SECS, "should have started concurrently, not 
with time difference "+timediff+" ("+p1+", "+p2+")" );
+        Assert.assertTrue(startTime >= PARENT_TASK_SLEEP_LENGTH_SECS*1000 - 
EARLY_RETURN_GRACE_MS, "startTime="+Time.makeTimeStringRounded(startTime));
+    }
+
+    @Test(groups = "Integration")
+    public void testModeForegroundLate() {
+        prep(ChildStartableMode.FOREGROUND_LATE);        
+        long startTime = startApp();
+
+        long timediff = timediff();
+        Assert.assertTrue( timediff >= SEQUENTIAL_MIN_ACCEPTABLE_DIFF_SECS, 
"should have started later, not with time difference "+timediff+" ("+p1+", 
"+p2+")" );
+        Assert.assertTrue(startTime >= 2*PARENT_TASK_SLEEP_LENGTH_SECS*1000 - 
EARLY_RETURN_GRACE_MS, "startTime="+Time.makeTimeStringRounded(startTime));
+    }
+
+    @Test(groups = "Integration")
+    public void testModeBackground() {
+        prep(ChildStartableMode.BACKGROUND);
+        long startTime = startApp();
+
+        checkChildComesUpSoon();
+        
+        long timediff = timediff();
+        Assert.assertTrue( Math.abs(timediff) <= 
CONCURRENT_MAX_ACCEPTABLE_DIFF_SECS, "should have started concurrently, not 
with time difference "+timediff+" ("+p1+", "+p2+")" );
+        Assert.assertTrue(startTime >= PARENT_TASK_SLEEP_LENGTH_SECS*1000 - 
EARLY_RETURN_GRACE_MS, "startTime="+Time.makeTimeStringRounded(startTime));
+    }
+
+    @Test(groups = "Integration")
+    public void testModeBackgroundLate() {
+        prep(ChildStartableMode.BACKGROUND_LATE);
+        long startTime = startApp();
+
+        checkChildNotUpYet();
+        checkChildComesUpSoon();
+        
+        long timediff = timediff();
+        Assert.assertTrue( Math.abs(timediff) >= 
SEQUENTIAL_MIN_ACCEPTABLE_DIFF_SECS, "should have started later, not with time 
difference "+timediff+" ("+p1+", "+p2+")" );
+        Assert.assertTrue(startTime >= PARENT_TASK_SLEEP_LENGTH_SECS*1000 - 
EARLY_RETURN_GRACE_MS, "startTime="+Time.makeTimeStringRounded(startTime));
+        
+        // just to prevent warnings
+        waitForBackgroundedTasks(CHILD_TASK_SLEEP_LENGTH_SECS+1);
+        app.stop();
+        app = null;
+    }
+    
+    private long startApp() {
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        app.start(Collections.singleton(localhost));
+        long result = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        log.info("Took "+Time.makeTimeStringRounded(result)+" for app.start to 
complete");
+        return result;
+    }
+
+    private void waitForBackgroundedTasks(int secs) {
+        // child task is backgrounded; quick and dirty way to make sure it 
finishes (after setting service_up)
+        Time.sleep(Duration.seconds(secs));
+    }
+
+    private void checkChildNotUpYet() {
+        Assert.assertFalse(p2.getAttribute(SoftwareProcess.SERVICE_UP));
+    }
+
+    private void checkChildComesUpSoon() {
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        EntityTestUtils.assertAttributeEqualsEventually(p2, 
Attributes.SERVICE_UP, true);
+        log.info("Took "+Time.makeTimeStringRounded(stopwatch)+" for 
child-process to be service-up");
+    }
+
+    private long timediff() {
+        Long d1 = getRunTimeUtc(p1);
+        Long d2 = getRunTimeUtc(p2);
+
+        log.info("timestamps for 
"+JavaClassNames.callerNiceClassAndMethod(1)+" have difference "+(d2-d1));
+
+        return d2 - d1;
+    }
+
+    private Long getRunTimeUtc(VanillaSoftwareProcess p) {
+        Assert.assertNotNull(p.getAttribute(SoftwareProcess.RUN_DIR));
+        return Long.parseLong( Strings.getFirstWordAfter(new 
ResourceUtils(this).getResourceAsString(Os.mergePaths(p.getAttribute(SoftwareProcess.RUN_DIR),
 "DATE")), "utc") );
+    }
+    
+    private void prep(ChildStartableMode mode) {
+        String parentCmd = "echo utc `date +%s` > DATE ; echo human `date` >> 
DATE ; "
+            + "{ nohup sleep 60 & } ; echo $! > $PID_FILE ; sleep 
"+PARENT_TASK_SLEEP_LENGTH_SECS;
+        
+        String childCmd = "echo utc `date +%s` > DATE ; echo human `date` >> 
DATE ; "
+                + "{ nohup sleep 60 & } ; echo $! > $PID_FILE ; sleep 
"+CHILD_TASK_SLEEP_LENGTH_SECS;
+        
+        p1 = 
app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+            .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, parentCmd)
+            .configure(VanillaSoftwareProcess.CHILDREN_STARTABLE_MODE, mode)
+            );
+        p2 = p1.addChild(EntitySpec.create(VanillaSoftwareProcess.class)
+            .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, childCmd));
+        Entities.manage(p2);
+        
+        log.info("testing "+JavaClassNames.callerNiceClassAndMethod(1)+", 
using "+p1+" and "+p2);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntity.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntity.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntity.java
new file mode 100644
index 0000000..aacbe9f
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntity.java
@@ -0,0 +1,27 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+
+@ImplementedBy(MyEntityImpl.class)
+public interface MyEntity extends SoftwareProcess {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityApp.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityApp.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityApp.java
new file mode 100644
index 0000000..461c676
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityApp.java
@@ -0,0 +1,26 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+public class MyEntityApp {
+    public static void main(String[] args) throws InterruptedException {
+        System.out.println("Properties: "+System.getProperties());
+        Thread.sleep(60*60*1000);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityImpl.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityImpl.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityImpl.java
new file mode 100644
index 0000000..15dd95b
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/MyEntityImpl.java
@@ -0,0 +1,125 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+import java.util.List;
+
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.entity.java.JavaSoftwareProcessSshDriver;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.apache.brooklyn.util.text.Identifiers;
+
+public class MyEntityImpl extends SoftwareProcessImpl implements MyEntity {
+    @Override
+    public Class<?> getDriverInterface() {
+        return MyEntityDriver.class;
+    }
+
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        connectServiceUpIsRunning();
+    }
+    
+    @Override
+    protected void disconnectSensors() {
+        super.disconnectSensors();
+        disconnectServiceUpIsRunning();
+    }
+    
+    public interface MyEntityDriver extends SoftwareProcessDriver {}
+
+    public static class MyEntitySshDriver extends JavaSoftwareProcessSshDriver 
implements MyEntityDriver {
+
+        @SetFromFlag("version")
+        public static final ConfigKey<String> SUGGESTED_VERSION = 
ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "0.1");
+
+        public MyEntitySshDriver(MyEntityImpl entity, SshMachineLocation 
machine) {
+            super(entity, machine);
+        }
+
+        @Override
+        protected String getLogFileLocation() {
+            return getRunDir()+"/nohup.out";
+        }
+        
+        @Override
+        public void install() {
+            String resourceName = "/"+MyEntityApp.class.getName().replace(".", 
"/")+".class";
+            ResourceUtils r = ResourceUtils.create(this);
+            if (r.getResourceFromUrl(resourceName) == null) 
+                throw new IllegalStateException("Cannot find resource 
"+resourceName);
+            String tmpFile = 
"/tmp/brooklyn-test-MyEntityApp-"+Identifiers.makeRandomId(6)+".class";
+            int result = getMachine().installTo(resourceName, tmpFile);
+            if (result!=0) throw new IllegalStateException("Cannot install 
"+resourceName+" to "+tmpFile);
+            String saveAs = 
"classes/"+MyEntityApp.class.getPackage().getName().replace(".", 
"/")+"/"+MyEntityApp.class.getSimpleName()+".class";
+            newScript(INSTALLING).
+                failOnNonZeroResultCode().
+                body.append(
+                    "curl -L \"file://"+tmpFile+"\" --create-dirs -o 
"+saveAs+" || exit 9"
+                ).execute();
+        }
+
+        @Override
+        public void customize() {
+            newScript(CUSTOMIZING)
+                .execute();
+        }
+        
+        @Override
+        public void launch() {
+            newScript(MutableMap.of("usePidFile", true), LAUNCHING)
+                .body.append(
+                    String.format("nohup java -classpath %s/classes $JAVA_OPTS 
%s &", getInstallDir(), MyEntityApp.class.getName())
+                ).execute();
+        }
+        
+        @Override
+        public boolean isRunning() {
+            //TODO use PID instead
+            return newScript(MutableMap.of("usePidFile", true), CHECK_RUNNING)
+                .execute() == 0;
+        }
+        
+        @Override
+        public void stop() {
+            newScript(MutableMap.of("usePidFile", true), STOPPING)
+                .execute();
+        }
+
+        @Override
+        public void kill() {
+            newScript(MutableMap.of("usePidFile", true), KILLING)
+                .execute();
+        }
+
+        @Override
+        protected List<String> getCustomJavaConfigOptions() {
+            return 
MutableList.<String>builder().addAll(super.getCustomJavaConfigOptions()).add("-Dabc=def").build();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/NaiveScriptRunnerTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/NaiveScriptRunnerTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/NaiveScriptRunnerTest.java
new file mode 100644
index 0000000..59103f1
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/NaiveScriptRunnerTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags.WrappedStream;
+import org.apache.brooklyn.entity.software.base.lifecycle.NaiveScriptRunner;
+import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.task.BasicExecutionContext;
+import org.apache.brooklyn.util.core.task.BasicExecutionManager;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.base.Throwables;
+
+@Test
+public class NaiveScriptRunnerTest {
+    
+    private static final Logger log = 
LoggerFactory.getLogger(NaiveScriptRunnerTest.class);
+    
+    List<String> commands = new ArrayList<String>();
+    
+    @BeforeMethod
+    private void setup() { commands.clear(); }
+    
+    @SuppressWarnings("rawtypes")
+    private NaiveScriptRunner newMockRunner(final int result) {
+        return new NaiveScriptRunner() {
+            @Override
+            public int execute(List<String> script, String summaryForLogging) {
+                return execute(new MutableMap(), script, summaryForLogging);
+            }
+            @Override
+            public int execute(Map flags, List<String> script, String 
summaryForLogging) {
+                commands.addAll(script);
+                return result;                
+            }
+        };
+    }
+
+    @SuppressWarnings("rawtypes")
+    public static NaiveScriptRunner newLocalhostRunner() {
+        return new NaiveScriptRunner() {
+            LocalhostMachineProvisioningLocation location = new 
LocalhostMachineProvisioningLocation();
+            @Override
+            public int execute(List<String> script, String summaryForLogging) {
+                return execute(new MutableMap(), script, summaryForLogging);
+            }
+            @SuppressWarnings("unchecked")
+            @Override
+            public int execute(Map flags, List<String> script, String 
summaryForLogging) {
+                try {
+                    Map flags2 = MutableMap.of("logPrefix", "test");
+                    flags2.putAll(flags);
+                    return location.obtain().execScript(flags2, 
summaryForLogging, script);
+                } catch (NoMachinesAvailableException e) {
+                    throw Throwables.propagate(e);
+                }
+            }
+        };
+    };
+
+    public void testHeadBodyFootAndResult() {
+        ScriptHelper h = new ScriptHelper(newMockRunner(101), "mock");
+        int result = h.header.
+                append("h1", "h2").body.append("b1", "b2").footer.append("f1", 
"f2").
+                execute();
+        Assert.assertEquals(result, 101);
+        Assert.assertEquals(commands, Arrays.asList("h1", "h2", "b1", "b2", 
"f1", "f2"), "List wrong: "+commands);
+    }
+
+    public void testFailOnNonZero() {
+        ScriptHelper h = new ScriptHelper(newMockRunner(106), "mock");
+        boolean succeededWhenShouldntHave = false;
+        try {
+            h.body.append("ignored").
+                failOnNonZeroResultCode().   // will happen
+                execute();            
+            succeededWhenShouldntHave = true;
+        } catch (Exception e) {
+            log.info("ScriptHelper non-zero causes return code: "+e);
+        }
+        if (succeededWhenShouldntHave) Assert.fail("succeeded when shouldn't 
have");
+    }
+
+    public void testFailOnNonZeroDontFailIfZero() {
+        int result = new ScriptHelper(newMockRunner(0), 
"mock").body.append("ignored").
+                failOnNonZeroResultCode().   // will happen
+                execute();
+        Assert.assertEquals(result, 0);
+    }
+
+
+    @Test(groups = "Integration")
+    public void testFailingCommandFailsEarly() {
+        ScriptHelper script = new ScriptHelper(newLocalhostRunner(), "mock").
+                body.append("curl road://to/nowhere", "exit 11").
+                gatherOutput();
+        int result = script.execute();
+        // should get _1_ from curl failing, not 11 from us
+        // TODO not sure why though!
+        Assert.assertEquals(1, result);
+    }
+
+    // TODO a good way to indicate when downloads fail, as that is quite a 
common case
+    // but i think we need quite a bit of scaffolding to detect that problem 
(without inspecting logs) ...
+
+    @Test(groups = "Integration")
+    public void testGatherOutputStdout() {
+        ScriptHelper script = new ScriptHelper(newLocalhostRunner(), "mock").
+                body.append("echo `echo foo``echo bar`", "exit 8").
+                gatherOutput();
+        int result = script.execute();
+        Assert.assertEquals(8, result);
+        if (!script.getResultStdout().contains("foobar"))
+            Assert.fail("STDOUT does not contain expected text 
'foobar'.\n"+script.getResultStdout()+
+                    "\nSTDERR:\n"+script.getResultStderr());
+    }
+
+    @Test(groups = "Integration")
+    public void testGatherOutputStderr() {
+        ScriptHelper script = new ScriptHelper(newLocalhostRunner(), "mock").
+                body.append("set -x", "curl road://to/nowhere || exit 11").
+                gatherOutput();
+        int result = script.execute();
+        Assert.assertEquals(11, result);
+        if (!script.getResultStderr().contains("road"))
+            Assert.fail("STDERR does not contain expected text 
'road'.\n"+script.getResultStderr()+
+                    "\nSTDOUT:\n"+script.getResultStdout());
+    }
+
+    @Test(groups = "Integration")
+    public void testGatherOutuputNotEnabled() {
+        ScriptHelper script = new ScriptHelper(newLocalhostRunner(), "mock").
+                body.append("echo foo", "exit 11");
+        int result = script.execute();
+        Assert.assertEquals(11, result);
+        boolean succeededWhenShouldNotHave = false;
+        try {
+            script.getResultStdout();
+            succeededWhenShouldNotHave = true;
+        } catch (Exception e) { /* expected */ }
+        if (succeededWhenShouldNotHave) Assert.fail("Should have failed");
+    }
+
+    @Test(groups = "Integration")
+    public void testStreamsInTask() {
+        final ScriptHelper script = new ScriptHelper(newLocalhostRunner(), 
"mock").
+                body.append("echo `echo foo``echo bar`", "grep absent-text 
badfile_which_does_not_exist_blaahblahasdewq").
+                gatherOutput();
+        Assert.assertNull(script.peekTask());
+        Task<Integer> task = script.newTask();
+        Assert.assertTrue(BrooklynTaskTags.streams(task).size() >= 3, 
"Expected at least 3 streams: "+BrooklynTaskTags.streams(task));
+        Assert.assertFalse(Tasks.isQueuedOrSubmitted(task));
+        WrappedStream in = BrooklynTaskTags.stream(task, 
BrooklynTaskTags.STREAM_STDIN);
+        Assert.assertNotNull(in);
+        Assert.assertTrue(in.streamContents.get().contains("echo foo"), 
"Expected 'echo foo' but had: "+in.streamContents.get());
+        Assert.assertTrue(in.streamSize.get() > 0);
+        Assert.assertNotNull(script.peekTask());
+    }
+
+    @Test(groups = "Integration")
+    public void testAutoQueueAndRuntimeStreamsInTask() {
+        final ScriptHelper script = new ScriptHelper(newLocalhostRunner(), 
"mock").
+                body.append("echo `echo foo``echo bar`", "grep absent-text 
badfile_which_does_not_exist_blaahblahasdewq").
+                gatherOutput();
+        Task<Integer> submitter = Tasks.<Integer>builder().body(new 
Callable<Integer>() {
+            public Integer call() {
+                int result = script.execute();
+                return result;
+            } 
+        }).build();
+        BasicExecutionManager em = new BasicExecutionManager("tests");
+        BasicExecutionContext ec = new BasicExecutionContext(em);
+        try {
+            Assert.assertNull(script.peekTask());
+            ec.submit(submitter);
+            // soon there should be a task which is submitted
+            Assert.assertTrue(Repeater.create("get 
script").every(Duration.millis(10)).limitTimeTo(Duration.FIVE_SECONDS).until(new
 Callable<Boolean>() {
+                public Boolean call() { 
+                    return (script.peekTask() != null) && 
Tasks.isQueuedOrSubmitted(script.peekTask());
+                }
+            }).run());
+            Task<Integer> task = script.peekTask();
+            Assert.assertTrue(BrooklynTaskTags.streams(task).size() >= 3, 
"Expected at least 3 streams: "+BrooklynTaskTags.streams(task));
+            // stdin should be populated
+            WrappedStream in = BrooklynTaskTags.stream(task, 
BrooklynTaskTags.STREAM_STDIN);
+            Assert.assertNotNull(in);
+            Assert.assertTrue(in.streamContents.get().contains("echo foo"), 
"Expected 'echo foo' but had: "+in.streamContents.get());
+            Assert.assertTrue(in.streamSize.get() > 0);
+            
+            // out and err should exist
+            WrappedStream out = BrooklynTaskTags.stream(task, 
BrooklynTaskTags.STREAM_STDOUT);
+            WrappedStream err = BrooklynTaskTags.stream(task, 
BrooklynTaskTags.STREAM_STDERR);
+            Assert.assertNotNull(out);
+            Assert.assertNotNull(err);
+            
+            // it should soon finish, with exit code
+            Integer result = task.getUnchecked(Duration.TEN_SECONDS);
+            Assert.assertNotNull(result);
+            Assert.assertTrue(result > 0, "Expected non-zero exit code: 
"+result);
+            // and should contain foobar in stdout
+            if (!script.getResultStdout().contains("foobar"))
+                Assert.fail("Script STDOUT does not contain expected text 
'foobar'.\n"+script.getResultStdout()+
+                        "\nSTDERR:\n"+script.getResultStderr());
+            if (!out.streamContents.get().contains("foobar"))
+                Assert.fail("Task STDOUT does not contain expected text 
'foobar'.\n"+out.streamContents.get()+
+                        "\nSTDERR:\n"+script.getResultStderr());
+            // and "No such file or directory" in stderr
+            if (!script.getResultStderr().contains("No such file or 
directory"))
+                Assert.fail("Script STDERR does not contain expected text 'No 
such ...'.\n"+script.getResultStdout()+
+                        "\nSTDERR:\n"+script.getResultStderr());
+            if (!err.streamContents.get().contains("No such file or 
directory"))
+                Assert.fail("Task STDERR does not contain expected text 'No 
such...'.\n"+out.streamContents.get()+
+                        "\nSTDERR:\n"+script.getResultStderr());
+        } finally {
+            em.shutdownNow();
+        }
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/ScriptHelperTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/ScriptHelperTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/ScriptHelperTest.java
new file mode 100644
index 0000000..479c24f
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/ScriptHelperTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest;
+import 
org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.MyService;
+import 
org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.MyServiceImpl;
+import org.apache.brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.sensor.feed.function.FunctionFeed;
+import org.apache.brooklyn.sensor.feed.function.FunctionPollConfig;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.TestException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.FixedListMachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+
+public class ScriptHelperTest extends BrooklynAppUnitTestSupport {
+    
+    private static final Logger log = 
LoggerFactory.getLogger(ScriptHelperTest.class);
+    
+    private SshMachineLocation machine;
+    private FixedListMachineProvisioningLocation<SshMachineLocation> loc;
+    boolean shouldFail = false;
+    int failCount = 0;
+    
+    @BeforeMethod(alwaysRun=true)
+    @SuppressWarnings("unchecked")
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = 
mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class));
+        machine = 
mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "localhost"));
+        loc.addMachine(machine);
+    }
+
+    @Test
+    public void testCheckRunningForcesInessential() {
+        MyService entity = 
app.createAndManageChild(EntitySpec.create(MyService.class, 
MyServiceInessentialDriverImpl.class));
+        
+        entity.start(ImmutableList.of(loc));
+        SimulatedInessentialIsRunningDriver driver = 
(SimulatedInessentialIsRunningDriver) entity.getDriver();
+        Assert.assertTrue(driver.isRunning());
+        
+        EntityTestUtils.assertAttributeEqualsEventually(entity, 
SoftwareProcess.SERVICE_PROCESS_IS_RUNNING, true);
+        EntityTestUtils.assertAttributeEqualsEventually(entity, 
Startable.SERVICE_UP, true);
+        
+        log.debug("up, now cause failure");
+        
+        driver.setFailExecution(true);
+        EntityTestUtils.assertAttributeEqualsEventually(entity, 
SoftwareProcess.SERVICE_PROCESS_IS_RUNNING, false);
+        
+        log.debug("caught failure, now clear");
+        driver.setFailExecution(false);
+        EntityTestUtils.assertAttributeEqualsEventually(entity, 
SoftwareProcess.SERVICE_PROCESS_IS_RUNNING, true);
+    }
+    
+    public static class MyServiceInessentialDriverImpl extends MyServiceImpl {
+
+        @Override public Class<?> getDriverInterface() {
+            return SimulatedInessentialIsRunningDriver.class;
+        }
+
+        @Override
+        protected void connectSensors() {
+            super.connectSensors();
+            connectServiceUpIsRunning();
+        }
+        
+        @Override
+        public void connectServiceUpIsRunning() {
+//            super.connectServiceUpIsRunning();
+            // run more often
+            FunctionFeed.builder()
+                .entity(this)
+                .period(Duration.millis(10))
+                .poll(new FunctionPollConfig<Boolean, 
Boolean>(SERVICE_PROCESS_IS_RUNNING)
+                    .onException(Functions.constant(Boolean.FALSE))
+                    .callable(new Callable<Boolean>() {
+                        public Boolean call() {
+                            return getDriver().isRunning();
+                        }
+                    }))
+                .build();
+        }
+    }
+    
+    public static class SimulatedInessentialIsRunningDriver extends 
SoftwareProcessEntityTest.SimulatedDriver {
+        private boolean failExecution = false;
+
+        public SimulatedInessentialIsRunningDriver(EntityLocal entity, 
SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        @Override
+        public boolean isRunning() {
+            return newScript(CHECK_RUNNING)
+                .execute() == 0;
+        }
+        
+        @Override
+        public int execute(List<String> script, String summaryForLogging) {
+            if (failExecution) {
+                throw new TestException("Simulated driver exception");
+            }
+            return 0;
+        }
+        
+        @SuppressWarnings("rawtypes")
+        @Override
+        public int execute(Map flags2, List<String> script, String 
summaryForLogging) {
+            if (failExecution) {
+                throw new TestException("Simulated driver exception");
+            }
+            return 0;
+        }
+        
+        public void setFailExecution(boolean failExecution) {
+            this.failExecution = failExecution;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/StartStopSshDriverTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/StartStopSshDriverTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/StartStopSshDriverTest.java
new file mode 100644
index 0000000..1bf3bbe
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/lifecycle/StartStopSshDriverTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.entity.software.base.lifecycle;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.core.test.entity.TestApplicationImpl;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.core.test.entity.TestEntityImpl;
+import org.apache.brooklyn.entity.core.BrooklynConfigKeys;
+import org.apache.brooklyn.entity.core.Entities;
+import 
org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.core.internal.ssh.cli.SshCliTool;
+import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
+import org.apache.brooklyn.util.stream.StreamGobbler;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class StartStopSshDriverTest {
+
+    public class BasicStartStopSshDriver extends 
AbstractSoftwareProcessSshDriver {
+        public BasicStartStopSshDriver(EntityLocal entity, SshMachineLocation 
machine) {
+            super(entity, machine);
+        }
+        public boolean isRunning() { return true; }
+        public void stop() {}
+        public void kill() {}
+        public void install() {}
+        public void customize() {}
+        public void launch() {}
+    }
+
+    private static class ThreadIdTransformer implements Function<ThreadInfo, 
Long> {
+        @Override
+        public Long apply(ThreadInfo t) {
+            return t.getThreadId();
+        }
+    }
+
+    private TestApplication app;
+    private TestEntity entity;
+    private SshMachineLocationWithSshTool sshMachineLocation;
+    private AbstractSoftwareProcessSshDriver driver;
+
+    @SuppressWarnings("rawtypes")
+    protected static class SshMachineLocationWithSshTool extends 
SshMachineLocation {
+        private static final long serialVersionUID = 1L;
+
+        SshTool lastTool;
+        public SshMachineLocationWithSshTool(Map flags) { super(flags); }
+        public SshTool connectSsh(Map args) {
+            SshTool result = super.connectSsh(args);
+            lastTool = result;
+            return result;
+        }
+    }
+    
+    @BeforeMethod(alwaysRun = true)
+    public void setUp() {
+        app = new TestApplicationImpl();
+        entity = new TestEntityImpl(app);
+        Entities.startManagement(app);
+        sshMachineLocation = new 
SshMachineLocationWithSshTool(ImmutableMap.of("address", "localhost"));
+        driver = new BasicStartStopSshDriver(entity, sshMachineLocation);
+    }
+    
+    @Test(groups="Integration")
+    public void testExecuteDoesNotLeaveRunningStreamGobblerThread() {
+        List<ThreadInfo> existingThreads = 
getThreadsCalling(StreamGobbler.class);
+        final List<Long> existingThreadIds = getThreadId(existingThreads);
+        
+        List<String> script = Arrays.asList("echo hello");
+        driver.execute(script, "mytest");
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", 10*1000), new 
Runnable() {
+            @Override
+            public void run() {
+              List<ThreadInfo> currentThreads = 
getThreadsCalling(StreamGobbler.class);
+              Set<Long> currentThreadIds = 
MutableSet.copyOf(getThreadId(currentThreads));
+
+              currentThreadIds.removeAll(existingThreadIds);
+              assertEquals(currentThreadIds, ImmutableSet.<Long>of());
+            }
+        });
+    }
+
+    @Test(groups="Integration")
+    public void testSshScriptHeaderUsedWhenSpecified() {
+        entity.setConfig(BrooklynConfigKeys.SSH_CONFIG_SCRIPT_HEADER, 
"#!/bin/bash -e\necho hello world");
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        driver.execute(ImmutableMap.of("out", out), Arrays.asList("echo 
goodbye"), "test");
+        String s = out.toString();
+        assertTrue(s.contains("goodbye"), "should have said goodbye: "+s);
+        assertTrue(s.contains("hello world"), "should have said hello: "+s);
+        assertTrue(sshMachineLocation.lastTool instanceof SshjTool, "expect 
sshj tool, got "+
+            (sshMachineLocation.lastTool!=null ? 
""+sshMachineLocation.lastTool.getClass()+":" : "") + 
sshMachineLocation.lastTool);
+    }
+
+    @Test(groups="Integration")
+    public void testSshCliPickedUpWhenSpecified() {
+        entity.setConfig(BrooklynConfigKeys.SSH_TOOL_CLASS, 
SshCliTool.class.getName());
+        driver.execute(Arrays.asList("echo hi"), "test");
+        assertTrue(sshMachineLocation.lastTool instanceof SshCliTool, "expect 
CLI tool, got "+
+                        (sshMachineLocation.lastTool!=null ? 
""+sshMachineLocation.lastTool.getClass()+":" : "") + 
sshMachineLocation.lastTool);
+    }
+    
+    private List<ThreadInfo> getThreadsCalling(Class<?> clazz) {
+        String clazzName = clazz.getCanonicalName();
+        List<ThreadInfo> result = MutableList.of();
+        ThreadMXBean threadMxbean = ManagementFactory.getThreadMXBean();
+        ThreadInfo[] threads = threadMxbean.dumpAllThreads(false, false);
+        
+        for (ThreadInfo thread : threads) {
+            StackTraceElement[] stackTrace = thread.getStackTrace();
+            for (StackTraceElement stackTraceElement : stackTrace) {
+                if (clazzName == stackTraceElement.getClassName()) {
+                    result.add(thread);
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    private ImmutableList<Long> getThreadId(List<ThreadInfo> existingThreads) {
+        return FluentIterable.from(existingThreads).transform(new 
ThreadIdTransformer()).toList();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/ApplicationUsageTrackingTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/ApplicationUsageTrackingTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/ApplicationUsageTrackingTest.java
new file mode 100644
index 0000000..11fba70
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/ApplicationUsageTrackingTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.entity.software.base.test.core.mgmt.usage;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+import 
org.apache.brooklyn.core.mgmt.internal.UsageListener.ApplicationMetadata;
+import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage;
+import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage.ApplicationEvent;
+import org.apache.brooklyn.core.objs.proxy.EntityProxy;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class ApplicationUsageTrackingTest {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ApplicationUsageTrackingTest.class);
+
+    protected TestApplication app;
+    protected ManagementContextInternal mgmt;
+
+    protected boolean shouldSkipOnBoxBaseDirResolution() {
+        return true;
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        mgmt = LocalManagementContextForTests.newInstance();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (mgmt != null) Entities.destroyAll(mgmt);
+        } catch (Throwable t) {
+            LOG.error("Caught exception in tearDown method", t);
+        } finally {
+            mgmt = null;
+        }
+    }
+
+    @Test
+    public void testUsageInitiallyEmpty() {
+        Set<ApplicationUsage> usage = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        assertEquals(usage, ImmutableSet.of());
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testAddAndRemoveLegacyUsageListener() throws Exception {
+        final RecordingLegacyUsageListener listener = new 
RecordingLegacyUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.setCatalogItemId("testCatalogItem");
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getApplicationEvents();
+                assertEquals(events.size(), 2, "events="+events); // expect 
STARTING and RUNNING
+                
+                String appId = (String) events.get(0).get(1);
+                String appName = (String) events.get(0).get(2);
+                String entityType = (String) events.get(0).get(3);
+                String catalogItemId = (String) events.get(0).get(4);
+                Map<?,?> metadata = (Map<?, ?>) events.get(0).get(5);
+                ApplicationEvent appEvent = (ApplicationEvent) 
events.get(0).get(6);
+                
+                assertEquals(appId, app.getId(), "events="+events);
+                assertNotNull(appName, "events="+events);
+                assertEquals(catalogItemId, app.getCatalogItemId(), 
"events="+events);
+                assertNotNull(entityType, "events="+events);
+                assertNotNull(metadata, "events="+events);
+                assertEquals(appEvent.getState(), Lifecycle.STARTING, 
"events="+events);
+            }});
+
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.start(ImmutableList.<Location>of());
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+
+    @Test
+    public void testAddAndRemoveUsageListener() throws Exception {
+        final RecordingUsageListener listener = new RecordingUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.setCatalogItemId("testCatalogItem");
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getApplicationEvents();
+                assertEquals(events.size(), 2, "events="+events); // expect 
STARTING and RUNNING
+                ApplicationMetadata appMetadata = (ApplicationMetadata) 
events.get(0).get(1);
+                ApplicationEvent appEvent = (ApplicationEvent) 
events.get(0).get(2);
+                
+                assertEquals(appMetadata.getApplication(), app, 
"events="+events);
+                assertTrue(appMetadata.getApplication() instanceof 
EntityProxy, "events="+events);
+                assertEquals(appMetadata.getApplicationId(), app.getId(), 
"events="+events);
+                assertNotNull(appMetadata.getApplicationName(), 
"events="+events);
+                assertEquals(appMetadata.getCatalogItemId(), 
app.getCatalogItemId(), "events="+events);
+                assertNotNull(appMetadata.getEntityType(), "events="+events);
+                assertNotNull(appMetadata.getMetadata(), "events="+events);
+                assertEquals(appEvent.getState(), Lifecycle.STARTING, 
"events="+events);
+            }});
+
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.start(ImmutableList.<Location>of());
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+    
+    @Test
+    public void testUsageIncludesStartAndStopEvents() {
+        // Start event
+        long preStart = System.currentTimeMillis();
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+        long postStart = System.currentTimeMillis();
+
+        Set<ApplicationUsage> usages1 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage1 = Iterables.getOnlyElement(usages1);
+        assertApplicationUsage(usage1, app);
+        assertApplicationEvent(usage1.getEvents().get(0), Lifecycle.STARTING, 
preStart, postStart);
+        assertApplicationEvent(usage1.getEvents().get(1), Lifecycle.RUNNING, 
preStart, postStart);
+
+        // Stop events
+        long preStop = System.currentTimeMillis();
+        app.stop();
+        long postStop = System.currentTimeMillis();
+
+        Set<ApplicationUsage> usages2 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage2 = Iterables.getOnlyElement(usages2);
+        assertApplicationUsage(usage2, app);
+        assertApplicationEvent(usage2.getEvents().get(2), Lifecycle.STOPPING, 
preStop, postStop);
+        assertApplicationEvent(usage2.getEvents().get(3), Lifecycle.STOPPED, 
preStop, postStop);
+        //Apps unmanage themselves on stop
+        assertApplicationEvent(usage2.getEvents().get(4), Lifecycle.DESTROYED, 
preStop, postStop);
+        
+        assertFalse(mgmt.getEntityManager().isManaged(app), "App should 
already be unmanaged");
+        
+        Set<ApplicationUsage> usages3 = 
mgmt.getUsageManager().getApplicationUsage(Predicates.alwaysTrue());
+        ApplicationUsage usage3 = Iterables.getOnlyElement(usages3);
+        assertApplicationUsage(usage3, app);
+        
+        assertEquals(usage3.getEvents().size(), 5, "usage="+usage3);
+    }
+    
+    private void assertApplicationUsage(ApplicationUsage usage, Application 
expectedApp) {
+        assertEquals(usage.getApplicationId(), expectedApp.getId());
+        assertEquals(usage.getApplicationName(), expectedApp.getDisplayName());
+        assertEquals(usage.getEntityType(), 
expectedApp.getEntityType().getName());
+    }
+    
+    private void assertApplicationEvent(ApplicationEvent event, Lifecycle 
expectedState, long preEvent, long postEvent) {
+        // Saw times differ by 1ms - perhaps different threads calling 
currentTimeMillis() can get out-of-order times?!
+        final int TIMING_GRACE = 5;
+        
+        assertEquals(event.getState(), expectedState);
+        long eventTime = event.getDate().getTime();
+        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + 
TIMING_GRACE)) {
+            fail("for "+expectedState+": event=" + 
Time.makeDateString(eventTime) + "("+eventTime + "); "
+                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ 
"); "
+                    + "post=" + Time.makeDateString(postEvent) + " 
("+postEvent + ")");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/LocationUsageTrackingTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/LocationUsageTrackingTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/LocationUsageTrackingTest.java
new file mode 100644
index 0000000..44760ac
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/LocationUsageTrackingTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.entity.software.base.test.core.mgmt.usage;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.core.mgmt.internal.UsageListener.LocationMetadata;
+import org.apache.brooklyn.core.mgmt.usage.LocationUsage;
+import org.apache.brooklyn.core.mgmt.usage.LocationUsage.LocationEvent;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.time.Time;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class LocationUsageTrackingTest extends BrooklynAppUnitTestSupport {
+
+    private DynamicLocalhostMachineProvisioningLocation loc;
+
+    @BeforeMethod(alwaysRun = true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = 
mgmt.getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
+    }
+
+    @Test
+    public void testUsageInitiallyEmpty() {
+        Set<LocationUsage> usage = 
mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
+        assertEquals(usage, ImmutableSet.of());
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testAddAndRemoveLegacyUsageListener() throws Exception {
+        final RecordingLegacyUsageListener listener = new 
RecordingLegacyUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        
app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+        app.start(ImmutableList.of(loc));
+        final SshMachineLocation machine = 
Iterables.getOnlyElement(loc.getAllMachines());
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                String locId = (String) events.get(0).get(1);
+                LocationEvent locEvent = (LocationEvent) events.get(0).get(3);
+                Map<?,?> metadata = (Map<?, ?>) events.get(0).get(2);
+                
+                assertEquals(events.size(), 1, "events="+events);
+                assertEquals(locId, machine.getId(), "events="+events);
+                assertNotNull(metadata, "events="+events);
+                assertEquals(locEvent.getApplicationId(), app.getId(), 
"events="+events);
+                assertEquals(locEvent.getState(), Lifecycle.CREATED, 
"events="+events);
+            }});
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.stop();
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+
+    @Test
+    public void testAddAndRemoveUsageListener() throws Exception {
+        final RecordingUsageListener listener = new RecordingUsageListener();
+        mgmt.getUsageManager().addUsageListener(listener);
+        
+        
app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+        app.start(ImmutableList.of(loc));
+        final SshMachineLocation machine = 
Iterables.getOnlyElement(loc.getAllMachines());
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                LocationMetadata locMetadata = (LocationMetadata) 
events.get(0).get(1);
+                LocationEvent locEvent = (LocationEvent) events.get(0).get(2);
+                
+                assertEquals(events.size(), 1, "events="+events);
+                assertEquals(locMetadata.getLocation(), machine, 
"events="+events);
+                assertEquals(locMetadata.getLocationId(), machine.getId(), 
"events="+events);
+                assertNotNull(locMetadata.getMetadata(), "events="+events);
+                assertEquals(locEvent.getApplicationId(), app.getId(), 
"events="+events);
+                assertEquals(locEvent.getState(), Lifecycle.CREATED, 
"events="+events);
+            }});
+
+        // Remove the listener; will get no more notifications
+        listener.clearEvents();
+        mgmt.getUsageManager().removeUsageListener(listener);
+        
+        app.stop();
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = listener.getLocationEvents();
+                assertEquals(events.size(), 0, "events="+events);
+            }});
+    }
+
+    @Test
+    public void testUsageIncludesStartAndStopEvents() {
+        SoftwareProcessEntityTest.MyService entity = 
app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+
+        // Start the app; expect record of location in use
+        long preStart = System.currentTimeMillis();
+        app.start(ImmutableList.of(loc));
+        long postStart = System.currentTimeMillis();
+        SshMachineLocation machine = 
Iterables.getOnlyElement(loc.getAllMachines());
+
+        Set<LocationUsage> usages1 = 
mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
+        LocationUsage usage1 = Iterables.getOnlyElement(usages1);
+        assertLocationUsage(usage1, machine);
+        assertLocationEvent(usage1.getEvents().get(0), entity, 
Lifecycle.CREATED, preStart, postStart);
+
+        // Stop the app; expect record of location no longer in use
+        long preStop = System.currentTimeMillis();
+        app.stop();
+        long postStop = System.currentTimeMillis();
+
+        Set<LocationUsage> usages2 = 
mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
+        LocationUsage usage2 = Iterables.getOnlyElement(usages2);
+        assertLocationUsage(usage2, machine);
+        assertLocationEvent(usage2.getEvents().get(1), app.getApplicationId(), 
entity.getId(), entity.getEntityType().getName(), Lifecycle.DESTROYED, preStop, 
postStop);
+        
+        assertEquals(usage2.getEvents().size(), 2, "usage="+usage2);
+    }
+
+    public static class DynamicLocalhostMachineProvisioningLocation extends 
LocalhostMachineProvisioningLocation {
+        private static final long serialVersionUID = 4822009936654077946L;
+
+        @Override
+        public SshMachineLocation obtain(Map<?, ?> flags) throws 
NoMachinesAvailableException {
+            System.out.println("called 
DynamicLocalhostMachineProvisioningLocation.obtain");
+            return super.obtain(flags);
+        }
+
+        @Override
+        public void release(SshMachineLocation machine) {
+            System.out.println("called 
DynamicLocalhostMachineProvisioningLocation.release");
+            super.release(machine);
+            super.machines.remove(machine);
+            super.removeChild(machine);
+        }
+    }
+    
+    private void assertLocationUsage(LocationUsage usage, Location 
expectedLoc) {
+        assertEquals(usage.getLocationId(), expectedLoc.getId(), 
"usage="+usage);
+        assertNotNull(usage.getMetadata(), "usage="+usage);
+    }
+
+    private void assertLocationEvent(LocationEvent event, Entity 
expectedEntity, Lifecycle expectedState, long preEvent, long postEvent) {
+        assertLocationEvent(event, expectedEntity.getApplicationId(), 
expectedEntity.getId(), expectedEntity.getEntityType().getName(), 
expectedState, preEvent, postEvent);
+    }
+    
+    private void assertLocationEvent(LocationEvent event, String 
expectedAppId, String expectedEntityId, String expectedEntityType, Lifecycle 
expectedState, long preEvent, long postEvent) {
+        // Saw times differ by 1ms - perhaps different threads calling 
currentTimeMillis() can get out-of-order times?!
+        final int TIMING_GRACE = 5;
+        
+        assertEquals(event.getApplicationId(), expectedAppId);
+        assertEquals(event.getEntityId(), expectedEntityId);
+        assertEquals(event.getEntityType(), expectedEntityType);
+        assertEquals(event.getState(), expectedState);
+        long eventTime = event.getDate().getTime();
+        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + 
TIMING_GRACE)) {
+            fail("for "+expectedState+": event=" + 
Time.makeDateString(eventTime) + "("+eventTime + "); "
+                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ 
"); "
+                    + "post=" + Time.makeDateString(postEvent) + " 
("+postEvent + ")");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingLegacyUsageListener.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingLegacyUsageListener.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingLegacyUsageListener.java
new file mode 100644
index 0000000..8e2e489
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingLegacyUsageListener.java
@@ -0,0 +1,70 @@
+/*
+ * 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.entity.software.base.test.core.mgmt.usage;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage.ApplicationEvent;
+import org.apache.brooklyn.core.mgmt.usage.LocationUsage.LocationEvent;
+import org.apache.brooklyn.util.collections.MutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+@Deprecated
+public class RecordingLegacyUsageListener implements 
org.apache.brooklyn.core.mgmt.internal.UsageManager.UsageListener {
+
+    private final List<List<?>> events = Lists.newCopyOnWriteArrayList();
+    
+    @Override
+    public void onApplicationEvent(String applicationId, String 
applicationName, String entityType, 
+            String catalogItemId, Map<String, String> metadata, 
ApplicationEvent event) {
+        events.add(MutableList.of("application", applicationId, 
applicationName, entityType, catalogItemId, metadata, event));
+    }
+
+    @Override
+    public void onLocationEvent(String locationId, Map<String, String> 
metadata, LocationEvent event) {
+        events.add(MutableList.of("location", locationId, metadata, event));
+    }
+    
+    public void clearEvents() {
+        events.clear();
+    }
+    
+    public List<List<?>> getEvents() {
+        return ImmutableList.copyOf(events);
+    }
+    
+    public List<List<?>> getLocationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("location")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+    
+    public List<List<?>> getApplicationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("application")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingUsageListener.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingUsageListener.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingUsageListener.java
new file mode 100644
index 0000000..399b575
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/RecordingUsageListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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.entity.software.base.test.core.mgmt.usage;
+
+import java.util.List;
+
+import org.apache.brooklyn.core.mgmt.usage.ApplicationUsage.ApplicationEvent;
+import org.apache.brooklyn.core.mgmt.usage.LocationUsage.LocationEvent;
+import org.apache.brooklyn.util.collections.MutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class RecordingUsageListener implements 
org.apache.brooklyn.core.mgmt.internal.UsageListener {
+
+    private final List<List<?>> events = Lists.newCopyOnWriteArrayList();
+    
+    @Override
+    public void onApplicationEvent(ApplicationMetadata app, ApplicationEvent 
event) {
+        events.add(MutableList.of("application", app, event));
+    }
+
+    @Override
+    public void onLocationEvent(LocationMetadata loc, LocationEvent event) {
+        events.add(MutableList.of("location", loc, event));
+    }
+    
+    public void clearEvents() {
+        events.clear();
+    }
+    
+    public List<List<?>> getEvents() {
+        return ImmutableList.copyOf(events);
+    }
+    
+    public List<List<?>> getLocationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("location")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+    
+    public List<List<?>> getApplicationEvents() {
+        List<List<?>> result = Lists.newArrayList();
+        for (List<?> event : events) {
+            if (event.get(0).equals("application")) result.add(event);
+        }
+        return ImmutableList.copyOf(result);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/UsageListenerTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/UsageListenerTest.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/UsageListenerTest.java
new file mode 100644
index 0000000..518f975
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/core/mgmt/usage/UsageListenerTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.entity.software.base.test.core.mgmt.usage;
+
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.test.Asserts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+import org.apache.brooklyn.core.mgmt.internal.UsageManager;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.Entities;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class UsageListenerTest {
+
+    // Also see {Application|Location}UsageTrackingTest for listener 
functionality
+    
+    private static final Logger LOG = 
LoggerFactory.getLogger(ApplicationUsageTrackingTest.class);
+
+    protected TestApplication app;
+    protected ManagementContextInternal mgmt;
+
+    protected boolean shouldSkipOnBoxBaseDirResolution() {
+        return true;
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        RecordingStaticLegacyUsageListener.clearInstances();
+        RecordingStaticUsageListener.clearInstances();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (mgmt != null) Entities.destroyAll(mgmt);
+        } catch (Throwable t) {
+            LOG.error("Caught exception in tearDown method", t);
+        } finally {
+            mgmt = null;
+            RecordingStaticLegacyUsageListener.clearInstances();
+            RecordingStaticUsageListener.clearInstances();
+        }
+    }
+
+    @Test
+    public void testAddLegacyUsageListenerViaProperties() throws Exception {
+        BrooklynProperties brooklynProperties = 
BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(UsageManager.USAGE_LISTENERS, 
RecordingStaticLegacyUsageListener.class.getName());
+        mgmt = LocalManagementContextForTests.newInstance(brooklynProperties);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = 
RecordingStaticLegacyUsageListener.getInstance().getApplicationEvents();
+                assertTrue(events.size() > 0, "events="+events); // expect 
some events
+            }});
+    }
+    
+    @Test
+    public void testAddUsageListenerViaProperties() throws Exception {
+        BrooklynProperties brooklynProperties = 
BrooklynProperties.Factory.newEmpty();
+        brooklynProperties.put(UsageManager.USAGE_LISTENERS, 
RecordingStaticUsageListener.class.getName());
+        mgmt = LocalManagementContextForTests.newInstance(brooklynProperties);
+        
+        app = TestApplication.Factory.newManagedInstanceForTests(mgmt);
+        app.start(ImmutableList.<Location>of());
+
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                List<List<?>> events = 
RecordingStaticUsageListener.getInstance().getApplicationEvents();
+                assertTrue(events.size() > 0, "events="+events); // expect 
some events
+            }});
+    }
+    
+    public static class RecordingStaticLegacyUsageListener extends 
RecordingLegacyUsageListener implements 
org.apache.brooklyn.core.mgmt.internal.UsageManager.UsageListener {
+        private static final List<RecordingStaticLegacyUsageListener> 
STATIC_INSTANCES = Lists.newCopyOnWriteArrayList();
+        
+        public static RecordingStaticLegacyUsageListener getInstance() {
+            return Iterables.getOnlyElement(STATIC_INSTANCES);
+        }
+
+        public static void clearInstances() {
+            STATIC_INSTANCES.clear();
+        }
+        
+        public RecordingStaticLegacyUsageListener() {
+            // Bad to leak a ref to this before constructor finished, but 
we'll live with it because
+            // it's just test code!
+            STATIC_INSTANCES.add(this);
+        }
+    }
+    
+    public static class RecordingStaticUsageListener extends 
RecordingUsageListener implements 
org.apache.brooklyn.core.mgmt.internal.UsageListener {
+        private static final List<RecordingStaticUsageListener> 
STATIC_INSTANCES = Lists.newCopyOnWriteArrayList();
+        
+        public static RecordingStaticUsageListener getInstance() {
+            return Iterables.getOnlyElement(STATIC_INSTANCES);
+        }
+
+        public static void clearInstances() {
+            STATIC_INSTANCES.clear();
+        }
+        
+        public RecordingStaticUsageListener() {
+            // Bad to leak a ref to this before constructor finished, but 
we'll live with it because
+            // it's just test code!
+            STATIC_INSTANCES.add(this);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/driver/MockSshDriver.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/driver/MockSshDriver.java
 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/driver/MockSshDriver.java
new file mode 100644
index 0000000..9c92b5b
--- /dev/null
+++ 
b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/test/driver/MockSshDriver.java
@@ -0,0 +1,72 @@
+/*
+ * 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.entity.software.base.test.driver;
+
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+public class MockSshDriver implements SoftwareProcessDriver {
+
+    public int numCallsToRunApp = 0;
+    private final EntityLocal entity;
+    private final SshMachineLocation machine;
+
+    public MockSshDriver(EntityLocal entity, SshMachineLocation machine) {
+        this.entity = entity;
+        this.machine = machine;
+    }
+    
+    @Override
+    public void start() {
+        numCallsToRunApp++;
+    }
+
+    @Override
+    public boolean isRunning() {
+        return numCallsToRunApp>0;
+    }
+
+    @Override
+    public EntityLocal getEntity() {
+        return entity;
+    }
+
+    @Override
+    public Location getLocation() {
+        return machine;
+    }
+
+    @Override
+    public void rebind() {
+    }
+
+    @Override
+    public void stop() {
+    }
+
+    @Override
+    public void restart() {
+    }
+    
+    @Override
+    public void kill() {
+    }
+}

Reply via email to