brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [2/3] brooklyn-server git commit: Rename SimpleShellCommandTest to TestSshCommand
Date Tue, 07 Jun 2016 14:38:54 GMT
Rename SimpleShellCommandTest to TestSshCommand

Deprecates on SimpleShellCommandTest (extending TestSshCommand).

Re-writes tests for TestSshCommand to use a stubbed SshTool, so
can write it as a unit test.


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/cef5d65c
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/cef5d65c
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/cef5d65c

Branch: refs/heads/master
Commit: cef5d65c86debee8ef12b55c95444febab4e4dc2
Parents: 19e63db
Author: Aled Sage <aled.sage@gmail.com>
Authored: Tue Jun 7 11:52:00 2016 +0100
Committer: Aled Sage <aled.sage@gmail.com>
Committed: Tue Jun 7 15:03:31 2016 +0100

----------------------------------------------------------------------
 .../core/internal/ssh/RecordingSshTool.java     |  63 +++-
 .../test/framework/SimpleShellCommandTest.java  |  77 +----
 .../framework/SimpleShellCommandTestImpl.java   | 242 +--------------
 .../brooklyn/test/framework/TestSshCommand.java | 101 ++++++
 .../test/framework/TestSshCommandImpl.java      | 258 ++++++++++++++++
 ...leShellCommandDeprecatedIntegrationTest.java | 292 ++++++++++++++++++
 .../SimpleShellCommandIntegrationTest.java      | 288 -----------------
 .../TestSshCommandIntegrationTest.java          | 157 ++++++++++
 .../test/framework/TestSshCommandTest.java      | 309 +++++++++++++++++++
 9 files changed, 1187 insertions(+), 600 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
