You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/08/19 13:09:26 UTC

[08/72] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to software-base, tidying package names, and moving a few sensory things to core

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java
new file mode 100644
index 0000000..b341b6e
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.Arrays;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.sensor.ssh.SshEffectorTasks;
+import org.apache.brooklyn.sensor.ssh.SshEffectorTasks.SshEffectorBody;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+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 org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import com.google.common.base.Throwables;
+
+public class SoftwareEffectorTest {
+
+    private static final Logger log = LoggerFactory.getLogger(SoftwareEffectorTest.class);
+                
+    TestApplication app;
+    ManagementContext mgmt;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setup() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        mgmt = app.getManagementContext();
+        
+        LocalhostMachineProvisioningLocation lhc = mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+        SshMachineLocation lh = lhc.obtain();
+        app.start(Arrays.asList(lh));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (mgmt != null) Entities.destroyAll(mgmt);
+        mgmt = null;
+    }
+
+    public static final Effector<String> GET_REMOTE_DATE_1 = Effectors.effector(String.class, "getRemoteDate")
+            .description("retrieves the date from the remote machine")
+            .impl(new SshEffectorBody<String>() {
+                public String call(ConfigBag parameters) {
+                    queue( ssh("date").requiringZeroAndReturningStdout() );
+                    return last(String.class);
+                }
+            })
+            .build();
+
+    public static final Effector<String> GET_REMOTE_DATE_2 = Effectors.effector(GET_REMOTE_DATE_1)
+            // Just get year to confirm implementation is different
+            .description("retrieves the year from the remote machine")
+            .impl(SshEffectorTasks.ssh("date +%Y").requiringZeroAndReturningStdout())
+            .build();
+
+    // TODO revisit next two tests before end 2019 ;)
+
+    @Test(groups="Integration")
+    public void testSshDateEffector1() {
+        Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_1);
+        log.info("ssh date 1 gives: "+call.getUnchecked());
+        Assert.assertTrue(call.getUnchecked().indexOf("201") > 0);
+    }
+
+    @Test(groups="Integration")
+    public void testSshDateEffector2() {
+        Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_2);
+        log.info("ssh date 2 gives: "+call.getUnchecked());
+        Assert.assertTrue(call.getUnchecked().indexOf("201") == 0);
+    }
+
+    public static final String COMMAND_THAT_DOES_NOT_EXIST = "blah_blah_blah_command_DOES_NOT_EXIST";
+    
+    @Test(groups="Integration")
+    public void testBadExitCodeCaught() {
+        Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode")
+                .impl(new SshEffectorBody<Void>() {
+                    public Void call(ConfigBag parameters) {
+                        queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringZeroAndReturningStdout() );
+                        return null;
+                    }
+                }).build() );
+        try {
+            Object result = call.getUnchecked();
+            Assert.fail("ERROR: should have failed earlier in this test, instead got successful task result "+result+" from "+call);
+        } catch (Exception e) {
+            Throwable root = Throwables.getRootCause(e);
+            if (!(root instanceof IllegalStateException)) Assert.fail("Should have failed with IAE, but got: "+root);
+            if (root.getMessage()==null || root.getMessage().indexOf("exit code")<=0) 
+                Assert.fail("Should have failed with 'exit code' message, but got: "+root);
+            // test passed
+            return;
+        }
+    }
+        
+    @Test(groups="Integration")
+    public void testBadExitCodeCaughtAndStdErrAvailable() {
+        final ProcessTaskWrapper<?>[] sshTasks = new ProcessTaskWrapper[1];
+        
+        Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode")
+                .impl(new SshEffectorBody<Void>() {
+                    public Void call(ConfigBag parameters) {
+                        sshTasks[0] = queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringExitCodeZero() );
+                        return null;
+                    }
+                }).build() );
+        call.blockUntilEnded();
+        Assert.assertTrue(call.isError());
+        log.info("stderr gives: "+new String(sshTasks[0].getStderr()));
+        Assert.assertTrue(new String(sshTasks[0].getStderr()).indexOf(COMMAND_THAT_DOES_NOT_EXIST) >= 0);
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java
new file mode 100644
index 0000000..e743b24
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.Entity;
+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.api.mgmt.Task;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+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.SoftwareProcessEntityTest.MyService;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.SimulatedDriver;
+import org.apache.brooklyn.entity.stock.BasicEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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 org.apache.brooklyn.sensor.core.DependentConfiguration;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.core.task.TaskInternal;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+
+public class SoftwareProcessEntityLatchTest extends BrooklynAppUnitTestSupport {
+
+    // NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves.
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessEntityLatchTest.class);
+
+    private SshMachineLocation machine;
+    private FixedListMachineProvisioningLocation<SshMachineLocation> loc;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = getLocation();
+    }
+
+    @SuppressWarnings("unchecked")
+    private FixedListMachineProvisioningLocation<SshMachineLocation> getLocation() {
+        FixedListMachineProvisioningLocation<SshMachineLocation> loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class));
+        machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "localhost"));
+        loc.addMachine(machine);
+        return loc;
+    }
+
+    @Test
+    public void testStartLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.START_LATCH, ImmutableList.<String>of());
+    }
+
+    @Test
+    public void testSetupLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.SETUP_LATCH, ImmutableList.<String>of());
+    }
+
+    @Test
+    public void testIntallResourcesLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.INSTALL_RESOURCES_LATCH, ImmutableList.of("setup"));
+    }
+
+    @Test
+    public void testInstallLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.INSTALL_LATCH, ImmutableList.of("setup", "copyInstallResources"));
+    }
+
+    @Test
+    public void testCustomizeLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.CUSTOMIZE_LATCH, ImmutableList.of("setup", "copyInstallResources", "install"));
+    }
+
+    @Test
+    public void testRuntimeResourcesLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.RUNTIME_RESOURCES_LATCH, ImmutableList.of("setup", "copyInstallResources", "install", "customize"));
+    }
+
+    @Test
+    public void testLaunchLatchBlocks() throws Exception {
+        runTestLatchBlocks(SoftwareProcess.LAUNCH_LATCH, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources"));
+    }
+
+    protected void runTestLatchBlocks(final ConfigKey<Boolean> latch, List<String> preLatchEvents) throws Exception {
+        final BasicEntity triggerEntity = app.createAndManageChild(EntitySpec.create(BasicEntity.class));
+        final MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
+                .configure(latch, DependentConfiguration.attributeWhenReady(triggerEntity, Attributes.SERVICE_UP)));
+        
+        final Task<Void> task = Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc)));
+        
+        assertEffectorBlockingDetailsEventually(entity, "Waiting for config "+latch.getName());
+        assertDriverEventsEquals(entity, preLatchEvents);
+
+        assertFalse(task.isDone());
+        ((EntityLocal)triggerEntity).setAttribute(Attributes.SERVICE_UP, true);
+        task.get(Duration.THIRTY_SECONDS);
+        assertDriverEventsEquals(entity, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources", "launch"));
+    }
+
+    private void assertDriverEventsEquals(MyService entity, List<String> expectedEvents) {
+        List<String> events = ((SimulatedDriver)entity.getDriver()).events;
+        assertEquals(events, expectedEvents, "events="+events);
+    }
+
+    private void assertEffectorBlockingDetailsEventually(final Entity entity, final String blockingDetailsSnippet) {
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                Task<?> entityTask = Iterables.getOnlyElement(mgmt.getExecutionManager().getTasksWithAllTags(ImmutableList.of(BrooklynTaskTags.EFFECTOR_TAG, BrooklynTaskTags.tagForContextEntity(entity))));
+                String blockingDetails = getBlockingDetails(entityTask);
+                assertTrue(blockingDetails.contains(blockingDetailsSnippet));
+            }});
+    }
+    
+    private String getBlockingDetails(Task<?> task) {
+        List<TaskInternal<?>> taskChain = Lists.newArrayList();
+        TaskInternal<?> taskI = (TaskInternal<?>) task;
+        while (taskI != null) {
+            taskChain.add(taskI);
+            if (taskI.getBlockingDetails() != null) {
+                return taskI.getBlockingDetails();
+            }
+            taskI = (TaskInternal<?>) taskI.getBlockingTask();
+        }
+        throw new IllegalStateException("No blocking details for "+task+" (walked task chain "+taskChain+")");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java
new file mode 100644
index 0000000..475cb09
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+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.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic.ServiceProblemsLogic;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.MyService;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.location.basic.AbstractLocation;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Files;
+
+public class SoftwareProcessEntityRebindTest extends BrooklynAppUnitTestSupport {
+
+    private ClassLoader classLoader = getClass().getClassLoader();
+    private TestApplication newApp;
+    private ManagementContext newManagementContext;
+    private MyService origE;
+    private File mementoDir;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        mementoDir = Files.createTempDir();
+        mgmt = RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader);
+        super.setUp();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        if (newApp != null) Entities.destroyAll(newApp.getManagementContext());
+        if (newManagementContext != null) Entities.destroyAll(newManagementContext);
+        if (mementoDir != null) RebindTestUtils.deleteMementoDir(mementoDir);
+    }
+    
+    @Test
+    public void testReleasesLocationOnStopAfterRebinding() throws Exception {
+        origE = app.createAndManageChild(EntitySpec.create(MyService.class));
+        
+        MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class)
+                .displayName("mylocname"));
+        app.start(ImmutableList.of(origLoc));
+        assertEquals(origLoc.inUseCount.get(), 1);
+        
+        newApp = (TestApplication) rebind();
+        MyProvisioningLocation newLoc = (MyProvisioningLocation) Iterables.getOnlyElement(newApp.getLocations());
+        assertEquals(newLoc.inUseCount.get(), 1);
+        
+        newApp.stop();
+        assertEquals(newLoc.inUseCount.get(), 0);
+    }
+
+    @Test
+    public void testCreatesDriverAfterRebind() throws Exception {
+        origE = app.createAndManageChild(EntitySpec.create(MyService.class));
+        //the entity skips enricher initialization, do it explicitly
+        origE.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp());
+
+        MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class)
+                .displayName("mylocname"));
+        app.start(ImmutableList.of(origLoc));
+        assertEquals(origE.getAttribute(Attributes.SERVICE_STATE_EXPECTED).getState(), Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+
+        ServiceProblemsLogic.updateProblemsIndicator((EntityLocal)origE, "test", "fire");
+        EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+
+        newApp = (TestApplication) rebind();
+        MyService newE = (MyService) Iterables.getOnlyElement(newApp.getChildren());
+        assertTrue(newE.getDriver() != null, "driver should be initialized");
+    }
+
+    @Test
+    public void testDoesNotCreateDriverAfterRebind() throws Exception {
+        origE = app.createAndManageChild(EntitySpec.create(MyService.class));
+        //the entity skips enricher initialization, do it explicitly
+        origE.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp());
+        
+        MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class)
+                .displayName("mylocname"));
+        app.start(ImmutableList.of(origLoc));
+        assertEquals(origE.getAttribute(Attributes.SERVICE_STATE_EXPECTED).getState(), Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+
+        ServiceStateLogic.setExpectedState(origE, Lifecycle.ON_FIRE);
+        EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+
+        newApp = (TestApplication) rebind();
+        MyService newE = (MyService) Iterables.getOnlyElement(newApp.getChildren());
+        assertNull(newE.getDriver(), "driver should not be initialized because entity is in a permanent failure");
+    }
+
+    private TestApplication rebind() throws Exception {
+        RebindTestUtils.waitForPersisted(app);
+        TestApplication result = (TestApplication) RebindTestUtils.rebind(mementoDir, getClass().getClassLoader());
+        newManagementContext = result.getManagementContext();
+        return result;
+    }
+    
+    public static class MyProvisioningLocation extends AbstractLocation implements MachineProvisioningLocation<SshMachineLocation> {
+        private static final long serialVersionUID = 1L;
+        
+        @SetFromFlag(defaultVal="0")
+        AtomicInteger inUseCount;
+
+        public MyProvisioningLocation() {
+        }
+        
+        @Override
+        public MachineProvisioningLocation<SshMachineLocation> newSubLocation(Map<?, ?> newFlags) {
+            throw new UnsupportedOperationException();
+        }
+        
+        @Override
+        public SshMachineLocation obtain(Map flags) throws NoMachinesAvailableException {
+            inUseCount.incrementAndGet();
+            return getManagementContext().getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                    .parent(this)
+                    .configure("address","localhost"));
+        }
+
+        @Override
+        public void release(SshMachineLocation machine) {
+            inUseCount.decrementAndGet();
+        }
+
+        @Override
+        public Map getProvisioningFlags(Collection tags) {
+            return Collections.emptyMap();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java
new file mode 100644
index 0000000..0a6bd26
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java
@@ -0,0 +1,798 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.mgmt.EntityManager;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.BrooklynConfigKeys;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.core.EntityInternal;
+import org.apache.brooklyn.entity.drivers.BasicEntityDriverManager;
+import org.apache.brooklyn.entity.drivers.ReflectiveEntityDriverFactory;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessDriver;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl;
+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.entity.software.base.SoftwareProcess.RestartSoftwareParameters;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters.RestartMachineMode;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
+import org.apache.brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.sensor.core.PortAttributeSensorAndConfigKey;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException;
+import org.apache.brooklyn.util.net.UserAndHostAndPort;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.jclouds.util.Throwables2;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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 org.apache.brooklyn.location.basic.FixedListMachineProvisioningLocation;
+import org.apache.brooklyn.location.basic.Locations;
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+
+public class SoftwareProcessEntityTest extends BrooklynAppUnitTestSupport {
+
+    // NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves.
+
+    private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessEntityTest.class);
+
+    private SshMachineLocation machine;
+    private FixedListMachineProvisioningLocation<SshMachineLocation> loc;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = getLocation();
+    }
+
+    @SuppressWarnings("unchecked")
+    private FixedListMachineProvisioningLocation<SshMachineLocation> getLocation() {
+        FixedListMachineProvisioningLocation<SshMachineLocation> loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class));
+        machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "localhost"));
+        loc.addMachine(machine);
+        return loc;
+    }
+
+    @Test
+    public void testSetsMachineAttributes() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        
+        assertEquals(entity.getAttribute(SoftwareProcess.HOSTNAME), machine.getAddress().getHostName());
+        assertEquals(entity.getAttribute(SoftwareProcess.ADDRESS), machine.getAddress().getHostAddress());
+        assertEquals(entity.getAttribute(Attributes.SSH_ADDRESS), UserAndHostAndPort.fromParts(machine.getUser(), machine.getAddress().getHostName(), machine.getPort()));
+        assertEquals(entity.getAttribute(SoftwareProcess.PROVISIONING_LOCATION), loc);
+    }
+
+    @Test
+    public void testProcessTemplateWithExtraSubstitutions() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver driver = (SimulatedDriver) entity.getDriver();
+        Map<String,String> substitutions = MutableMap.of("myname","peter");
+        String result = driver.processTemplate("/org/apache/brooklyn/entity/software/base/template_with_extra_substitutions.txt",substitutions);
+        Assert.assertTrue(result.contains("peter"));
+    }
+
+    @Test
+    public void testInstallDirAndRunDir() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
+            .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo"));
+
+        entity.start(ImmutableList.of(loc));
+
+        Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/MyService");
+        Assert.assertEquals(entity.getAttribute(SoftwareProcess.RUN_DIR), "/tmp/brooklyn-foo/apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId());
+    }
+
+    @Test
+    public void testInstallDirAndRunDirUsingTilde() throws Exception {
+        String dataDirName = ".brooklyn-foo"+Strings.makeRandomId(4);
+        String dataDir = "~/"+dataDirName;
+        String resolvedDataDir = Os.mergePaths(Os.home(), dataDirName);
+        
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
+            .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, dataDir));
+
+        entity.start(ImmutableList.of(loc));
+
+        Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.INSTALL_DIR)),
+                            Os.nativePath(Os.mergePaths(resolvedDataDir, "installs/MyService")));
+        Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.RUN_DIR)),
+                            Os.nativePath(Os.mergePaths(resolvedDataDir, "apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId())));
+    }
+
+    protected <T extends MyService> void doStartAndCheckVersion(Class<T> type, String expectedLabel, ConfigBag config) {
+        MyService entity = app.createAndManageChild(EntitySpec.create(type)
+            .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo")
+            .configure(config.getAllConfigAsConfigKeyMap()));
+        entity.start(ImmutableList.of(loc));
+        Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/"
+            + expectedLabel);
+    }
+    
+    @Test
+    public void testCustomInstallDir0() throws Exception {
+        doStartAndCheckVersion(MyService.class, "MyService", ConfigBag.newInstance());
+    }
+    @Test
+    public void testCustomInstallDir1() throws Exception {
+        doStartAndCheckVersion(MyService.class, "MyService_9.9.8", ConfigBag.newInstance()
+            .configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.8"));
+    }
+    @Test
+    public void testCustomInstallDir2() throws Exception {
+        doStartAndCheckVersion(MyService.class, "MySvc_998", ConfigBag.newInstance()
+            .configure(SoftwareProcess.INSTALL_UNIQUE_LABEL, "MySvc_998"));
+    }
+    @Test
+    public void testCustomInstallDir3() throws Exception {
+        doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9", ConfigBag.newInstance());
+    }
+    @Test
+    public void testCustomInstallDir4() throws Exception {
+        doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.7", ConfigBag.newInstance()
+            .configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.7"));
+    }
+    @Test
+    public void testCustomInstallDir5() throws Exception {
+        doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9_NaCl", ConfigBag.newInstance()
+            .configure(ConfigKeys.newStringConfigKey("salt"), "NaCl"));
+    }
+
+    @Test
+    public void testBasicSoftwareProcessEntityLifecycle() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Assert.assertTrue(d.isRunning());
+        entity.stop();
+        Assert.assertEquals(d.events, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources", "launch", "stop"));
+        assertFalse(d.isRunning());
+    }
+    
+    @Test
+    public void testBasicSoftwareProcessRestarts() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Assert.assertTrue(d.isRunning());
+        
+        // this will cause restart to fail if it attempts to replace the machine
+        loc.removeMachine(Locations.findUniqueSshMachineLocation(entity.getLocations()).get());
+        
+        // with defaults, it won't reboot machine
+        d.events.clear();
+        entity.restart();
+        assertEquals(d.events, ImmutableList.of("stop", "launch"));
+
+        // but here, it will try to reboot, and fail because there is no machine available
+        TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART, 
+                ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE)));
+        t1.asTask().blockUntilEnded(Duration.TEN_SECONDS);
+        if (!t1.asTask().isError()) {
+            Assert.fail("Should have thrown error during "+t1+" because no more machines available at "+loc);
+        }
+
+        // now it has a machine, so reboot should succeed
+        SshMachineLocation machine2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+            .configure("address", "localhost"));
+        loc.addMachine(machine2);
+        TaskAdaptable<Void> t2 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART, 
+            ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE)));
+        t2.asTask().get();
+        
+        assertFalse(d.isRunning());
+    }
+
+    @Test
+    public void testBasicSoftwareProcessStopsEverything() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+
+        d.events.clear();
+        entity.stop();
+        assertEquals(d.events, ImmutableList.of("stop"));
+        assertEquals(entity.getLocations().size(), 0);
+        assertTrue(loc.getAvailable().contains(machine));
+    }
+
+    @Test
+    public void testBasicSoftwareProcessStopEverythingExplicitly() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+        d.events.clear();
+
+        TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
+                ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.IF_NOT_STOPPED)));
+        t1.asTask().get();
+
+        assertEquals(d.events, ImmutableList.of("stop"));
+        assertEquals(entity.getLocations().size(), 0);
+        assertTrue(loc.getAvailable().contains(machine));
+    }
+
+    @Test
+    public void testBasicSoftwareProcessStopsProcess() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+        d.events.clear();
+
+        TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
+                ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.NEVER)));
+        t1.asTask().get(10, TimeUnit.SECONDS);
+
+        assertEquals(d.events, ImmutableList.of("stop"));
+        assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine));
+        assertFalse(loc.getAvailable().contains(machine));
+    }
+    
+    @Test(groups = "Integration")
+    public void testBasicSoftwareProcessStopAllModes() throws Exception {
+        for (boolean isEntityStopped : new boolean[] {true, false}) {
+            for (StopMode stopProcessMode : StopMode.values()) {
+                for (StopMode stopMachineMode : StopMode.values()) {
+                    try {
+                        testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped);
+                    } catch (Exception e) {
+                        String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped;
+                        throw new PropagatedRuntimeException(msg, e);
+                    }
+                }
+            }
+        }
+    }
+    
+    @Test
+    public void testBasicSoftwareProcessStopSomeModes() throws Exception {
+        for (boolean isEntityStopped : new boolean[] {true, false}) {
+            StopMode stopProcessMode = StopMode.IF_NOT_STOPPED;
+            StopMode stopMachineMode = StopMode.IF_NOT_STOPPED;
+            try {
+                testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped);
+            } catch (Exception e) {
+                String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped;
+                throw new PropagatedRuntimeException(msg, e);
+            }
+        }
+    }
+    
+    private void testBasicSoftwareProcessStopModes(StopMode stopProcessMode, StopMode stopMachineMode, boolean isEntityStopped) throws Exception {
+        FixedListMachineProvisioningLocation<SshMachineLocation> l = getLocation();
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(l));
+        SimulatedDriver d = (SimulatedDriver) entity.getDriver();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+        d.events.clear();
+
+        if (isEntityStopped) {
+            ((EntityInternal)entity).setAttribute(ServiceStateLogic.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
+        }
+
+        TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
+                ConfigBag.newInstance()
+                    .configure(StopSoftwareParameters.STOP_PROCESS_MODE, stopProcessMode)
+                    .configure(StopSoftwareParameters.STOP_MACHINE_MODE, stopMachineMode)));
+        t1.asTask().get(10, TimeUnit.SECONDS);
+
+        if (MachineLifecycleEffectorTasksTest.canStop(stopProcessMode, isEntityStopped)) {
+            assertEquals(d.events, ImmutableList.of("stop"));
+        } else {
+            assertTrue(d.events.isEmpty());
+        }
+        if (MachineLifecycleEffectorTasksTest.canStop(stopMachineMode, machine == null)) {
+            assertTrue(entity.getLocations().isEmpty());
+            assertTrue(l.getAvailable().contains(machine));
+        } else {
+            assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine));
+            assertFalse(l.getAvailable().contains(machine));
+        }
+    }
+
+    @Test
+    public void testShutdownIsIdempotent() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        entity.start(ImmutableList.of(loc));
+        entity.stop();
+        
+        entity.stop();
+    }
+    
+    @Test
+    public void testReleaseEvenIfErrorDuringStart() throws Exception {
+        MyServiceImpl entity = new MyServiceImpl(app) {
+            @Override public Class<?> getDriverInterface() {
+                return SimulatedFailOnStartDriver.class;
+            }
+        };
+        Entities.manage(entity);
+        
+        try {
+            entity.start(ImmutableList.of(loc));
+            Assert.fail();
+        } catch (Exception e) {
+            IllegalStateException cause = Throwables2.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (cause == null || !cause.toString().contains("Simulating start error")) throw e; 
+        }
+        
+        try {
+            entity.stop();
+        } catch (Exception e) {
+            // Keep going
+            LOG.info("Error during stop, after simulating error during start", e);
+        }
+        Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine));
+        Entities.unmanage(entity);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public void doTestReleaseEvenIfErrorDuringStop(final Class driver) throws Exception {
+        MyServiceImpl entity = new MyServiceImpl(app) {
+            @Override public Class<?> getDriverInterface() {
+                return driver;
+            }
+        };
+        Entities.manage(entity);
+        
+        entity.start(ImmutableList.of(loc));
+        Task<Void> t = entity.invoke(Startable.STOP);
+        t.blockUntilEnded();
+        
+        assertFalse(t.isError(), "Expected parent to succeed, not fail with " + Tasks.getError(t));
+        Iterator<Task<?>> failures;
+        failures = Tasks.failed(Tasks.descendants(t, true)).iterator();
+        Assert.assertTrue(failures.hasNext(), "Expected error in descendants");
+        failures = Tasks.failed(Tasks.children(t)).iterator();
+        Assert.assertTrue(failures.hasNext(), "Expected error in child");
+        Throwable e = Tasks.getError(failures.next());
+        if (e == null || !e.toString().contains("Simulating stop error")) 
+            Assert.fail("Wrong error", e);
+
+        Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine), "Expected location to be available again");
+
+        Entities.unmanage(entity);
+    }
+
+    @Test
+    public void testReleaseEvenIfErrorDuringStop() throws Exception {
+        doTestReleaseEvenIfErrorDuringStop(SimulatedFailOnStopDriver.class);
+    }
+    
+    @Test
+    public void testReleaseEvenIfChildErrorDuringStop() throws Exception {
+        doTestReleaseEvenIfErrorDuringStop(SimulatedFailInChildOnStopDriver.class);
+    }
+
+    @Test
+    public void testDoubleStopEntity() {
+        ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory();
+        f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName());
+
+        // Second stop on SoftwareProcess will return early, while the first stop is still in progress
+        // This causes the app to shutdown prematurely, leaking machines.
+        EntityManager emgr = mgmt.getEntityManager();
+        EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class);
+        TestApplication app = emgr.createEntity(appSpec);
+        emgr.manage(app);
+        EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class);
+        Entity entity = app.createAndManageChild(latchEntitySpec);
+
+        final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class));
+        try {
+            app.start(ImmutableSet.of(loc));
+            EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+    
+            final Task<Void> firstStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of());
+            // Wait until first task tries to release the location, at this point the entity's reference 
+            // to the location is already cleared.
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(loc.isBlocked());
+                }
+            });
+    
+            // Subsequent stops will end quickly - no location to release,
+            // while the first one is still releasing the machine.
+            final Task<Void> secondStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of());;
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(secondStop.isDone());
+                }
+            });
+    
+            // Entity state is STOPPED even though first location is still releasing
+            EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
+            Asserts.succeedsContinually(new Runnable() {
+                @Override
+                public void run() {
+                    assertFalse(firstStop.isDone());
+                }
+            });
+
+            loc.unblock();
+
+            // After the location is released, first task ends as well.
+            EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(firstStop.isDone());
+                }
+            });
+
+        } finally {
+            loc.unblock();
+        }
+
+    }
+
+    @Test
+    public void testDoubleStopApp() {
+        ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory();
+        f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName());
+
+        // Second stop on SoftwareProcess will return early, while the first stop is still in progress
+        // This causes the app to shutdown prematurely, leaking machines.
+        EntityManager emgr = mgmt.getEntityManager();
+        EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class);
+        final TestApplication app = emgr.createEntity(appSpec);
+        emgr.manage(app);
+        EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class);
+        final Entity entity = app.createAndManageChild(latchEntitySpec);
+
+        final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class));
+        try {
+            app.start(ImmutableSet.of(loc));
+            EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+    
+            final Task<Void> firstStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());
+            // Wait until first task tries to release the location, at this point the entity's reference 
+            // to the location is already cleared.
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(loc.isBlocked());
+                }
+            });
+    
+            // Subsequent stops will end quickly - no location to release,
+            // while the first one is still releasing the machine.
+            final Task<Void> secondStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());;
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(secondStop.isDone());
+                }
+            });
+    
+            // Since second stop succeeded the app will get unmanaged.
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(!Entities.isManaged(entity));
+                    assertTrue(!Entities.isManaged(app));
+                }
+            });
+    
+            // Unmanage will cancel the first task
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    assertTrue(firstStop.isDone());
+                }
+            });
+        } finally {
+            // We still haven't unblocked the location release, but entity is already unmanaged.
+            // Double STOP on an application could leak locations!!!
+            loc.unblock();
+        }
+    }
+
+    @Test
+    public void testOpenPortsWithPortRangeConfig() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
+            .configure("http.port", "9999+"));
+        Assert.assertTrue(entity.getRequiredOpenPorts().contains(9999));
+    }
+
+    @ImplementedBy(MyServiceImpl.class)
+    public interface MyService extends SoftwareProcess {
+        PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT;
+        public SoftwareProcessDriver getDriver();
+        public Collection<Integer> getRequiredOpenPorts();
+    }
+
+    public static class MyServiceImpl extends SoftwareProcessImpl implements MyService {
+        public MyServiceImpl() {}
+        public MyServiceImpl(Entity parent) { super(parent); }
+
+        @Override
+        protected void initEnrichers() {
+            // Don't add enrichers messing with the SERVICE_UP state - we are setting it manually
+        }
+
+        @Override
+        public Class<?> getDriverInterface() {
+            return SimulatedDriver.class;
+        }
+
+        @Override
+        public Collection<Integer> getRequiredOpenPorts() {
+            return super.getRequiredOpenPorts();
+        }
+    }
+
+    @ImplementedBy(MyServiceWithVersionImpl.class)
+    public interface MyServiceWithVersion extends MyService {
+        public static ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "9.9.9");
+    }
+
+    public static class MyServiceWithVersionImpl extends MyServiceImpl implements MyServiceWithVersion {
+        public MyServiceWithVersionImpl() {}
+        public MyServiceWithVersionImpl(Entity parent) { super(parent); }
+    }
+
+    public static class SimulatedFailOnStartDriver extends SimulatedDriver {
+        public SimulatedFailOnStartDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        @Override
+        public void install() {
+            throw new IllegalStateException("Simulating start error");
+        }
+    }
+    
+    public static class SimulatedFailOnStopDriver extends SimulatedDriver {
+        public SimulatedFailOnStopDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        @Override
+        public void stop() {
+            throw new IllegalStateException("Simulating stop error");
+        }
+    }
+    
+    public static class SimulatedFailInChildOnStopDriver extends SimulatedDriver {
+        public SimulatedFailInChildOnStopDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        @Override
+        public void stop() {
+            DynamicTasks.queue(Tasks.fail("Simulating stop error in child", null));
+        }
+    }
+    
+    public static class SimulatedDriver extends AbstractSoftwareProcessSshDriver {
+        public List<String> events = new ArrayList<String>();
+        private volatile boolean launched = false;
+        
+        public SimulatedDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+
+        @Override
+        public boolean isRunning() {
+            return launched;
+        }
+    
+        @Override
+        public void stop() {
+            events.add("stop");
+            launched = false;
+            entity.setAttribute(Startable.SERVICE_UP, false);
+            entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
+        }
+    
+        @Override
+        public void kill() {
+            events.add("kill");
+            launched = false;
+            entity.setAttribute(Startable.SERVICE_UP, false);
+        }
+    
+        @Override
+        public void install() {
+            events.add("install");
+            entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+        }
+    
+        @Override
+        public void customize() {
+            events.add("customize");
+        }
+    
+        @Override
+        public void launch() {
+            events.add("launch");
+            launched = true;
+            entity.setAttribute(Startable.SERVICE_UP, true);
+            entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        }
+
+        @Override
+        public void setup() {
+            events.add("setup");
+        }
+
+        @Override
+        public void copyInstallResources() {
+            events.add("copyInstallResources");
+        }
+
+        @Override
+        public void copyRuntimeResources() {
+            events.add("copyRuntimeResources");
+        }
+
+        @Override
+        public void runPreInstallCommand(String command) { }
+
+        @Override
+        public void runPostInstallCommand(String command) { }
+
+        @Override
+        public void runPreLaunchCommand(String command) { }
+
+        @Override
+        public void runPostLaunchCommand(String command) { }
+
+        @Override
+        protected String getInstallLabelExtraSalt() {
+            return (String)getEntity().getConfigRaw(ConfigKeys.newStringConfigKey("salt"), true).or((String)null);
+        }
+    }
+
+    public static class ReleaseLatchLocation extends SimulatedLocation {
+        private static final long serialVersionUID = 1L;
+        
+        private CountDownLatch lock = new CountDownLatch(1);
+        private volatile boolean isBlocked;
+
+        public void unblock() {
+            lock.countDown();
+        }
+        @Override
+        public void release(MachineLocation machine) {
+            super.release(machine);
+            try {
+                isBlocked = true;
+                lock.await();
+                isBlocked = false;
+            } catch (InterruptedException e) {
+                throw Exceptions.propagate(e);
+            }
+        }
+
+        public boolean isBlocked() {
+            return isBlocked;
+        }
+
+    }
+
+    public static class MinimalEmptySoftwareProcessTestDriver implements EmptySoftwareProcessDriver {
+
+        private EmptySoftwareProcessImpl entity;
+        private Location location;
+
+        public MinimalEmptySoftwareProcessTestDriver(EmptySoftwareProcessImpl entity, Location location) {
+            this.entity = entity;
+            this.location = location;
+        }
+
+        @Override
+        public Location getLocation() {
+            return location;
+        }
+
+        @Override
+        public EntityLocal getEntity() {
+            return entity;
+        }
+
+        @Override
+        public boolean isRunning() {
+            return true;
+        }
+
+        @Override
+        public void rebind() {
+        }
+
+        @Override
+        public void start() {
+        }
+
+        @Override
+        public void restart() {
+        }
+
+        @Override
+        public void stop() {
+        }
+
+        @Override
+        public void kill() {
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java
new file mode 100644
index 0000000..1b270b9
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java
@@ -0,0 +1,389 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.core.BrooklynConfigKeys;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.factory.ApplicationBuilder;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
+import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
+import org.apache.brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.BrooklynNetworkUtils;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.stream.KnownSizeInputStream;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.yaml.Yamls;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+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 com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteSource;
+import com.google.common.io.Files;
+
+
+public class SoftwareProcessSshDriverIntegrationTest {
+
+    private LocalManagementContext managementContext;
+    private LocalhostMachineProvisioningLocation localhost;
+    private SshMachineLocation machine127;
+    private TestApplication app;
+    private File tempDataDir;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        tempDataDir = Files.createTempDir();
+        managementContext = new LocalManagementContext();
+        
+        localhost = managementContext.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+        machine127 = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "localhost"));
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+        if (tempDataDir != null) Os.deleteRecursively(tempDataDir);
+    }
+
+    // Integration test because requires ssh'ing (and takes about 5 seconds)
+    // See also SoftwareProcessEntityTest.testCustomInstallDirX for a lot more mocked variants
+    @Test(groups="Integration")
+    public void testCanInstallMultipleVersionsOnSameMachine() throws Exception {
+        managementContext.getBrooklynProperties().put(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
+                .configure(SoftwareProcess.SUGGESTED_VERSION, "0.1.0"));
+        MyService entity2 = app.createAndManageChild(EntitySpec.create(MyService.class)
+                .configure(SoftwareProcess.SUGGESTED_VERSION, "0.2.0"));
+        app.start(ImmutableList.of(machine127));
+        
+        String installDir1 = entity.getAttribute(SoftwareProcess.INSTALL_DIR);
+        String installDir2 = entity2.getAttribute(SoftwareProcess.INSTALL_DIR);
+        
+        assertNotEquals(installDir1, installDir2);
+        assertTrue(installDir1.contains("0.1.0"), "installDir1="+installDir1);
+        assertTrue(installDir2.contains("0.2.0"), "installDir2="+installDir2);
+        assertTrue(new File(new File(installDir1), "myfile").isFile());
+        assertTrue(new File(new File(installDir2), "myfile").isFile());
+    }
+
+    @Test(groups="Integration")
+    public void testLocalhostInTmp() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(localhost));
+
+        String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR);
+        assertTrue(installDir.startsWith("/tmp/brooklyn-"+Os.user()+"/installs/"), "installed in "+installDir);
+    }
+
+    @Test(groups="Integration")
+    public void testMachine127InHome() throws Exception {
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(machine127));
+
+        String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR);
+        assertTrue(installDir.startsWith(Os.home()+"/brooklyn-managed-processes/installs/"), "installed in "+installDir);
+    }
+
+    @Test(groups="Integration")
+    public void testLocalhostInCustom() throws Exception {
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(localhost));
+
+        String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR);
+        assertTrue(installDir.startsWith(tempDataDir.getAbsolutePath()+"/installs/"), "installed in "+installDir);
+    }
+
+    @Test(groups="Integration")
+    @Deprecated
+    public void testMachineInCustomFromDataDir() throws Exception {
+        managementContext.getBrooklynProperties().put(BrooklynConfigKeys.BROOKLYN_DATA_DIR, tempDataDir.getAbsolutePath());
+
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(machine127));
+
+        String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR);
+        assertTrue(installDir.startsWith(tempDataDir.getAbsolutePath()+"/installs/"), "installed in "+installDir);
+    }
+
+    @Test(groups="Integration")
+    public void testCopyResource() throws Exception {
+        File tempDest = new File(tempDataDir, "tempDest.txt");
+        String tempLocalContent = "abc";
+        File tempLocal = new File(tempDataDir, "tempLocal.txt");
+        Files.write(tempLocalContent, tempLocal, Charsets.UTF_8);
+        
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(localhost));
+
+        // Copy local file
+        entity.getDriver().copyResource(tempLocal, tempDest.getAbsolutePath());
+        assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        tempDest.delete();
+        
+        // Copy local file using url
+        entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath());
+        assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        tempDest.delete();
+        
+        // Copy reader
+        entity.getDriver().copyResource(new StringReader(tempLocalContent), tempDest.getAbsolutePath());
+        assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        tempDest.delete();
+        
+        // Copy stream
+        entity.getDriver().copyResource(ByteSource.wrap(tempLocalContent.getBytes()).openStream(), tempDest.getAbsolutePath());
+        assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        tempDest.delete();
+        
+        // Copy known-size stream
+        entity.getDriver().copyResource(new KnownSizeInputStream(Streams.newInputStreamWithContents(tempLocalContent), tempLocalContent.length()), tempDest.getAbsolutePath());
+        assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        tempDest.delete();
+    }
+
+    @Test(groups="Integration")
+    public void testCopyResourceCreatingParentDir() throws Exception {
+        /*
+         * TODO copyResource will now always create the parent dir, irrespective of the createParentDir value!
+         * In SshMachineLocation on 2014-05-29, Alex added: mkdir -p `dirname '$DEST'`
+         * 
+         * Changing this test to assert that parent dir always created; should we delete boolean createParentDir
+         * from the copyResource method?
+         * 
+         * TODO Have also deleted test that if relative path is given it will write that relative to $RUN_DIR.
+         * That is not the case: it is relative to $HOME, which seems fine. For example, if copyResource
+         * is used during install phase then $RUN_DIR would be the wrong default. 
+         * Is there any code that relies on this behaviour?
+         */
+        File tempDataDirSub = new File(tempDataDir, "subdir");
+        File tempDest = new File(tempDataDirSub, "tempDest.txt");
+        String tempLocalContent = "abc";
+        File tempLocal = new File(tempDataDir, "tempLocal.txt");
+        Files.write(tempLocalContent, tempLocal, Charsets.UTF_8);
+        
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+
+        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
+        app.start(ImmutableList.of(localhost));
+
+        // First confirm that even if createParentDir==false that it still gets created!
+        try {
+            entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath(), false);
+            assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        } finally {
+            Os.deleteRecursively(tempDataDirSub);
+        }
+
+        // Copy to absolute path
+        try {
+            entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath(), true);
+            assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent));
+        } finally {
+            Os.deleteRecursively(tempDataDirSub);
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testPreAndPostLaunchCommands() throws IOException {
+        File tempFile = new File(tempDataDir, "tempFile.txt");
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+        app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "")
+                .configure(SoftwareProcess.PRE_LAUNCH_COMMAND, String.format("echo inPreLaunch >> %s", tempFile.getAbsoluteFile()))
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, String.format("echo inLaunch >> %s", tempFile.getAbsoluteFile()))
+                .configure(SoftwareProcess.POST_LAUNCH_COMMAND, String.format("echo inPostLaunch >> %s", tempFile.getAbsoluteFile())));
+        app.start(ImmutableList.of(localhost));
+
+        List<String> output = Files.readLines(tempFile, Charsets.UTF_8);
+        assertEquals(output.size(), 3);
+        assertEquals(output.get(0), "inPreLaunch");
+        assertEquals(output.get(1), "inLaunch");
+        assertEquals(output.get(2), "inPostLaunch");
+        tempFile.delete();
+    }
+
+    @Test(groups="Integration")
+    public void testInstallResourcesCopy() throws IOException {
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+        File template = new File(Os.tmp(), "template.yaml");
+        VanillaSoftwareProcess entity = app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "")
+                .configure(SoftwareProcess.INSTALL_FILES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/frogs.txt", "frogs.txt"))
+                .configure(SoftwareProcess.INSTALL_TEMPLATES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/template.yaml", template.getAbsolutePath()))
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "date"));
+        app.start(ImmutableList.of(localhost));
+
+        File frogs = new File(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "frogs.txt");
+        try {
+            Assert.assertTrue(frogs.canRead(), "File not readable: " + frogs);
+            String output = Files.toString(frogs, Charsets.UTF_8);
+            Assert.assertTrue(output.contains("Brekekekex"), "File content not found: " + output);
+        } finally {
+            frogs.delete();
+        }
+
+        try {
+            String expectedHostname = BrooklynNetworkUtils.getLocalhostInetAddress().getHostName();
+            String expectedIp = BrooklynNetworkUtils.getLocalhostInetAddress().getHostAddress();
+            
+            Map<?,?> data = (Map) Iterables.getOnlyElement(Yamls.parseAll(Files.toString(template, Charsets.UTF_8)));
+            Assert.assertEquals(data.size(), 3);
+            Assert.assertEquals(data.get("entity.hostname"), expectedHostname);
+            Assert.assertEquals(data.get("entity.address"), expectedIp);
+            Assert.assertEquals(data.get("frogs"), Integer.valueOf(12));
+        } finally {
+            template.delete();
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testRuntimeResourcesCopy() throws IOException {
+        localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath());
+        File template = new File(Os.tmp(), "template.yaml");
+        VanillaSoftwareProcess entity = app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "")
+                .configure(SoftwareProcess.RUNTIME_FILES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/frogs.txt", "frogs.txt"))
+                .configure(SoftwareProcess.RUNTIME_TEMPLATES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/template.yaml", template.getAbsolutePath()))
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "date"));
+        app.start(ImmutableList.of(localhost));
+
+        File frogs = new File(entity.getAttribute(SoftwareProcess.RUN_DIR), "frogs.txt");
+        try {
+            Assert.assertTrue(frogs.canRead(), "File not readable: " + frogs);
+            String output = Files.toString(frogs, Charsets.UTF_8);
+            Assert.assertTrue(output.contains("Brekekekex"), "File content not found: " + output);
+        } finally {
+            frogs.delete();
+        }
+
+        try {
+            String expectedHostname = BrooklynNetworkUtils.getLocalhostInetAddress().getHostName();
+            String expectedIp = BrooklynNetworkUtils.getLocalhostInetAddress().getHostAddress();
+            
+            Map<?,?> data = (Map) Iterables.getOnlyElement(Yamls.parseAll(Files.toString(template, Charsets.UTF_8)));
+            Assert.assertEquals(data.size(), 3);
+            Assert.assertEquals(data.get("entity.hostname"), expectedHostname);
+            Assert.assertEquals(data.get("entity.address"), expectedIp);
+            Assert.assertEquals(data.get("frogs"), Integer.valueOf(12));
+        } finally {
+            template.delete();
+        }
+    }
+
+    @ImplementedBy(MyServiceImpl.class)
+    public interface MyService extends SoftwareProcess {
+        public SimulatedDriver getDriver();
+    }
+    
+    public static class MyServiceImpl extends SoftwareProcessImpl implements MyService {
+        public MyServiceImpl() {
+        }
+
+        @Override
+        public Class<?> getDriverInterface() {
+            return SimulatedDriver.class;
+        }
+        
+        @Override
+        public SimulatedDriver getDriver() {
+            return (SimulatedDriver) super.getDriver();
+        }
+    }
+
+    public static class SimulatedDriver extends AbstractSoftwareProcessSshDriver {
+        public List<String> events = new ArrayList<String>();
+        private volatile boolean launched = false;
+        
+        public SimulatedDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+
+        @Override
+        public void install() {
+            events.add("install");
+            newScript(INSTALLING)
+                    .failOnNonZeroResultCode()
+                    .body.append("touch myfile")
+                    .execute();
+        }
+        
+        @Override
+        public void customize() {
+            events.add("customize");
+        }
+    
+        @Override
+        public void launch() {
+            events.add("launch");
+            launched = true;
+            entity.setAttribute(Startable.SERVICE_UP, true);
+        }
+        
+        @Override
+        public boolean isRunning() {
+            return launched;
+        }
+    
+        @Override
+        public void stop() {
+            events.add("stop");
+            launched = false;
+            entity.setAttribute(Startable.SERVICE_UP, false);
+        }
+    
+        @Override
+        public void kill() {
+            events.add("kill");
+            launched = false;
+            entity.setAttribute(Startable.SERVICE_UP, false);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java
new file mode 100644
index 0000000..338b1f3
--- /dev/null
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.effector.core.EffectorAndBody;
+import org.apache.brooklyn.effector.core.MethodEffector;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+
+public class SoftwareProcessSubclassTest extends BrooklynAppUnitTestSupport {
+
+//  NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves.
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessSubclassTest.class);
+
+    @ImplementedBy(SubSoftwareProcessImpl.class)
+    public static interface SubSoftwareProcess extends EmptySoftwareProcess {
+        public List<String> getCallHistory();
+        public void triggerStopOutsideOfEffector();
+        public void customRestart();
+    }
+    
+    public static class SubSoftwareProcessImpl extends EmptySoftwareProcessImpl implements SubSoftwareProcess {
+        
+        protected List<String> callHistory = Collections.synchronizedList(Lists.<String>newArrayList());
+
+        @Override
+        public void init() {
+            super.init();
+            getMutableEntityType().addEffector(new EffectorAndBody<Void>(SoftwareProcess.RESTART, new MethodEffector<Void>(SubSoftwareProcess.class, "customRestart").getBody()));
+        }
+        
+        @Override
+        public List<String> getCallHistory() {
+            return callHistory;
+        }
+
+        @Override
+        public void preStart() {
+            callHistory.add("doStart");
+            super.preStart();
+        }
+        
+        @Override
+        public void preStop() {
+            callHistory.add("doStop");
+            super.preStop();
+        }
+        
+        @Override
+        public void customRestart() {
+            callHistory.add("doRestart");
+        }
+        
+        @Override
+        public void triggerStopOutsideOfEffector() {
+            stop();
+        }
+        
+    }
+    
+    private Location loc;
+    private List<Location> locs;
+    private SubSoftwareProcess entity;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = mgmt.getLocationRegistry().resolve("localhost");
+        locs = ImmutableList.of(loc);
+        entity = app.createAndManageChild(EntitySpec.create(SubSoftwareProcess.class));
+    }
+
+    @Test
+    public void testStartCalledViaMethod() throws Exception {
+        entity.start(locs);
+        
+        assertCallHistory(ImmutableList.of("doStart"));
+    }
+
+    @Test
+    public void testStopCalledViaMethod() throws Exception {
+        app.start(locs);
+        entity.stop();
+        
+        assertCallHistory(ImmutableList.of("doStart", "doStop"));
+    }
+
+    @Test
+    public void testRestartCalledViaMethod() throws Exception {
+        app.start(locs);
+        entity.restart();
+        
+        assertCallHistory(ImmutableList.of("doStart", "doRestart"));
+    }
+    
+    @Test
+    public void testStopCalledWithoutEffector() throws Exception {
+        app.start(locs);
+        entity.triggerStopOutsideOfEffector();
+        
+        assertCallHistory(ImmutableList.of("doStart", "doStop"));
+    }
+
+    @Test
+    public void testStartCalledViaInvokeEffector() throws Exception {
+        entity.invoke(SubSoftwareProcess.START, ImmutableMap.<String,Object>of("locations", locs)).get();
+        
+        assertCallHistory(ImmutableList.of("doStart"));
+    }
+
+    @Test
+    public void testStopCalledViaInvokeEffector() throws Exception {
+        app.start(locs);
+        entity.invoke(SubSoftwareProcess.STOP, ImmutableMap.<String,Object>of()).get();
+        
+        assertCallHistory(ImmutableList.of("doStart", "doStop"));
+    }
+
+    @Test
+    public void testRestartCalledViaInvokeEffector() throws Exception {
+        app.start(locs);
+        entity.invoke(SubSoftwareProcess.RESTART, ImmutableMap.<String,Object>of()).get();
+        
+        assertCallHistory(ImmutableList.of("doStart", "doRestart"));
+    }
+    
+    private void assertCallHistory(Iterable<String> expected) {
+        List<String> actual = entity.getCallHistory();
+        assertEquals(actual, ImmutableList.copyOf(expected), "actual="+actual);
+    }
+}