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/11/23 19:29:41 UTC

[12/20] incubator-brooklyn git commit: Rename everything as SimpleShellCommand.

Rename everything as SimpleShellCommand.

https://github.com/apache/incubator-brooklyn/pull/1030#issuecomment-157311566


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

Branch: refs/heads/master
Commit: 5f98079e62a7668aec8139fab4848b8e064824b6
Parents: f3a6395
Author: Geoff Macartney <ge...@cloudsoftcorp.com>
Authored: Tue Nov 17 14:14:57 2015 +0000
Committer: Geoff Macartney <ge...@cloudsoftcorp.com>
Committed: Tue Nov 17 14:14:57 2015 +0000

----------------------------------------------------------------------
 .../brooklyn/test/framework/SimpleCommand.java  |  75 ------
 .../test/framework/SimpleCommandDriver.java     |  70 ------
 .../test/framework/SimpleCommandImpl.java       | 252 -------------------
 .../SimpleCommandLifecycleEffectorTasks.java    |  58 -----
 .../test/framework/SimpleCommandTest.java       |  78 ------
 .../test/framework/SimpleCommandTestImpl.java   | 149 -----------
 .../test/framework/SimpleShellCommand.java      |  75 ++++++
 .../test/framework/SimpleShellCommandImpl.java  | 252 +++++++++++++++++++
 ...impleShellCommandLifecycleEffectorTasks.java |  57 +++++
 .../test/framework/SimpleShellCommandTest.java  |  78 ++++++
 .../framework/SimpleShellCommandTestImpl.java   | 147 +++++++++++
 .../framework/SimpleCommandIntegrationTest.java | 166 ------------
 .../SimpleShellCommandIntegrationTest.java      | 166 ++++++++++++
 13 files changed, 775 insertions(+), 848 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommand.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommand.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommand.java