index b31299d..a2a6764 100644
--- a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
+++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
@@ -18,26 +18,49 @@
 */
 package org.apache.brooklyn.util.core.internal.ssh;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Strings;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 
 /** Mock tool */
 public class RecordingSshTool implements SshTool {
     
+    public static class CustomResponse {
+        public final int exitCode;
+        public final String stdout;
+        public final String stderr;
+        
+        public CustomResponse(int exitCode, String stdout, String stderr) {
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+        }
+        
+        @Override
+        public String toString() {
+            return "ExecCmd["+exitCode+"; "+stdout+"; "+stderr+"]";
+        }
+    }
+    
     public static class ExecCmd {
         public final Map<String,?> props;
         public final String summaryForLogging;
         public final List<String> commands;
         public final Map<?,?> env;
         
-        ExecCmd(Map<String,?> props, String summaryForLogging, List<String> commands, Map env) {
+        ExecCmd(Map<String,?> props, String summaryForLogging, List<String> commands, Map<?,?> env) {
             this.props = props;
             this.summaryForLogging = summaryForLogging;
             this.commands = commands;
@@ -52,12 +75,22 @@ public class RecordingSshTool implements SshTool {
     
     public static List<ExecCmd> execScriptCmds = Lists.newCopyOnWriteArrayList();
     public static List<Map<?,?>> constructorProps = Lists.newCopyOnWriteArrayList();
+    public static Map<String, CustomResponse> customResponses = Maps.newConcurrentMap();
     
     private boolean connected;
     
     public static void clear() {
         execScriptCmds.clear();
         constructorProps.clear();
+        customResponses.clear();
+    }
+    
+    public static void setCustomResponse(String cmd, CustomResponse response) {
+        customResponses.put(cmd, checkNotNull(response, "response"));
+    }
+    
+    public static ExecCmd getLastExecCmd() {
+        return execScriptCmds.get(execScriptCmds.size()-1);
     }
     
     public RecordingSshTool(Map<?,?> props) {
@@ -77,6 +110,13 @@ public class RecordingSshTool implements SshTool {
     }
     @Override public int execScript(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
         execScriptCmds.add(new ExecCmd(props, "", commands, env));
+        for (String cmd : commands) {
+            if (customResponses.containsKey(cmd)) {
+                CustomResponse response = customResponses.get(cmd);
+                writeCustomResponseStreams(props, response);
+                return response.exitCode;
+            }
+        }
         return 0;
     }
     @Override public int execScript(Map<String, ?> props, List<String> commands) {
@@ -84,6 +124,13 @@ public class RecordingSshTool implements SshTool {
     }
     @Override public int execCommands(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
         execScriptCmds.add(new ExecCmd(props, "", commands, env));
+        for (String cmd : commands) {
+            if (customResponses.containsKey(cmd)) {
+                CustomResponse response = customResponses.get(cmd);
+                writeCustomResponseStreams(props, response);
+                return response.exitCode;
+            }
+        }
         return 0;
     }
     @Override public int execCommands(Map<String, ?> props, List<String> commands) {
@@ -101,4 +148,16 @@ public class RecordingSshTool implements SshTool {
     @Override public int copyFromServer(Map<String, ?> props, String pathAndFileOnRemoteServer, File local) {
         return 0;
     }
+    protected void writeCustomResponseStreams(Map<String, ?> props, CustomResponse response) {
+        try {
+            if (Strings.isNonBlank(response.stdout) && props.get(SshTool.PROP_OUT_STREAM.getName()) != null) {
+                ((OutputStream)props.get(SshTool.PROP_OUT_STREAM.getName())).write(response.stdout.getBytes());
+            }
+            if (Strings.isNonBlank(response.stderr) && props.get(SshTool.PROP_ERR_STREAM.getName()) != null) {
+                ((OutputStream)props.get(SshTool.PROP_ERR_STREAM.getName())).write(response.stderr.getBytes());
+            }
+        } catch (IOException e) {
+            Exceptions.propagate(e);
+        }
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java b/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
index abd0aea..bda1000 100644
--- a/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
+++ b/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
@@ -18,83 +18,14 @@
  */
 package org.apache.brooklyn.test.framework;
 
-import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
-
-import java.util.Map;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
 import org.apache.brooklyn.api.entity.ImplementedBy;
-import org.apache.brooklyn.config.ConfigKey;
-import org.apache.brooklyn.core.config.ConfigKeys;
-import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
-import org.apache.brooklyn.entity.software.base.SoftwareProcess;
-import org.apache.brooklyn.util.core.flags.SetFromFlag;
 
 /**
  * Tests using a simple command execution.
+ * 
+ * @deprecated since 0.10.0; use {@link TestSshCommand}
  */
+@Deprecated
 @ImplementedBy(SimpleShellCommandTestImpl.class)
-public interface SimpleShellCommandTest extends BaseTest {
-
-    /**
-     * Default location for temporary files.
-     */
-    String TMP_DEFAULT = "/tmp";
-
-    /**
-     * Supply the command to invoke directly. Cannot be used together with {@link #DOWNLOAD_URL}.
-     */
-    @SetFromFlag(nullable = false)
-    ConfigKey<String> COMMAND = ConfigKeys.newConfigKey(String.class, "command", "Command to invoke");
-
-    /**
-     * Download a script to invoke. Cannot be used together with {@link #COMMAND}.
-     */
-    @SetFromFlag("downloadUrl")
-    AttributeSensorAndConfigKey<String, String> DOWNLOAD_URL = SoftwareProcess.DOWNLOAD_URL;
-
-    /**
-     * Where the script will be downloaded on the target machine.
-     */
-    @SetFromFlag("scriptDir")
-    ConfigKey<String> SCRIPT_DIR = newConfigKey("script.dir", "directory where downloaded scripts should be put", TMP_DEFAULT);
-
-    /**
-     * The working directory that the script will be run from on the target machine.
-     */
-    @SetFromFlag("runDir")
-    ConfigKey<String> RUN_DIR = newConfigKey(String.class, "run.dir", "directory where downloaded scripts should be run from");
-
-
-    /**
-     * If no assertions are configured in the test then the default is this assertion that exit status of the command
-     * is zero (successful).
-     */
-    Map<String, Object> DEFAULT_ASSERTION = ImmutableMap.<String,Object>of(TestFrameworkAssertions.EQUALS, 0);
-
-    /**
-     * Assertions on the exit code of the simple command.
-     *
-     * If not explicitly configured, the default assertion is a non-zero exit code.
-     */
-    @SetFromFlag("assertStatus")
-    ConfigKey<Object> ASSERT_STATUS = ConfigKeys.newConfigKey(Object.class, "assert.status", "Assertions on command exit code",
-        ImmutableList.<Map<String, Object>>of());
-
-    /**
-     * Assertions on the standard output of the command as a String.
-     */
-    @SetFromFlag("assertOut")
-    ConfigKey<Object> ASSERT_OUT = ConfigKeys.newConfigKey(Object.class, "assert.out", "Assertions on command standard output",
-        ImmutableList.<Map<String, Object>>of());
-
-    /**
-     * Assertions on the standard error of the command as a String.
-     */
-    @SetFromFlag("assertErr")
-    ConfigKey<Object> ASSERT_ERR = ConfigKeys.newConfigKey(Object.class, "assert.err", "Assertions on command standard error",
-        ImmutableList.<Map<String, Object>>of());
-
+public interface SimpleShellCommandTest extends TestSshCommand {
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
----------------------------------------------------------------------
diff --git a/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java b/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
index f523893..23546d7 100644
--- a/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
+++ b/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
@@ -18,241 +18,9 @@
  */
 package org.apache.brooklyn.test.framework;
 
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.ON_FIRE;
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.RUNNING;
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STARTING;
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STOPPED;
-import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.checkAssertions;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
-import static org.apache.brooklyn.util.text.Strings.isBlank;
-import static org.apache.brooklyn.util.text.Strings.isNonBlank;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.base.Joiner;
-import com.google.common.base.Splitter;
-import com.google.common.base.Suppliers;
-import com.google.common.collect.ImmutableMap;
-
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.mgmt.TaskFactory;
-import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
-import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
-import org.apache.brooklyn.core.location.Machines;
-import org.apache.brooklyn.location.ssh.SshMachineLocation;
-import org.apache.brooklyn.test.framework.TestFrameworkAssertions.AssertionSupport;
-import org.apache.brooklyn.util.collections.MutableList;
-import org.apache.brooklyn.util.core.task.DynamicTasks;
-import org.apache.brooklyn.util.core.task.ssh.SshTasks;
-import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
-import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.apache.brooklyn.util.text.Identifiers;
-import org.apache.brooklyn.util.text.Strings;
-import org.apache.brooklyn.util.time.Duration;
-
-// TODO assertions below should use TestFrameworkAssertions but that class needs to be improved to give better error messages
-public class SimpleShellCommandTestImpl extends TargetableTestComponentImpl implements SimpleShellCommandTest {
-
-    private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandTestImpl.class);
-    private static final int A_LINE = 80;
-    public static final String DEFAULT_NAME = "download.sh";
-    private static final String CD = "cd";
-
-    @Override
-    public void start(Collection<? extends Location> locations) {
-        setExpectedState(this, STARTING);
-        execute();
-    }
-
-    @Override
-    public void stop() {
-        LOG.debug("{} Stopping simple command", this);
-        setUpAndRunState(false, STOPPED);
-    }
-
-    @Override
-    public void restart() {
-        LOG.debug("{} Restarting simple command", this);
-        execute();
-    }
-
-    private void setUpAndRunState(boolean up, Lifecycle status) {
-        sensors().set(SERVICE_UP, up);
-        setExpectedState(this, status);
-    }
-
-    private static class Result {
-        int exitCode;
-        String stdout;
-        String stderr;
-        public Result(final ProcessTaskWrapper<Integer> job) {
-            exitCode = job.get();
-            stdout = job.getStdout().trim();
-            stderr = job.getStderr().trim();
-        }
-        public int getExitCode() {
-            return exitCode;
-        }
-        public String getStdout() {
-            return stdout;
-        }
-        public String getStderr() {
-            return stderr;
-        }
-    }
-
-    protected void handle(Result result) {
-        LOG.debug("{}, Result is {}\nwith output [\n{}\n] and error [\n{}\n]", new Object[] {
-            this, result.getExitCode(), shorten(result.getStdout()), shorten(result.getStderr())
-        });
-        ImmutableMap<String, Duration> flags = ImmutableMap.of("timeout", getConfig(TIMEOUT));
-        AssertionSupport support = new AssertionSupport();
-        checkAssertions(support, flags, exitCodeAssertions(), "exit code", Suppliers.ofInstance(result.getExitCode()));
-        checkAssertions(support, flags, getAssertions(this, ASSERT_OUT), "stdout", Suppliers.ofInstance(result.getStdout()));
-        checkAssertions(support, flags, getAssertions(this, ASSERT_ERR), "stderr", Suppliers.ofInstance(result.getStderr()));
-        support.validate();
-    }
-
-    private String shorten(String text) {
-        return Strings.maxlenWithEllipsis(text, A_LINE);
-    }
-
-    public void execute() {
-        try {
-            SshMachineLocation machineLocation =
-                Machines.findUniqueMachineLocation(resolveTarget().getLocations(), SshMachineLocation.class).get();
-            executeCommand(machineLocation);
-            setUpAndRunState(true, RUNNING);
-        } catch (Throwable t) {
-            setUpAndRunState(false, ON_FIRE);
-            throw Exceptions.propagate(t);
-        }
-    }
-
-    private void executeCommand(SshMachineLocation machineLocation) {
-
-        Result result = null;
-        String downloadUrl = getConfig(DOWNLOAD_URL);
-        String command = getConfig(COMMAND);
-
-        String downloadName = DOWNLOAD_URL.getName();
-        String commandName = COMMAND.getName();
-
-        if (!(isNonBlank(downloadUrl) ^ isNonBlank(command))) {
-            throw illegal("Must specify exactly one of", downloadName, "and", commandName);
-        }
-
-        if (isNonBlank(downloadUrl)) {
-            String scriptDir = getConfig(SCRIPT_DIR);
-            String scriptPath = calculateDestPath(downloadUrl, scriptDir);
-            result = executeDownloadedScript(machineLocation, downloadUrl, scriptPath);
-        }
-
-        if (isNonBlank(command)) {
-            result = executeShellCommand(machineLocation, command);
-        }
-
-        handle(result);
-    }
-
-    private Result executeDownloadedScript(SshMachineLocation machineLocation, String url, String scriptPath) {
-
-        TaskFactory<?> install = SshTasks.installFromUrl(ImmutableMap.<String, Object>of(), machineLocation, url, scriptPath);
-        DynamicTasks.queue(install);
-        DynamicTasks.waitForLast();
-
-        List<String> commands = new ArrayList<>();
-        commands.add("chmod u+x " + scriptPath);
-        maybeCdToRunDir(commands);
-        commands.add(scriptPath);
-
-        return runCommands(machineLocation, commands);
-    }
-
-    private Result executeShellCommand(SshMachineLocation machineLocation, String command) {
-
-        List<String> commands = new ArrayList<>();
-        maybeCdToRunDir(commands);
-        commands.add(command);
-
-        return runCommands(machineLocation, commands);
-    }
-
-    private void maybeCdToRunDir(List<String> commands) {
-        String runDir = getConfig(RUN_DIR);
-        if (!isBlank(runDir)) {
-            commands.add(CD + " " + runDir);
-        }
-    }
-
-    private Result runCommands(SshMachineLocation machine, List<String> commands) {
-        SshEffectorTasks.SshEffectorTaskFactory<Integer> etf = SshEffectorTasks.ssh(commands.toArray(new String[]{}))
-            .machine(machine);
-
-        ProcessTaskWrapper<Integer> job = DynamicTasks.queue(etf);
-        job.asTask().blockUntilEnded();
-        return new Result(job);
-    }
-
-
-
-    private IllegalArgumentException illegal(String message, String ...messages) {
-        return new IllegalArgumentException(Joiner.on(' ').join(this.toString() + ":", message, messages));
-    }
-
-    private String calculateDestPath(String url, String directory) {
-        try {
-            URL asUrl = new URL(url);
-            Iterable<String> path = Splitter.on("/").split(asUrl.getPath());
-            String scriptName = getLastPartOfPath(path, DEFAULT_NAME);
-            return Joiner.on("/").join(directory, "test-" + Identifiers.makeRandomId(8), scriptName);
-        } catch (MalformedURLException e) {
-            throw illegal("Malformed URL:", url);
-        }
-    }
-
-    private static String getLastPartOfPath(Iterable<String> path, String defaultName) {
-        MutableList<String> parts = MutableList.copyOf(path);
-        Collections.reverse(parts);
-        Iterator<String> it = parts.iterator();
-        String scriptName = null;
-
-        // strip any trailing "/" parts of URL
-        while (isBlank(scriptName) && it.hasNext()) {
-            scriptName = it.next();
-        }
-        if (isBlank(scriptName)) {
-            scriptName = defaultName;
-        }
-        return scriptName;
-    }
-    
-
-    private List<Map<String, Object>> exitCodeAssertions() {
-
-        List<Map<String, Object>> assertStatus = getAssertions(this, ASSERT_STATUS);
-        List<Map<String, Object>> assertOut = getAssertions(this, ASSERT_OUT);
-        List<Map<String, Object>> assertErr = getAssertions(this, ASSERT_ERR);
-
-        List<Map<String, Object>> result;
-        if (assertStatus.isEmpty() && assertOut.isEmpty() && assertErr.isEmpty()) {
-            Map<String, Object> shouldSucceed = DEFAULT_ASSERTION;
-            result = MutableList.of(shouldSucceed);
-        } else {
-            result = assertStatus;
-        }
-        return result;
-    }
-
+/**
+ * @deprecated since 0.10.0; use {@link TestSshCommand}
+ */
+@Deprecated
+public class SimpleShellCommandTestImpl extends TestSshCommandImpl implements SimpleShellCommandTest {
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommand.java
----------------------------------------------------------------------
diff --git a/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommand.java b/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommand.java
new file mode 100644
index 0000000..36b6ece
--- /dev/null
+++ b/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommand.java
@@ -0,0 +1,101 @@
+/*
+ * 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.test.framework;
+
+import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
+
+import java.util.Map;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+/**
+ * Tests ssh command execution, against the {@link org.apache.brooklyn.location.ssh.SshMachineLocation} 
+ * of the target entity.
+ */
+@ImplementedBy(TestSshCommandImpl.class)
+public interface TestSshCommand extends BaseTest {
+
+    /**
+     * Default location for temporary files.
+     */
+    String TMP_DEFAULT = "/tmp";
+
+    /**
+     * Supply the command to invoke directly. Cannot be used together with {@link #DOWNLOAD_URL}.
+     */
+    @SetFromFlag(nullable = false)
+    ConfigKey<String> COMMAND = ConfigKeys.newConfigKey(String.class, "command", "Command to invoke");
+
+    /**
+     * Download a script to invoke. Cannot be used together with {@link #COMMAND}.
+     */
+    @SetFromFlag("downloadUrl")
+    AttributeSensorAndConfigKey<String, String> DOWNLOAD_URL = SoftwareProcess.DOWNLOAD_URL;
+
+    /**
+     * Where the script will be downloaded on the target machine.
+     */
+    @SetFromFlag("scriptDir")
+    ConfigKey<String> SCRIPT_DIR = newConfigKey("script.dir", "directory where downloaded scripts should be put", TMP_DEFAULT);
+
+    /**
+     * The working directory that the script will be run from on the target machine.
+     */
+    @SetFromFlag("runDir")
+    ConfigKey<String> RUN_DIR = newConfigKey(String.class, "run.dir", "directory where downloaded scripts should be run from");
+
+
+    /**
+     * If no assertions are configured in the test then the default is this assertion that exit status of the command
+     * is zero (successful).
+     */
+    Map<String, Object> DEFAULT_ASSERTION = ImmutableMap.<String,Object>of(TestFrameworkAssertions.EQUALS, 0);
+
+    /**
+     * Assertions on the exit code of the simple command.
+     *
+     * If not explicitly configured, the default assertion is a non-zero exit code.
+     */
+    @SetFromFlag("assertStatus")
+    ConfigKey<Object> ASSERT_STATUS = ConfigKeys.newConfigKey(Object.class, "assert.status", "Assertions on command exit code",
+        ImmutableList.<Map<String, Object>>of());
+
+    /**
+     * Assertions on the standard output of the command as a String.
+     */
+    @SetFromFlag("assertOut")
+    ConfigKey<Object> ASSERT_OUT = ConfigKeys.newConfigKey(Object.class, "assert.out", "Assertions on command standard output",
+        ImmutableList.<Map<String, Object>>of());
+
+    /**
+     * Assertions on the standard error of the command as a String.
+     */
+    @SetFromFlag("assertErr")
+    ConfigKey<Object> ASSERT_ERR = ConfigKeys.newConfigKey(Object.class, "assert.err", "Assertions on command standard error",
+        ImmutableList.<Map<String, Object>>of());
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommandImpl.java
----------------------------------------------------------------------
diff --git a/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommandImpl.java b/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommandImpl.java
new file mode 100644
index 0000000..02b977f
--- /dev/null
+++ b/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSshCommandImpl.java
@@ -0,0 +1,258 @@
+/*
+ * 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.test.framework;
+
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.ON_FIRE;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.RUNNING;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STARTING;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STOPPED;
+import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.checkAssertions;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
+import static org.apache.brooklyn.util.text.Strings.isBlank;
+import static org.apache.brooklyn.util.text.Strings.isNonBlank;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.test.framework.TestFrameworkAssertions.AssertionSupport;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.ssh.SshTasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+
+// TODO assertions below should use TestFrameworkAssertions but that class needs to be improved to give better error messages
+public class TestSshCommandImpl extends TargetableTestComponentImpl implements TestSshCommand {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TestSshCommandImpl.class);
+    private static final int A_LINE = 80;
+    public static final String DEFAULT_NAME = "download.sh";
+    private static final String CD = "cd";
+
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        setExpectedState(this, STARTING);
+        execute();
+    }
+
+    @Override
+    public void stop() {
+        LOG.debug("{} Stopping simple command", this);
+        setUpAndRunState(false, STOPPED);
+    }
+
+    @Override
+    public void restart() {
+        LOG.debug("{} Restarting simple command", this);
+        execute();
+    }
+
+    private void setUpAndRunState(boolean up, Lifecycle status) {
+        sensors().set(SERVICE_UP, up);
+        setExpectedState(this, status);
+    }
+
+    private static class Result {
+        int exitCode;
+        String stdout;
+        String stderr;
+        public Result(final ProcessTaskWrapper<Integer> job) {
+            exitCode = job.get();
+            stdout = job.getStdout().trim();
+            stderr = job.getStderr().trim();
+        }
+        public int getExitCode() {
+            return exitCode;
+        }
+        public String getStdout() {
+            return stdout;
+        }
+        public String getStderr() {
+            return stderr;
+        }
+    }
+
+    protected void handle(Result result) {
+        LOG.debug("{}, Result is {}\nwith output [\n{}\n] and error [\n{}\n]", new Object[] {
+            this, result.getExitCode(), shorten(result.getStdout()), shorten(result.getStderr())
+        });
+        ImmutableMap<String, Duration> flags = ImmutableMap.of("timeout", getConfig(TIMEOUT));
+        AssertionSupport support = new AssertionSupport();
+        checkAssertions(support, flags, exitCodeAssertions(), "exit code", Suppliers.ofInstance(result.getExitCode()));
+        checkAssertions(support, flags, getAssertions(this, ASSERT_OUT), "stdout", Suppliers.ofInstance(result.getStdout()));
+        checkAssertions(support, flags, getAssertions(this, ASSERT_ERR), "stderr", Suppliers.ofInstance(result.getStderr()));
+        support.validate();
+    }
+
+    private String shorten(String text) {
+        return Strings.maxlenWithEllipsis(text, A_LINE);
+    }
+
+    public void execute() {
+        try {
+            SshMachineLocation machineLocation =
+                Machines.findUniqueMachineLocation(resolveTarget().getLocations(), SshMachineLocation.class).get();
+            executeCommand(machineLocation);
+            setUpAndRunState(true, RUNNING);
+        } catch (Throwable t) {
+            setUpAndRunState(false, ON_FIRE);
+            throw Exceptions.propagate(t);
+        }
+    }
+
+    private void executeCommand(SshMachineLocation machineLocation) {
+
+        Result result = null;
+        String downloadUrl = getConfig(DOWNLOAD_URL);
+        String command = getConfig(COMMAND);
+
+        String downloadName = DOWNLOAD_URL.getName();
+        String commandName = COMMAND.getName();
+
+        if (!(isNonBlank(downloadUrl) ^ isNonBlank(command))) {
+            throw illegal("Must specify exactly one of", downloadName, "and", commandName);
+        }
+
+        if (isNonBlank(downloadUrl)) {
+            String scriptDir = getConfig(SCRIPT_DIR);
+            String scriptPath = calculateDestPath(downloadUrl, scriptDir);
+            result = executeDownloadedScript(machineLocation, downloadUrl, scriptPath);
+        }
+
+        if (isNonBlank(command)) {
+            result = executeShellCommand(machineLocation, command);
+        }
+
+        handle(result);
+    }
+
+    private Result executeDownloadedScript(SshMachineLocation machineLocation, String url, String scriptPath) {
+
+        TaskFactory<?> install = SshTasks.installFromUrl(ImmutableMap.<String, Object>of(), machineLocation, url, scriptPath);
+        DynamicTasks.queue(install);
+        DynamicTasks.waitForLast();
+
+        List<String> commands = new ArrayList<>();
+        commands.add("chmod u+x " + scriptPath);
+        maybeCdToRunDir(commands);
+        commands.add(scriptPath);
+
+        return runCommands(machineLocation, commands);
+    }
+
+    private Result executeShellCommand(SshMachineLocation machineLocation, String command) {
+
+        List<String> commands = new ArrayList<>();
+        maybeCdToRunDir(commands);
+        commands.add(command);
+
+        return runCommands(machineLocation, commands);
+    }
+
+    private void maybeCdToRunDir(List<String> commands) {
+        String runDir = getConfig(RUN_DIR);
+        if (!isBlank(runDir)) {
+            commands.add(CD + " " + runDir);
+        }
+    }
+
+    private Result runCommands(SshMachineLocation machine, List<String> commands) {
+        SshEffectorTasks.SshEffectorTaskFactory<Integer> etf = SshEffectorTasks.ssh(commands.toArray(new String[]{}))
+            .machine(machine);
+
+        ProcessTaskWrapper<Integer> job = DynamicTasks.queue(etf);
+        job.asTask().blockUntilEnded();
+        return new Result(job);
+    }
+
+
+
+    private IllegalArgumentException illegal(String message, String ...messages) {
+        return new IllegalArgumentException(Joiner.on(' ').join(this.toString() + ":", message, messages));
+    }
+
+    private String calculateDestPath(String url, String directory) {
+        try {
+            URL asUrl = new URL(url);
+            Iterable<String> path = Splitter.on("/").split(asUrl.getPath());
+            String scriptName = getLastPartOfPath(path, DEFAULT_NAME);
+            return Joiner.on("/").join(directory, "test-" + Identifiers.makeRandomId(8), scriptName);
+        } catch (MalformedURLException e) {
+            throw illegal("Malformed URL:", url);
+        }
+    }
+
+    private static String getLastPartOfPath(Iterable<String> path, String defaultName) {
+        MutableList<String> parts = MutableList.copyOf(path);
+        Collections.reverse(parts);
+        Iterator<String> it = parts.iterator();
+        String scriptName = null;
+
+        // strip any trailing "/" parts of URL
+        while (isBlank(scriptName) && it.hasNext()) {
+            scriptName = it.next();
+        }
+        if (isBlank(scriptName)) {
+            scriptName = defaultName;
+        }
+        return scriptName;
+    }
+    
+
+    private List<Map<String, Object>> exitCodeAssertions() {
+
+        List<Map<String, Object>> assertStatus = getAssertions(this, ASSERT_STATUS);
+        List<Map<String, Object>> assertOut = getAssertions(this, ASSERT_OUT);
+        List<Map<String, Object>> assertErr = getAssertions(this, ASSERT_ERR);
+
+        List<Map<String, Object>> result;
+        if (assertStatus.isEmpty() && assertOut.isEmpty() && assertErr.isEmpty()) {
+            Map<String, Object> shouldSucceed = DEFAULT_ASSERTION;
+            result = MutableList.of(shouldSucceed);
+        } else {
+            result = assertStatus;
+        }
+        return result;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandDeprecatedIntegrationTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandDeprecatedIntegrationTest.java b/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandDeprecatedIntegrationTest.java
new file mode 100644
index 0000000..34ffc6d
--- /dev/null
+++ b/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandDeprecatedIntegrationTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.test.framework;
+
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.ASSERT_OUT;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.ASSERT_STATUS;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.COMMAND;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.DOWNLOAD_URL;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.RUN_DIR;
+import static org.apache.brooklyn.test.framework.TargetableTestComponent.TARGET_ENTITY;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.CONTAINS;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.EQUALS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * @deprecated since 0.10.0
+ */
+@Deprecated
+public class SimpleShellCommandDeprecatedIntegrationTest extends BrooklynAppUnitTestSupport {
+
+    private static final String UP = "up";
+
+    @DataProvider(name = "shouldInsistOnJustOneOfCommandAndScript")
+    public Object[][] createData1() {
+
+        return new Object[][] {
+            { "pwd", "pwd.sh", Boolean.FALSE },
+            { null, null, Boolean.FALSE },
+            { "pwd", null, Boolean.TRUE },
+            { null, "pwd.sh", Boolean.TRUE }
+        };
+    }
+
+    @Test(groups= "Integration", dataProvider = "shouldInsistOnJustOneOfCommandAndScript")
+    public void shouldInsistOnJustOneOfCommandAndScript(String command, String script, boolean valid) throws Exception {
+        Path scriptPath = null;
+        String scriptUrl = null;
+        if (null != script) {
+            scriptPath = createTempScript("pwd", "pwd");
+            scriptUrl = "file:" + scriptPath;
+        }
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, command)
+            .configure(DOWNLOAD_URL, scriptUrl));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            if (!valid) {
+                Asserts.shouldHaveFailedPreviously();
+            }
+
+        } catch (Exception e) {
+            Asserts.expectedFailureContains(e, "Must specify exactly one of download.url and command");
+
+        } finally {
+            if (null != scriptPath) {
+                Files.delete(scriptPath);
+            }
+        }
+    }
+
+    private List<Map<String, Object>> makeAssertions(Map<String, Object> ...maps) {
+        ArrayList<Map<String, Object>> assertions = new ArrayList<>();
+        for (Map<String, Object> map : maps) {
+            assertions.add(map);
+        }
+        return assertions;
+    }
+
+
+
+    @Test(groups = "Integration")
+    public void shouldSucceedUsingSuccessfulExitAsDefaultCondition() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime"));
+
+        app.start(ImmutableList.<Location>of());
+
+        assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
+            .withFailMessage("Service should be up");
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
+            .withFailMessage("Service should be marked running");
+    }
+
+
+    @Test(groups = "Integration")
+    public void shouldFailUsingSuccessfulExitAsDefaultCondition() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "ls /tmp/bogus-" + Identifiers.randomLong()));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+        } catch (Throwable t) {
+            Asserts.expectedFailureContains(t, "exit code equals 0");
+        }
+
+        assertThat(uptime.sensors().get(SERVICE_UP)).isFalse()
+            .withFailMessage("Service should be down");
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
+            .withFailMessage("Service should be marked on fire");
+    }
+
+
+
+    @Test(groups = "Integration")
+    public void shouldInvokeCommand() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        Map<String, Object> equalsZero = MutableMap.of();
+        equalsZero.put(EQUALS, 0);
+
+        Map<String, Object> containsUp = MutableMap.of();
+        containsUp.put(CONTAINS, UP);
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime")
+            .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+            .configure(ASSERT_OUT, makeAssertions(containsUp)));
+
+        app.start(ImmutableList.<Location>of());
+
+        assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
+            .withFailMessage("Service should be up");
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
+            .withFailMessage("Service should be marked running");
+
+    }
+
+    @Test(groups = "Integration")
+    public void shouldNotBeUpIfAssertionsFail() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        Map<String, Object> equalsOne = MutableMap.of();
+        equalsOne.put(EQUALS, 1);
+
+        Map<String, Object> equals255 = MutableMap.of();
+        equals255.put(EQUALS, 255);
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime")
+            .configure(ASSERT_STATUS, makeAssertions(equalsOne, equals255)));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+        } catch (Exception e) {
+            Asserts.expectedFailureContains(e, "exit code equals 1", "exit code equals 255");
+        }
+
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
+            .withFailMessage("Service should be marked on fire");
+
+    }
+
+    @Test(groups = "Integration")
+    public void shouldInvokeScript() throws Exception {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        String text = "hello world";
+        Path testScript = createTempScript("script", "echo " + text);
+
+        try {
+
+            Map<String, Object> equalsZero = MutableMap.of();
+            equalsZero.put(EQUALS, 0);
+
+            Map<String, Object> containsText = MutableMap.of();
+            containsText.put(CONTAINS, text);
+
+            SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(DOWNLOAD_URL, "file:" + testScript)
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+                .configure(ASSERT_OUT, makeAssertions(containsText)));
+
+            app.start(ImmutableList.<Location>of());
+
+            assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
+                .withFailMessage("Service should be up");
+            assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
+                .withFailMessage("Service should be marked running");
+
+        } finally {
+            Files.delete(testScript);
+        }
+    }
+
+    @Test(groups = "Integration")
+    public void shouldExecuteInTheRunDir() throws Exception {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
+
+        Path pwdPath = createTempScript("pwd", "pwd");
+
+        try {
+
+            Map<String, Object> equalsZero = MutableMap.of();
+            equalsZero.put(EQUALS, 0);
+
+            Map<String, Object> containsTmp = MutableMap.of();
+            containsTmp.put(CONTAINS, "/tmp");
+
+            SimpleShellCommandTest pwd = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(DOWNLOAD_URL, "file:" + pwdPath)
+                .configure(RUN_DIR, "/tmp")
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
+
+
+            SimpleShellCommandTest alsoPwd = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(COMMAND, "pwd")
+                .configure(RUN_DIR, "/tmp")
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
+
+            app.start(ImmutableList.<Location>of());
+
+            assertThat(pwd.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
+            assertThat(ServiceStateLogic.getExpectedState(pwd)).isEqualTo(Lifecycle.RUNNING)
+                .withFailMessage("Service should be marked running");
+
+            assertThat(alsoPwd.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
+            assertThat(ServiceStateLogic.getExpectedState(alsoPwd)).isEqualTo(Lifecycle.RUNNING)
+                .withFailMessage("Service should be marked running");
+
+        } finally {
+            Files.delete(pwdPath);
+        }
+    }
+
+    private Path createTempScript(String filename, String contents) {
+        try {
+            Path tempFile = Files.createTempFile("SimpleShellCommandIntegrationTest-" + filename, ".sh");
+            Files.write(tempFile, contents.getBytes());
+            return tempFile;
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java b/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
deleted file mode 100644
index 7e53d59..0000000
--- a/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * 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.test.framework;
-
-import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
-import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.ASSERT_OUT;
-import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.ASSERT_STATUS;
-import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.COMMAND;
-import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.DOWNLOAD_URL;
-import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.RUN_DIR;
-import static org.apache.brooklyn.test.framework.TargetableTestComponent.TARGET_ENTITY;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.CONTAINS;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.EQUALS;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
-import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
-import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
-import org.apache.brooklyn.core.test.entity.TestApplication;
-import org.apache.brooklyn.core.test.entity.TestEntity;
-import org.apache.brooklyn.test.Asserts;
-import org.apache.brooklyn.util.collections.MutableMap;
-import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.apache.brooklyn.util.text.Identifiers;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import com.google.common.collect.ImmutableList;
-
-public class SimpleShellCommandIntegrationTest extends BrooklynAppUnitTestSupport {
-
-    private static final String UP = "up";
-
-    @DataProvider(name = "shouldInsistOnJustOneOfCommandAndScript")
-    public Object[][] createData1() {
-
-        return new Object[][] {
-            { "pwd", "pwd.sh", Boolean.FALSE },
-            { null, null, Boolean.FALSE },
-            { "pwd", null, Boolean.TRUE },
-            { null, "pwd.sh", Boolean.TRUE }
-        };
-    }
-
-    @Test(groups= "Integration", dataProvider = "shouldInsistOnJustOneOfCommandAndScript")
-    public void shouldInsistOnJustOneOfCommandAndScript(String command, String script, boolean valid) throws Exception {
-        Path scriptPath = null;
-        String scriptUrl = null;
-        if (null != script) {
-            scriptPath = createTempScript("pwd", "pwd");
-            scriptUrl = "file:" + scriptPath;
-        }
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, command)
-            .configure(DOWNLOAD_URL, scriptUrl));
-
-        try {
-            app.start(ImmutableList.<Location>of());
-            if (!valid) {
-                Asserts.shouldHaveFailedPreviously();
-            }
-
-        } catch (Exception e) {
-            Asserts.expectedFailureContains(e, "Must specify exactly one of download.url and command");
-
-        } finally {
-            if (null != scriptPath) {
-                Files.delete(scriptPath);
-            }
-        }
-    }
-
-    private List<Map<String, Object>> makeAssertions(Map<String, Object> ...maps) {
-        ArrayList<Map<String, Object>> assertions = new ArrayList<>();
-        for (Map<String, Object> map : maps) {
-            assertions.add(map);
-        }
-        return assertions;
-    }
-
-
-
-    @Test(groups = "Integration")
-    public void shouldSucceedUsingSuccessfulExitAsDefaultCondition() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "uptime"));
-
-        app.start(ImmutableList.<Location>of());
-
-        assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
-            .withFailMessage("Service should be up");
-        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
-            .withFailMessage("Service should be marked running");
-    }
-
-
-    @Test(groups = "Integration")
-    public void shouldFailUsingSuccessfulExitAsDefaultCondition() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "ls /tmp/bogus-" + Identifiers.randomLong()));
-
-        try {
-            app.start(ImmutableList.<Location>of());
-        } catch (Throwable t) {
-            Asserts.expectedFailureContains(t, "exit code equals 0");
-        }
-
-        assertThat(uptime.sensors().get(SERVICE_UP)).isFalse()
-            .withFailMessage("Service should be down");
-        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
-            .withFailMessage("Service should be marked on fire");
-    }
-
-
-
-    @Test(groups = "Integration")
-    public void shouldInvokeCommand() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        Map<String, Object> equalsZero = MutableMap.of();
-        equalsZero.put(EQUALS, 0);
-
-        Map<String, Object> containsUp = MutableMap.of();
-        containsUp.put(CONTAINS, UP);
-
-        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "uptime")
-            .configure(ASSERT_STATUS, makeAssertions(equalsZero))
-            .configure(ASSERT_OUT, makeAssertions(containsUp)));
-
-        app.start(ImmutableList.<Location>of());
-
-        assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
-            .withFailMessage("Service should be up");
-        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
-            .withFailMessage("Service should be marked running");
-
-    }
-
-    @Test(groups = "Integration")
-    public void shouldNotBeUpIfAssertionsFail() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        Map<String, Object> equalsOne = MutableMap.of();
-        equalsOne.put(EQUALS, 1);
-
-        Map<String, Object> equals255 = MutableMap.of();
-        equals255.put(EQUALS, 255);
-
-        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "uptime")
-            .configure(ASSERT_STATUS, makeAssertions(equalsOne, equals255)));
-
-        try {
-            app.start(ImmutableList.<Location>of());
-        } catch (Exception e) {
-            Asserts.expectedFailureContains(e, "exit code equals 1", "exit code equals 255");
-        }
-
-        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
-            .withFailMessage("Service should be marked on fire");
-
-    }
-
-    @Test(groups = "Integration")
-    public void shouldInvokeScript() throws Exception {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        String text = "hello world";
-        Path testScript = createTempScript("script", "echo " + text);
-
-        try {
-
-            Map<String, Object> equalsZero = MutableMap.of();
-            equalsZero.put(EQUALS, 0);
-
-            Map<String, Object> containsText = MutableMap.of();
-            containsText.put(CONTAINS, text);
-
-            SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-                .configure(TARGET_ENTITY, testEntity)
-                .configure(DOWNLOAD_URL, "file:" + testScript)
-                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
-                .configure(ASSERT_OUT, makeAssertions(containsText)));
-
-            app.start(ImmutableList.<Location>of());
-
-            assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
-                .withFailMessage("Service should be up");
-            assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
-                .withFailMessage("Service should be marked running");
-
-        } finally {
-            Files.delete(testScript);
-        }
-    }
-
-    @Test(groups = "Integration")
-    public void shouldExecuteInTheRunDir() throws Exception {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC));
-
-        Path pwdPath = createTempScript("pwd", "pwd");
-
-        try {
-
-            Map<String, Object> equalsZero = MutableMap.of();
-            equalsZero.put(EQUALS, 0);
-
-            Map<String, Object> containsTmp = MutableMap.of();
-            containsTmp.put(CONTAINS, "/tmp");
-
-            SimpleShellCommandTest pwd = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-                .configure(TARGET_ENTITY, testEntity)
-                .configure(DOWNLOAD_URL, "file:" + pwdPath)
-                .configure(RUN_DIR, "/tmp")
-                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
-                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
-
-
-            SimpleShellCommandTest alsoPwd = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
-                .configure(TARGET_ENTITY, testEntity)
-                .configure(COMMAND, "pwd")
-                .configure(RUN_DIR, "/tmp")
-                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
-                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
-
-            app.start(ImmutableList.<Location>of());
-
-            assertThat(pwd.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
-            assertThat(ServiceStateLogic.getExpectedState(pwd)).isEqualTo(Lifecycle.RUNNING)
-                .withFailMessage("Service should be marked running");
-
-            assertThat(alsoPwd.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
-            assertThat(ServiceStateLogic.getExpectedState(alsoPwd)).isEqualTo(Lifecycle.RUNNING)
-                .withFailMessage("Service should be marked running");
-
-        } finally {
-            Files.delete(pwdPath);
-        }
-    }
-
-    private Path createTempScript(String filename, String contents) {
-        try {
-            Path tempFile = Files.createTempFile("SimpleShellCommandIntegrationTest-" + filename, ".sh");
-            Files.write(tempFile, contents.getBytes());
-            return tempFile;
-        } catch (IOException e) {
-            throw Exceptions.propagate(e);
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandIntegrationTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandIntegrationTest.java b/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandIntegrationTest.java
new file mode 100644
index 0000000..60c6aaf
--- /dev/null
+++ b/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandIntegrationTest.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.test.framework;
+
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.framework.TargetableTestComponent.TARGET_ENTITY;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.CONTAINS;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.EQUALS;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_ERR;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_OUT;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_STATUS;
+import static org.apache.brooklyn.test.framework.TestSshCommand.COMMAND;
+import static org.apache.brooklyn.test.framework.TestSshCommand.DOWNLOAD_URL;
+import static org.apache.brooklyn.test.framework.TestSshCommand.RUN_DIR;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestSshCommandIntegrationTest extends BrooklynAppUnitTestSupport {
+
+    private TestEntity testEntity;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .location(TestApplication.LOCALHOST_MACHINE_SPEC));
+    }
+
+    @Test(groups = "Integration")
+    public void shouldExecuteInTheRunDir() throws Exception {
+        Path pwdPath = createTempScript("pwd", "pwd");
+
+        try {
+            Map<String, ?> equalsZero = ImmutableMap.of(EQUALS, 0);
+            Map<String, ?> containsTmp = ImmutableMap.of(CONTAINS, "/tmp");
+
+            TestSshCommand testWithScript = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(DOWNLOAD_URL, "file:" + pwdPath)
+                .configure(RUN_DIR, "/tmp")
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
+
+
+            TestSshCommand testWithCmd = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(COMMAND, "pwd")
+                .configure(RUN_DIR, "/tmp")
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero))
+                .configure(ASSERT_OUT, makeAssertions(containsTmp)));
+
+            app.start(ImmutableList.<Location>of());
+
+            assertThat(testWithScript.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
+            assertThat(ServiceStateLogic.getExpectedState(testWithScript)).isEqualTo(Lifecycle.RUNNING)
+                .withFailMessage("Service should be marked running");
+
+            assertThat(testWithCmd.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
+            assertThat(ServiceStateLogic.getExpectedState(testWithCmd)).isEqualTo(Lifecycle.RUNNING)
+                .withFailMessage("Service should be marked running");
+
+        } finally {
+            Files.delete(pwdPath);
+        }
+    }
+
+    @Test(groups = "Integration")
+    public void shouldCaptureStdoutAndStderrOfCommands() {
+        TestSshCommand uptime = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "echo 'a' 'b' && CMDSUFFIX=Suffix && doesNotExist${CMDSUFFIX}")
+            .configure(ASSERT_OUT, makeAssertions(ImmutableMap.of(CONTAINS, "a b")))
+            .configure(ASSERT_ERR, makeAssertions(ImmutableMap.of(CONTAINS, "doesNotExistSuffix"))));
+
+        app.start(ImmutableList.<Location>of());
+
+        assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
+            .withFailMessage("Service should be up");
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
+            .withFailMessage("Service should be marked running");
+    }
+
+    @Test(groups = "Integration")
+    public void shouldCaptureStdoutAndStderrOfScript() throws Exception {
+        String text = "echo 'a' 'b' && CMDSUFFIX=Suffix && doesNotExist${CMDSUFFIX}";
+        Path testScript = createTempScript("script", "echo " + text);
+
+        try {
+            TestSshCommand uptime = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+                    .configure(TARGET_ENTITY, testEntity)
+                    .configure(DOWNLOAD_URL, "file:" + testScript)
+                    .configure(ASSERT_OUT, makeAssertions(ImmutableMap.of(CONTAINS, "a b")))
+                    .configure(ASSERT_ERR, makeAssertions(ImmutableMap.of(CONTAINS, "doesNotExistSuffix"))));
+
+                app.start(ImmutableList.<Location>of());
+
+                assertThat(uptime.sensors().get(SERVICE_UP)).isTrue()
+                    .withFailMessage("Service should be up");
+                assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.RUNNING)
+                    .withFailMessage("Service should be marked running");
+
+        } finally {
+            Files.delete(testScript);
+        }
+    }
+    
+    private Path createTempScript(String filename, String contents) {
+        try {
+            Path tempFile = Files.createTempFile("SimpleShellCommandIntegrationTest-" + filename, ".sh");
+            Files.write(tempFile, contents.getBytes());
+            return tempFile;
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    private List<Map<String, ?>> makeAssertions(Map<String, ?> map) {
+        return ImmutableList.<Map<String, ?>>of(map);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/cef5d65c/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandTest.java b/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandTest.java
new file mode 100644
index 0000000..4104e97
--- /dev/null
+++ b/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestSshCommandTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.test.framework;
+
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.framework.TargetableTestComponent.TARGET_ENTITY;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.CONTAINS;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.EQUALS;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_ERR;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_OUT;
+import static org.apache.brooklyn.test.framework.TestSshCommand.ASSERT_STATUS;
+import static org.apache.brooklyn.test.framework.TestSshCommand.COMMAND;
+import static org.apache.brooklyn.test.framework.TestSshCommand.DOWNLOAD_URL;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+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.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestSshCommandTest extends BrooklynAppUnitTestSupport {
+
+    private TestEntity testEntity;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        LocationSpec<SshMachineLocation> machineSpec = LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "1.2.3.4")
+                .configure("sshToolClass", RecordingSshTool.class.getName());
+        testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .location(machineSpec));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        RecordingSshTool.clear();
+    }
+    
+    @DataProvider(name = "shouldInsistOnJustOneOfCommandAndScript")
+    public Object[][] createData1() {
+        return new Object[][] {
+                { "pwd", "pwd.sh", Boolean.FALSE },
+                { null, null, Boolean.FALSE },
+                { "pwd", null, Boolean.TRUE },
+                { null, "pwd.sh", Boolean.TRUE }
+        };
+    }
+
+    @Test(dataProvider = "shouldInsistOnJustOneOfCommandAndScript")
+    public void shouldInsistOnJustOneOfCommandAndScript(String command, String script, boolean valid) throws Exception {
+        Path scriptPath = null;
+        String scriptUrl = null;
+        if (null != script) {
+            scriptPath = createTempScript("pwd", "pwd");
+            scriptUrl = "file:" + scriptPath;
+        }
+
+        try {
+            app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+                    .configure(TARGET_ENTITY, testEntity)
+                    .configure(COMMAND, command)
+                    .configure(DOWNLOAD_URL, scriptUrl));
+
+            app.start(ImmutableList.<Location>of());
+            if (!valid) {
+                Asserts.shouldHaveFailedPreviously();
+            }
+
+        } catch (Exception e) {
+            Asserts.expectedFailureContains(e, "Must specify exactly one of download.url and command");
+
+        } finally {
+            if (null != scriptPath) {
+                Files.delete(scriptPath);
+            }
+        }
+    }
+
+    @Test
+    public void shouldSucceedUsingSuccessfulExitAsDefaultCondition() {
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime"));
+
+        app.start(ImmutableList.<Location>of());
+
+        assertServiceHealthy(test);
+        assertThat(RecordingSshTool.getLastExecCmd().commands).isEqualTo(ImmutableList.of("uptime"));
+    }
+
+    @Test
+    public void shouldFailUsingSuccessfulExitAsDefaultCondition() {
+        String cmd = "commandExpectedToFail-" + Identifiers.randomLong();
+        RecordingSshTool.setCustomResponse(cmd, new RecordingSshTool.CustomResponse(1, null, null));
+        
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, cmd));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            Asserts.shouldHaveFailedPreviously();
+        } catch (Throwable t) {
+            Asserts.expectedFailureContains(t, "exit code equals 0");
+        }
+
+        assertServiceFailed(test);
+        assertThat(RecordingSshTool.getLastExecCmd().commands).isEqualTo(ImmutableList.of(cmd));
+    }
+
+    @Test
+    public void shouldMatchStdoutAndStderr() {
+        String cmd = "stdoutAndStderr-" + Identifiers.randomLong();
+        RecordingSshTool.setCustomResponse(cmd, new RecordingSshTool.CustomResponse(0, "mystdout", "mystderr"));
+        
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, cmd)
+            .configure(ASSERT_OUT, makeAssertions(ImmutableMap.of(CONTAINS, "mystdout")))
+            .configure(ASSERT_ERR, makeAssertions(ImmutableMap.of(CONTAINS, "mystderr"))));
+
+        app.start(ImmutableList.<Location>of());
+
+        assertServiceHealthy(test);
+    }
+
+    @Test
+    public void shouldFailOnUnmatchedStdout() {
+        String cmd = "stdoutAndStderr-" + Identifiers.randomLong();
+        RecordingSshTool.setCustomResponse(cmd, new RecordingSshTool.CustomResponse(0, "wrongstdout", null));
+        
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, cmd)
+            .configure(ASSERT_OUT, makeAssertions(ImmutableMap.of(CONTAINS, "mystdout"))));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            Asserts.shouldHaveFailedPreviously();
+        } catch (Throwable t) {
+            Asserts.expectedFailureContains(t, "stdout contains mystdout");
+        }
+
+        assertServiceFailed(test);
+    }
+
+    @Test
+    public void shouldFailOnUnmatchedStderr() {
+        String cmd = "stdoutAndStderr-" + Identifiers.randomLong();
+        RecordingSshTool.setCustomResponse(cmd, new RecordingSshTool.CustomResponse(0, null, "wrongstderr"));
+        
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, cmd)
+            .configure(ASSERT_ERR, makeAssertions(ImmutableMap.of(CONTAINS, "mystderr"))));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            Asserts.shouldHaveFailedPreviously();
+        } catch (Throwable t) {
+            Asserts.expectedFailureContains(t, "stderr contains mystderr");
+        }
+
+        assertServiceFailed(test);
+    }
+
+    @Test
+    public void shouldNotBeUpIfAssertionsFail() {
+        Map<String, Object> equalsOne = MutableMap.of();
+        equalsOne.put(EQUALS, 1);
+
+        Map<String, Object> equals255 = MutableMap.of();
+        equals255.put(EQUALS, 255);
+
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime")
+            .configure(ASSERT_STATUS, makeAssertions(equalsOne, equals255)));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            Asserts.shouldHaveFailedPreviously();
+        } catch (Exception e) {
+            Asserts.expectedFailureContains(e, "exit code equals 1", "exit code equals 255");
+        }
+
+        assertServiceFailed(test);
+    }
+
+    @Test
+    public void shouldInvokeScript() throws Exception {
+        String text = "hello world";
+        Path testScript = createTempScript("script", "echo " + text);
+
+        try {
+            Map<String, Object> equalsZero = MutableMap.of();
+            equalsZero.put(EQUALS, 0);
+
+            Map<String, Object> containsText = MutableMap.of();
+            containsText.put(CONTAINS, text);
+
+            TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+                .configure(TARGET_ENTITY, testEntity)
+                .configure(DOWNLOAD_URL, "file:" + testScript)
+                .configure(ASSERT_STATUS, makeAssertions(equalsZero)));
+
+            app.start(ImmutableList.<Location>of());
+
+            assertServiceHealthy(test);
+            assertThat(RecordingSshTool.getLastExecCmd().commands.toString()).contains("TestSshCommandTest-script");
+
+        } finally {
+            Files.delete(testScript);
+        }
+    }
+
+    @Test
+    public void shouldFailIfTestEntityHasNoMachine() throws Exception {
+        TestEntity testEntityWithNoMachine = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        TestSshCommand test = app.createAndManageChild(EntitySpec.create(TestSshCommand.class)
+            .configure(TARGET_ENTITY, testEntityWithNoMachine)
+            .configure(COMMAND, "mycmd"));
+
+        try {
+            app.start(ImmutableList.<Location>of());
+            Asserts.shouldHaveFailedPreviously();
+        } catch (Exception e) {
+            Asserts.expectedFailureContains(e, "No instances of class "+SshMachineLocation.class.getName()+" available");
+        }
+
+        assertServiceFailed(test);
+    }
+    
+    private Path createTempScript(String filename, String contents) {
+        try {
+            Path tempFile = Files.createTempFile("TestSshCommandTest-" + filename, ".sh");
+            Files.write(tempFile, contents.getBytes());
+            return tempFile;
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+    
+    private void assertServiceFailed(TestSshCommand test) {
+        assertThat(test.sensors().get(SERVICE_UP)).isFalse()
+            .withFailMessage("Service should be down");
+        assertThat(ServiceStateLogic.getExpectedState(test)).isEqualTo(Lifecycle.ON_FIRE)
+            .withFailMessage("Service should be marked on fire");
+    }
+
+    private void assertServiceHealthy(TestSshCommand test) {
+        assertThat(test.sensors().get(SERVICE_UP)).isTrue()
+            .withFailMessage("Service should be up");
+        assertThat(ServiceStateLogic.getExpectedState(test)).isEqualTo(Lifecycle.RUNNING)
+            .withFailMessage("Service should be marked running");
+    }
+
+    private List<Map<String, ?>> makeAssertions(Map<String, ?> map) {
+        return ImmutableList.<Map<String, ?>>of(map);
+    }
+
+    private List<Map<String, ?>> makeAssertions(Map<String, ?> map1, Map<String, ?> map2) {
+        return ImmutableList.of(map1, map2);
+    }
+}


Mime
View raw message