deleted file mode 100644
index 3959b39..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommand.java
+++ /dev/null
@@ -1,75 +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 org.apache.brooklyn.api.entity.Entity;
-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.entity.trait.Startable;
-import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
-import org.apache.brooklyn.entity.software.base.SoftwareProcess;
-import org.apache.brooklyn.util.core.flags.SetFromFlag;
-
-import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
-
-/**
- * Entity to invoke on a node a simple command that will immediately succeed or fail.
- *
- * Invokes the command in the start operation, and declares itself RUNNING.
- */
-@ImplementedBy(SimpleCommandImpl.class)
-public interface SimpleCommand extends Entity, Startable {
-
-    String TMP_DEFAULT = "/tmp";
-
-    /**
-     * Result of a command invocation.
-     */
-    interface Result {
-        int getExitCode();
-        String getStdout();
-        String getStderr();
-
-    }
-
-    /**
-     * 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("run.dir", "directory where downloaded scripts should be put", TMP_DEFAULT);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandDriver.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandDriver.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandDriver.java
deleted file mode 100644
index d95d509..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandDriver.java
+++ /dev/null
@@ -1,70 +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 org.apache.brooklyn.api.entity.EntityLocal;
-import org.apache.brooklyn.api.entity.drivers.EntityDriver;
-import org.apache.brooklyn.api.location.Location;
-
-import java.util.Collection;
-
-/**
- * Driver to invoke a command on a node.
- */
-public interface SimpleCommandDriver extends EntityDriver {
-
-    /**
-     * Result of the command invocation.
-     */
-    interface Result {
-        int getExitCode();
-        String getStdout();
-        String getStderr();
-    }
-
-    /**
-     * The entity whose components we are controlling.
-     */
-    EntityLocal getEntity();
-
-    /**
-     * Execute the simple command during the start operation.
-     */
-    void start();
-
-    /**
-     * Execute the simple command during the restart.
-     */
-    void restart();
-
-    /**
-     * Does nothing.
-     */
-    void stop();
-
-    /**
-     * Execute the given command on the supplied host.
-     */
-    Result execute(Collection<? extends Location> hostLocations, String command);
-
-    /**
-     * Download the script at the given URL to the given directory on the host and execute it.
-     */
-    Result executeDownloadedScript(Collection<? extends Location> hostLocations, String url, String directory);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandImpl.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandImpl.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandImpl.java
deleted file mode 100644
index 7731801..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandImpl.java
+++ /dev/null
@@ -1,252 +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 com.google.common.base.Joiner;
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.MachineLocation;
-import org.apache.brooklyn.api.mgmt.TaskFactory;
-import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
-import org.apache.brooklyn.core.entity.AbstractEntity;
-import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
-import org.apache.brooklyn.core.location.Locations;
-import org.apache.brooklyn.location.ssh.SshMachineLocation;
-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.guava.Maybe;
-import org.apache.brooklyn.util.text.Strings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.Random;
-
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.*;
-import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
-import static org.apache.brooklyn.util.text.Strings.isBlank;
-import static org.apache.brooklyn.util.text.Strings.isNonBlank;
-
-/**
- * Implementation for {@link SimpleCommand}.
- */
-public class SimpleCommandImpl extends AbstractEntity implements SimpleCommand {
-
-    private static final Logger LOG = LoggerFactory.getLogger(SimpleCommandImpl.class);
-    private static final int A_LINE = 80;
-    public static final String DEFAULT_NAME = "download.sh";
-    private static final String CD = "cd";
-    private static final String SHELL_AND = "&&";
-
-    @Override
-    public void init() {
-        super.init();
-        getLifecycleEffectorTasks().attachLifecycleEffectors(this);
-    }
-
-    protected SimpleCommandLifecycleEffectorTasks getLifecycleEffectorTasks() {
-        return new SimpleCommandLifecycleEffectorTasks();
-    }
-
-    /**
-     * Gives the opportunity to sub-classes to do additional work based on the result of the command.
-     */
-    protected void handle(SimpleCommand.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())
-        });
-    }
-
-    private String shorten(String text) {
-        return Strings.maxlenWithEllipsis(text, A_LINE);
-    }
-
-    /**
-     * Does nothing in this class but gives sub-classes the opportunity to filter locations according to some criterion.
-     */
-    public Collection<? extends Location> filterLocations(Collection<? extends Location> locations) {
-        return locations;
-    }
-
-
-    @Override
-    public void start(Collection<? extends Location> locations) {
-        addLocations(locations);
-        setExpectedState(this, STARTING);
-    }
-
-    @Override
-    public void stop() {
-        LOG.debug("{} Stopping simple command", this);
-        setUpAndRunState(false, STOPPED);
-    }
-
-    @Override
-    public void restart() {
-        LOG.debug("{} Restarting simple command", this);
-        setUpAndRunState(true, RUNNING);
-    }
-
-    private void setUpAndRunState(boolean up, Lifecycle status) {
-        sensors().set(SERVICE_UP, up);
-        setExpectedState(this, status);
-    }
-
-    public void execute(MachineLocation machineLocation) {
-        try {
-            executeCommand(machineLocation);
-            setUpAndRunState(true, RUNNING);
-        } catch (Exception e) {
-            setUpAndRunState(false, ON_FIRE);
-            throw Exceptions.propagate(e);
-        }
-    }
-
-    private void executeCommand(MachineLocation machineLocation) {
-
-        SimpleCommand.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("Cannot specify both", downloadName, "and", commandName);
-        }
-
-        if (isBlank(downloadUrl) && isBlank(commandName)) {
-            throw illegal("No", downloadName, "and no", commandName, "provided");
-        }
-
-        if (Strings.isNonBlank(downloadUrl)) {
-            String scriptDir = getConfig(SCRIPT_DIR);
-            String scriptPath = calculateDestPath(downloadUrl, scriptDir);
-            result = executeDownloadedScript(machineLocation, downloadUrl, scriptPath);
-        }
-
-        if (Strings.isNonBlank(command)) {
-            result = executeShellCommand(machineLocation, command);
-        }
-
-        handle(result);
-    }
-
-    private IllegalArgumentException illegal(String ...messages) {
-        return new IllegalArgumentException(Joiner.on(' ').join(this.toString() + ":", messages));
-    }
-
-    private SimpleCommand.Result executeDownloadedScript(MachineLocation machineLocation, String url, String scriptPath) {
-
-        SshMachineLocation machine = getSshMachine(ImmutableList.<Location>of(machineLocation));
-
-        TaskFactory<?> install = SshTasks.installFromUrl(ImmutableMap.<String, Object>of(), machine, url, scriptPath);
-        DynamicTasks.queue(install);
-        DynamicTasks.waitForLast();
-
-        machine.execCommands("make the script executable", ImmutableList.<String>of("chmod u+x " + scriptPath));
-
-        String runDir = getConfig(RUN_DIR);
-        String cdAndRun = Joiner.on(' ').join(CD, runDir, SHELL_AND, scriptPath);
-
-        return executeShellCommand(machineLocation, cdAndRun);
-    }
-
-
-    private SimpleCommand.Result executeShellCommand(MachineLocation machineLocation, String command) {
-
-        SshMachineLocation machine = getSshMachine(ImmutableList.of(machineLocation));
-        SshEffectorTasks.SshEffectorTaskFactory<Integer> etf = SshEffectorTasks.ssh(machine, command);
-
-        LOG.debug("{} Creating task to execute '{}' on location {}", new Object[] {this, command, machine});
-        ProcessTaskWrapper<Integer> job = DynamicTasks.queue(etf);
-        DynamicTasks.waitForLast();
-        return buildResult(job);
-    }
-
-
-    private <T> SimpleCommand.Result buildResult(final ProcessTaskWrapper<Integer> job) {
-        return new SimpleCommand.Result() {
-
-            @Override
-            public int getExitCode() {
-                return job.get();
-            }
-
-            @Override
-            public String getStdout() {
-                return job.getStdout().trim();
-            }
-
-            @Override
-            public String getStderr() {
-                return job.getStderr().trim();
-            }
-        };
-    }
-
-    private SshMachineLocation getSshMachine(Collection<? extends Location> hostLocations) {
-        Maybe<SshMachineLocation> host = Locations.findUniqueSshMachineLocation(hostLocations);
-        if (host.isAbsent()) {
-            throw new IllegalArgumentException("No SSH machine found to run command");
-        }
-        return host.get();
-    }
-
-    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-" + randomDir(), scriptName);
-        } catch (MalformedURLException e) {
-            throw illegal("Malformed URL:", url);
-        }
-    }
-
-    private String randomDir() {
-        return Integer.valueOf(new Random(System.currentTimeMillis()).nextInt(100000)).toString();
-    }
-
-    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;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandLifecycleEffectorTasks.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandLifecycleEffectorTasks.java
deleted file mode 100644
index ae318ed..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandLifecycleEffectorTasks.java
+++ /dev/null
@@ -1,58 +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 com.google.common.base.Supplier;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.MachineLocation;
-import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
-import org.apache.http.util.Asserts;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.annotation.Nullable;
-import java.util.Collection;
-
-public class SimpleCommandLifecycleEffectorTasks extends MachineLifecycleEffectorTasks {
-
-    private static final Logger LOG = LoggerFactory.getLogger(SimpleCommandLifecycleEffectorTasks.class);
-
-    protected Location getLocation(@Nullable Collection<? extends Location> locations) {
-        return super.getLocation(entity().filterLocations(locations));
-    }
-
-
-    @Override
-    protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
-        LOG.debug("Performing lifecycle startProcessesAtMachine on simple command");
-        MachineLocation machineLocation = machineS.get();
-        entity().execute(machineLocation);
-        return "Started simple command on " + machineLocation;
-    }
-
-    @Override
-    protected String stopProcessesAtMachine() {
-        LOG.debug("No action needed on simple command stopped");
-        return "Stopped";
-    }
-
-    protected SimpleCommandImpl entity() {
-        return (SimpleCommandImpl) super.entity();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTest.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTest.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTest.java
deleted file mode 100644
index c962403..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTest.java
+++ /dev/null
@@ -1,78 +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 com.google.common.collect.Maps;
-import org.apache.brooklyn.api.entity.ImplementedBy;
-import org.apache.brooklyn.config.ConfigKey;
-import org.apache.brooklyn.core.config.ConfigKeys;
-import org.apache.brooklyn.util.core.flags.SetFromFlag;
-
-import java.util.Map;
-
-/**
- * Tests using a simple command execution.
- */
-@ImplementedBy(SimpleCommandTestImpl.class)
-public interface SimpleCommandTest extends SimpleCommand, BaseTest {
-
-    /**
-     * Equals assertion on command result.
-     */
-    String EQUALS = "equals";
-
-    /**
-     * String contains assertion on command result.
-     */
-    String CONTAINS = "contains";
-
-    /**
-     * Regex match assertion on command result.
-     */
-    String MATCHES = "matches";
-
-    /**
-     * Is-empty match assertion on command result.
-     */
-    String IS_EMPTY = "isEmpty";
-
-    /**
-     * 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<Map> ASSERT_STATUS = ConfigKeys.newConfigKey(Map.class, "assert.status",
-            "Assertions on command exit code", Maps.newLinkedHashMap());
-
-    /**
-     * Assertions on the standard output of the command as a String.
-     */
-    @SetFromFlag("assertOut")
-    ConfigKey<Map> ASSERT_OUT = ConfigKeys.newConfigKey(Map.class, "assert.out",
-            "Assertions on command standard output", Maps.newLinkedHashMap());
-
-    /**
-     * Assertions on the standard error of the command as a String.
-     */
-    @SetFromFlag("assertErr")
-    ConfigKey<Map> ASSERT_ERR = ConfigKeys.newConfigKey(Map.class, "assert.err",
-            "Assertions on command standard error", Maps.newLinkedHashMap());
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTestImpl.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTestImpl.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTestImpl.java
deleted file mode 100644
index cf85b37..0000000
--- a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleCommandTestImpl.java
+++ /dev/null
@@ -1,149 +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 com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableMap;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.core.location.Locations;
-import org.apache.brooklyn.test.Asserts;
-import org.apache.brooklyn.util.groovy.GroovyJavaMethods;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.truth;
-import static org.apache.commons.collections.MapUtils.isEmpty;
-
-public class SimpleCommandTestImpl extends SimpleCommandImpl implements SimpleCommandTest {
-
-    public static final int SUCCESS = 0;
-
-    @Override
-    public Entity resolveTarget() {
-        return AbstractTest.resolveTarget(getExecutionContext(), this);
-    }
-
-    /**
-     * The test will choose the location of its target entity.
-     */
-    public Collection<? extends Location> filterLocations(Collection<? extends Location> locations) {
-        Entity target = resolveTarget();
-        return target.getLocations();
-    }
-
-    @Override
-    protected void handle(SimpleCommand.Result result) {
-        AssertionSupport support = new AssertionSupport();
-        checkAssertions(support, exitCodeAssertions(), "exit code", result.getExitCode());
-        checkAssertions(support, getConfig(ASSERT_OUT), "stdout", result.getStdout());
-        checkAssertions(support, getConfig(ASSERT_ERR), "stderr", result.getStderr());
-        support.validate();
-    }
-
-    private <T> void checkAssertions(AssertionSupport support, Map<?, ?> assertions, String target, T actual) {
-        if (null == assertions) {
-            return;
-        }
-        if (null == actual) {
-            support.fail(target, "no actual value", "");
-            return;
-        }
-        for (Map.Entry<?, ?> assertion : assertions.entrySet()) {
-            String condition = assertion.getKey().toString();
-            Object expected = assertion.getValue();
-            switch (condition) {
-                case EQUALS :
-                    if (!actual.equals(expected)) {
-                        support.fail(target, EQUALS, expected);
-                    }
-                    break;
-                case CONTAINS :
-                    if (!actual.toString().contains(expected.toString())) {
-                        support.fail(target, CONTAINS, expected);
-                    }
-                    break;
-                case IS_EMPTY:
-                    if (!actual.toString().isEmpty() && truth(expected)) {
-                        support.fail(target, IS_EMPTY, expected);
-                    }
-                    break;
-                case MATCHES :
-                    if (!actual.toString().matches(expected.toString())) {
-                        support.fail(target, MATCHES, expected);
-                    }
-                    break;
-                default:
-                    support.fail(target, "unknown condition", condition);
-            }
-        }
-    }
-
-    private Map<?, ?> exitCodeAssertions() {
-        Map<?, ?> assertStatus = getConfig(ASSERT_STATUS);
-        if (isEmpty(assertStatus)) {
-            assertStatus = ImmutableMap.of(EQUALS, SUCCESS);
-        }
-        return assertStatus;
-    }
-
-    public static class FailedAssertion {
-        String target;
-        String assertion;
-        String expected;
-
-        public FailedAssertion(String target, String assertion, String expected) {
-            this.target = target;
-            this.assertion = assertion;
-            this.expected = expected;
-        }
-        public String description() {
-            return Joiner.on(' ').join(target, assertion, expected);
-        }
-    }
-
-    /**
-     * A convenience to collect and validate any assertion failures.
-     */
-    public static class AssertionSupport {
-        private List<FailedAssertion> failures = new ArrayList<>();
-
-        public void fail(String target, String assertion, Object expected) {
-            failures.add(new FailedAssertion(target, assertion, expected.toString()));
-        }
-
-        /**
-         * @throws AssertionError if any failures were collected.
-         */
-        public void validate() {
-            if (0 < failures.size()) {
-                StringBuilder summary = new StringBuilder();
-                summary.append("Assertion Failures: \n");
-                for (FailedAssertion fail : failures) {
-                    summary.append(fail.description()).append("\n");
-                }
-                Asserts.fail(summary.toString());
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommand.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommand.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommand.java
new file mode 100644
index 0000000..1f430a7
--- /dev/null
+++ b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommand.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.apache.brooklyn.api.entity.Entity;
+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.entity.trait.Startable;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
+
+/**
+ * Entity to invoke on a node a simple command that will immediately succeed or fail.
+ *
+ * Invokes the command in the start operation, and declares itself RUNNING.
+ */
+@ImplementedBy(SimpleShellCommandImpl.class)
+public interface SimpleShellCommand extends Entity, Startable {
+
+    String TMP_DEFAULT = "/tmp";
+
+    /**
+     * Result of a command invocation.
+     */
+    interface Result {
+        int getExitCode();
+        String getStdout();
+        String getStderr();
+
+    }
+
+    /**
+     * 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("run.dir", "directory where downloaded scripts should be put", TMP_DEFAULT);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandImpl.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandImpl.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandImpl.java
new file mode 100644
index 0000000..1ccce5e
--- /dev/null
+++ b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandImpl.java
@@ -0,0 +1,252 @@
+/*
+ * 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 com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.location.Locations;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+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.guava.Maybe;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Random;
+
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.*;
+import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
+import static org.apache.brooklyn.util.text.Strings.isBlank;
+import static org.apache.brooklyn.util.text.Strings.isNonBlank;
+
+/**
+ * Implementation for {@link SimpleShellCommand}.
+ */
+public class SimpleShellCommandImpl extends AbstractEntity implements SimpleShellCommand {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandImpl.class);
+    private static final int A_LINE = 80;
+    public static final String DEFAULT_NAME = "download.sh";
+    private static final String CD = "cd";
+    private static final String SHELL_AND = "&&";
+
+    @Override
+    public void init() {
+        super.init();
+        getLifecycleEffectorTasks().attachLifecycleEffectors(this);
+    }
+
+    protected SimpleShellCommandLifecycleEffectorTasks getLifecycleEffectorTasks() {
+        return new SimpleShellCommandLifecycleEffectorTasks();
+    }
+
+    /**
+     * Gives the opportunity to sub-classes to do additional work based on the result of the command.
+     */
+    protected void handle(SimpleShellCommand.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())
+        });
+    }
+
+    private String shorten(String text) {
+        return Strings.maxlenWithEllipsis(text, A_LINE);
+    }
+
+    /**
+     * Does nothing in this class but gives sub-classes the opportunity to filter locations according to some criterion.
+     */
+    public Collection<? extends Location> filterLocations(Collection<? extends Location> locations) {
+        return locations;
+    }
+
+
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        addLocations(locations);
+        setExpectedState(this, STARTING);
+    }
+
+    @Override
+    public void stop() {
+        LOG.debug("{} Stopping simple command", this);
+        setUpAndRunState(false, STOPPED);
+    }
+
+    @Override
+    public void restart() {
+        LOG.debug("{} Restarting simple command", this);
+        setUpAndRunState(true, RUNNING);
+    }
+
+    private void setUpAndRunState(boolean up, Lifecycle status) {
+        sensors().set(SERVICE_UP, up);
+        setExpectedState(this, status);
+    }
+
+    public void execute(MachineLocation machineLocation) {
+        try {
+            executeCommand(machineLocation);
+            setUpAndRunState(true, RUNNING);
+        } catch (Exception e) {
+            setUpAndRunState(false, ON_FIRE);
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    private void executeCommand(MachineLocation machineLocation) {
+
+        SimpleShellCommand.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("Cannot specify both", downloadName, "and", commandName);
+        }
+
+        if (isBlank(downloadUrl) && isBlank(commandName)) {
+            throw illegal("No", downloadName, "and no", commandName, "provided");
+        }
+
+        if (Strings.isNonBlank(downloadUrl)) {
+            String scriptDir = getConfig(SCRIPT_DIR);
+            String scriptPath = calculateDestPath(downloadUrl, scriptDir);
+            result = executeDownloadedScript(machineLocation, downloadUrl, scriptPath);
+        }
+
+        if (Strings.isNonBlank(command)) {
+            result = executeShellCommand(machineLocation, command);
+        }
+
+        handle(result);
+    }
+
+    private IllegalArgumentException illegal(String ...messages) {
+        return new IllegalArgumentException(Joiner.on(' ').join(this.toString() + ":", messages));
+    }
+
+    private SimpleShellCommand.Result executeDownloadedScript(MachineLocation machineLocation, String url, String scriptPath) {
+
+        SshMachineLocation machine = getSshMachine(ImmutableList.<Location>of(machineLocation));
+
+        TaskFactory<?> install = SshTasks.installFromUrl(ImmutableMap.<String, Object>of(), machine, url, scriptPath);
+        DynamicTasks.queue(install);
+        DynamicTasks.waitForLast();
+
+        machine.execCommands("make the script executable", ImmutableList.<String>of("chmod u+x " + scriptPath));
+
+        String runDir = getConfig(RUN_DIR);
+        String cdAndRun = Joiner.on(' ').join(CD, runDir, SHELL_AND, scriptPath);
+
+        return executeShellCommand(machineLocation, cdAndRun);
+    }
+
+
+    private SimpleShellCommand.Result executeShellCommand(MachineLocation machineLocation, String command) {
+
+        SshMachineLocation machine = getSshMachine(ImmutableList.of(machineLocation));
+        SshEffectorTasks.SshEffectorTaskFactory<Integer> etf = SshEffectorTasks.ssh(machine, command);
+
+        LOG.debug("{} Creating task to execute '{}' on location {}", new Object[] {this, command, machine});
+        ProcessTaskWrapper<Integer> job = DynamicTasks.queue(etf);
+        DynamicTasks.waitForLast();
+        return buildResult(job);
+    }
+
+
+    private <T> SimpleShellCommand.Result buildResult(final ProcessTaskWrapper<Integer> job) {
+        return new SimpleShellCommand.Result() {
+
+            @Override
+            public int getExitCode() {
+                return job.get();
+            }
+
+            @Override
+            public String getStdout() {
+                return job.getStdout().trim();
+            }
+
+            @Override
+            public String getStderr() {
+                return job.getStderr().trim();
+            }
+        };
+    }
+
+    private SshMachineLocation getSshMachine(Collection<? extends Location> hostLocations) {
+        Maybe<SshMachineLocation> host = Locations.findUniqueSshMachineLocation(hostLocations);
+        if (host.isAbsent()) {
+            throw new IllegalArgumentException("No SSH machine found to run command");
+        }
+        return host.get();
+    }
+
+    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-" + randomDir(), scriptName);
+        } catch (MalformedURLException e) {
+            throw illegal("Malformed URL:", url);
+        }
+    }
+
+    private String randomDir() {
+        return Integer.valueOf(new Random(System.currentTimeMillis()).nextInt(100000)).toString();
+    }
+
+    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;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandLifecycleEffectorTasks.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandLifecycleEffectorTasks.java
new file mode 100644
index 0000000..fdccd99
--- /dev/null
+++ b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandLifecycleEffectorTasks.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.google.common.base.Supplier;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+
+public class SimpleShellCommandLifecycleEffectorTasks extends MachineLifecycleEffectorTasks {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandLifecycleEffectorTasks.class);
+
+    protected Location getLocation(@Nullable Collection<? extends Location> locations) {
+        return super.getLocation(entity().filterLocations(locations));
+    }
+
+
+    @Override
+    protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
+        LOG.debug("Performing lifecycle startProcessesAtMachine on simple command");
+        MachineLocation machineLocation = machineS.get();
+        entity().execute(machineLocation);
+        return "Started simple command on " + machineLocation;
+    }
+
+    @Override
+    protected String stopProcessesAtMachine() {
+        LOG.debug("No action needed on simple command stopped");
+        return "Stopped";
+    }
+
+    protected SimpleShellCommandImpl entity() {
+        return (SimpleShellCommandImpl) super.entity();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
new file mode 100644
index 0000000..17621d9
--- /dev/null
+++ b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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 com.google.common.collect.Maps;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+import java.util.Map;
+
+/**
+ * Tests using a simple command execution.
+ */
+@ImplementedBy(SimpleShellCommandTestImpl.class)
+public interface SimpleShellCommandTest extends SimpleShellCommand, BaseTest {
+
+    /**
+     * Equals assertion on command result.
+     */
+    String EQUALS = "equals";
+
+    /**
+     * String contains assertion on command result.
+     */
+    String CONTAINS = "contains";
+
+    /**
+     * Regex match assertion on command result.
+     */
+    String MATCHES = "matches";
+
+    /**
+     * Is-empty match assertion on command result.
+     */
+    String IS_EMPTY = "isEmpty";
+
+    /**
+     * 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<Map> ASSERT_STATUS = ConfigKeys.newConfigKey(Map.class, "assert.status",
+            "Assertions on command exit code", Maps.newLinkedHashMap());
+
+    /**
+     * Assertions on the standard output of the command as a String.
+     */
+    @SetFromFlag("assertOut")
+    ConfigKey<Map> ASSERT_OUT = ConfigKeys.newConfigKey(Map.class, "assert.out",
+            "Assertions on command standard output", Maps.newLinkedHashMap());
+
+    /**
+     * Assertions on the standard error of the command as a String.
+     */
+    @SetFromFlag("assertErr")
+    ConfigKey<Map> ASSERT_ERR = ConfigKeys.newConfigKey(Map.class, "assert.err",
+            "Assertions on command standard error", Maps.newLinkedHashMap());
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
new file mode 100644
index 0000000..9c9a15a
--- /dev/null
+++ b/usage/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.test.Asserts;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.truth;
+import static org.apache.commons.collections.MapUtils.isEmpty;
+
+public class SimpleShellCommandTestImpl extends SimpleShellCommandImpl implements SimpleShellCommandTest {
+
+    public static final int SUCCESS = 0;
+
+    @Override
+    public Entity resolveTarget() {
+        return AbstractTest.resolveTarget(getExecutionContext(), this);
+    }
+
+    /**
+     * The test will choose the location of its target entity.
+     */
+    public Collection<? extends Location> filterLocations(Collection<? extends Location> locations) {
+        Entity target = resolveTarget();
+        return target.getLocations();
+    }
+
+    @Override
+    protected void handle(SimpleShellCommand.Result result) {
+        AssertionSupport support = new AssertionSupport();
+        checkAssertions(support, exitCodeAssertions(), "exit code", result.getExitCode());
+        checkAssertions(support, getConfig(ASSERT_OUT), "stdout", result.getStdout());
+        checkAssertions(support, getConfig(ASSERT_ERR), "stderr", result.getStderr());
+        support.validate();
+    }
+
+    private <T> void checkAssertions(AssertionSupport support, Map<?, ?> assertions, String target, T actual) {
+        if (null == assertions) {
+            return;
+        }
+        if (null == actual) {
+            support.fail(target, "no actual value", "");
+            return;
+        }
+        for (Map.Entry<?, ?> assertion : assertions.entrySet()) {
+            String condition = assertion.getKey().toString();
+            Object expected = assertion.getValue();
+            switch (condition) {
+                case EQUALS :
+                    if (!actual.equals(expected)) {
+                        support.fail(target, EQUALS, expected);
+                    }
+                    break;
+                case CONTAINS :
+                    if (!actual.toString().contains(expected.toString())) {
+                        support.fail(target, CONTAINS, expected);
+                    }
+                    break;
+                case IS_EMPTY:
+                    if (!actual.toString().isEmpty() && truth(expected)) {
+                        support.fail(target, IS_EMPTY, expected);
+                    }
+                    break;
+                case MATCHES :
+                    if (!actual.toString().matches(expected.toString())) {
+                        support.fail(target, MATCHES, expected);
+                    }
+                    break;
+                default:
+                    support.fail(target, "unknown condition", condition);
+            }
+        }
+    }
+
+    private Map<?, ?> exitCodeAssertions() {
+        Map<?, ?> assertStatus = getConfig(ASSERT_STATUS);
+        if (isEmpty(assertStatus)) {
+            assertStatus = ImmutableMap.of(EQUALS, SUCCESS);
+        }
+        return assertStatus;
+    }
+
+    public static class FailedAssertion {
+        String target;
+        String assertion;
+        String expected;
+
+        public FailedAssertion(String target, String assertion, String expected) {
+            this.target = target;
+            this.assertion = assertion;
+            this.expected = expected;
+        }
+        public String description() {
+            return Joiner.on(' ').join(target, assertion, expected);
+        }
+    }
+
+    /**
+     * A convenience to collect and validate any assertion failures.
+     */
+    public static class AssertionSupport {
+        private List<FailedAssertion> failures = new ArrayList<>();
+
+        public void fail(String target, String assertion, Object expected) {
+            failures.add(new FailedAssertion(target, assertion, expected.toString()));
+        }
+
+        /**
+         * @throws AssertionError if any failures were collected.
+         */
+        public void validate() {
+            if (0 < failures.size()) {
+                StringBuilder summary = new StringBuilder();
+                summary.append("Assertion Failures: \n");
+                for (FailedAssertion fail : failures) {
+                    summary.append(fail.description()).append("\n");
+                }
+                Asserts.fail(summary.toString());
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleCommandIntegrationTest.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleCommandIntegrationTest.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleCommandIntegrationTest.java
deleted file mode 100644
index 0b7b121..0000000
--- a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleCommandIntegrationTest.java
+++ /dev/null
@@ -1,166 +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 com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import org.apache.brooklyn.api.entity.EntitySpec;
-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.localhost.LocalhostMachineProvisioningLocation;
-import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Random;
-import java.util.UUID;
-
-import static org.apache.brooklyn.test.framework.BaseTest.TARGET_ENTITY;
-import static org.apache.brooklyn.test.framework.SimpleCommand.COMMAND;
-import static org.apache.brooklyn.test.framework.SimpleCommandTest.*;
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class SimpleCommandIntegrationTest extends BrooklynAppUnitTestSupport {
-    private static final Logger LOG = LoggerFactory.getLogger(SimpleCommandIntegrationTest.class);
-
-    private static final String UP = "up";
-    private LocalhostMachineProvisioningLocation localhost;
-    private String testId;
-
-
-    protected void setUpApp() {
-        super.setUpApp();
-        testId = UUID.randomUUID().toString();
-
-        localhost = app.getManagementContext().getLocationManager()
-            .createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
-                .configure("name", testId));
-    }
-
-    @Test(groups = "Integration")
-    public void shouldInvokeCommand() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-
-        SimpleCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "uptime")
-            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
-            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, UP)));
-
-        app.start(ImmutableList.of(localhost));
-
-        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 shouldNotBeUpIfAssertionFails() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-
-        SimpleCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "uptime")
-            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 1)));
-
-        try {
-            app.start(ImmutableList.of(localhost));
-        } catch (Exception e) {
-            assertThat(e.getCause().getMessage().contains("exit code equals 1"));
-        }
-
-        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
-            .withFailMessage("Service should be marked on fire");
-
-    }
-
-    @Test(groups = "Integration")
-    public void shouldInvokeScript() {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-
-        String text = "hello world";
-        String testUrl = createTempScript("script.sh", "echo " + text);
-
-        SimpleCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(DOWNLOAD_URL, testUrl)
-            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
-            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, text)));
-
-        app.start(ImmutableList.of(localhost));
-
-        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
-    public void shouldExecuteInTheRightPlace() throws Exception {
-        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-
-        String remoteTmp = randomName();
-        SimpleCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(COMMAND, "mkdir " + remoteTmp)
-            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0)));
-
-        String pwdUrl = createTempScript("pwd.sh", "pwd");
-
-        SimpleCommandTest pwd = app.createAndManageChild(EntitySpec.create(SimpleCommandTest.class)
-            .configure(TARGET_ENTITY, testEntity)
-            .configure(DOWNLOAD_URL, pwdUrl)
-            .configure(RUN_DIR, remoteTmp)
-            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
-            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, remoteTmp)));
-
-        app.start(ImmutableList.of(localhost));
-
-        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");
-    }
-
-    private String createTempScript(String filename, String contents) {
-        try {
-            Path tempDirectory = Files.createTempDirectory(randomName());
-            tempDirectory.toFile().deleteOnExit();
-            Path tempFile = Files.createFile(tempDirectory.resolve(filename));
-            Files.write(tempFile, contents.getBytes());
-            return "file:" + tempFile.toString();
-        } catch (IOException e) {
-            throw Exceptions.propagate(e);
-        }
-    }
-
-    private String randomName() {
-        return Integer.valueOf(new Random(System.currentTimeMillis()).nextInt(100000)).toString();
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5f98079e/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
----------------------------------------------------------------------
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
new file mode 100644
index 0000000..d6ff949
--- /dev/null
+++ b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/SimpleShellCommandIntegrationTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.brooklyn.api.entity.EntitySpec;
+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.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Random;
+import java.util.UUID;
+
+import static org.apache.brooklyn.test.framework.BaseTest.TARGET_ENTITY;
+import static org.apache.brooklyn.test.framework.SimpleShellCommand.COMMAND;
+import static org.apache.brooklyn.test.framework.SimpleShellCommandTest.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SimpleShellCommandIntegrationTest extends BrooklynAppUnitTestSupport {
+    private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandIntegrationTest.class);
+
+    private static final String UP = "up";
+    private LocalhostMachineProvisioningLocation localhost;
+    private String testId;
+
+
+    protected void setUpApp() {
+        super.setUpApp();
+        testId = UUID.randomUUID().toString();
+
+        localhost = app.getManagementContext().getLocationManager()
+            .createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+                .configure("name", testId));
+    }
+
+    @Test(groups = "Integration")
+    public void shouldInvokeCommand() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime")
+            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
+            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, UP)));
+
+        app.start(ImmutableList.of(localhost));
+
+        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 shouldNotBeUpIfAssertionFails() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "uptime")
+            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 1)));
+
+        try {
+            app.start(ImmutableList.of(localhost));
+        } catch (Exception e) {
+            assertThat(e.getCause().getMessage().contains("exit code equals 1"));
+        }
+
+        assertThat(ServiceStateLogic.getExpectedState(uptime)).isEqualTo(Lifecycle.ON_FIRE)
+            .withFailMessage("Service should be marked on fire");
+
+    }
+
+    @Test(groups = "Integration")
+    public void shouldInvokeScript() {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+
+        String text = "hello world";
+        String testUrl = createTempScript("script.sh", "echo " + text);
+
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(DOWNLOAD_URL, testUrl)
+            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
+            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, text)));
+
+        app.start(ImmutableList.of(localhost));
+
+        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
+    public void shouldExecuteInTheRightPlace() throws Exception {
+        TestEntity testEntity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+
+        String remoteTmp = randomName();
+        SimpleShellCommandTest uptime = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(COMMAND, "mkdir " + remoteTmp)
+            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0)));
+
+        String pwdUrl = createTempScript("pwd.sh", "pwd");
+
+        SimpleShellCommandTest pwd = app.createAndManageChild(EntitySpec.create(SimpleShellCommandTest.class)
+            .configure(TARGET_ENTITY, testEntity)
+            .configure(DOWNLOAD_URL, pwdUrl)
+            .configure(RUN_DIR, remoteTmp)
+            .configure(ASSERT_STATUS, ImmutableMap.of(EQUALS, 0))
+            .configure(ASSERT_OUT, ImmutableMap.of(CONTAINS, remoteTmp)));
+
+        app.start(ImmutableList.of(localhost));
+
+        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");
+    }
+
+    private String createTempScript(String filename, String contents) {
+        try {
+            Path tempDirectory = Files.createTempDirectory(randomName());
+            tempDirectory.toFile().deleteOnExit();
+            Path tempFile = Files.createFile(tempDirectory.resolve(filename));
+            Files.write(tempFile, contents.getBytes());
+            return "file:" + tempFile.toString();
+        } catch (IOException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    private String randomName() {
+        return Integer.valueOf(new Random(System.currentTimeMillis()).nextInt(100000)).toString();
+    }
+
+}