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

[09/64] incubator-brooklyn git commit: [BROOKLYN-162] Refactor package in ./core/util

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/ShellToolAbstractTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/ShellToolAbstractTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/ShellToolAbstractTest.java
new file mode 100644
index 0000000..794a512
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/ShellToolAbstractTest.java
@@ -0,0 +1,441 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.internal.ssh.ShellTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+public abstract class ShellToolAbstractTest {
+
+    protected List<ShellTool> tools = Lists.newArrayList();
+    protected List<String> filesCreated;
+    protected String localFilePath;
+    
+    protected ShellTool tool;
+    
+    protected ShellTool newTool() {
+        return newTool(MutableMap.<String,Object>of());
+    }
+    
+    protected ShellTool newTool(Map<String,?> flags) {
+        ShellTool t = newUnregisteredTool(flags);
+        tools.add(t);
+        return t;
+    }
+
+    protected abstract ShellTool newUnregisteredTool(Map<String,?> flags);
+    
+    protected ShellTool tool() { return tool; }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        localFilePath = "/tmp/ssh-test-local-"+Identifiers.makeRandomId(8);
+        filesCreated = new ArrayList<String>();
+        filesCreated.add(localFilePath);
+
+        tool = newTool();
+        connect(tool);
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void afterMethod() throws Exception {
+        for (ShellTool t : tools) {
+            if (t instanceof SshTool) ((SshTool)t).disconnect();
+        }
+        for (String fileCreated : filesCreated) {
+            new File(fileCreated).delete();
+        }
+    }
+
+    protected static void connect(ShellTool tool) {
+        if (tool instanceof SshTool)
+            ((SshTool)tool).connect();
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecConsecutiveCommands() throws Exception {
+        String out = execScript("echo run1");
+        String out2 = execScript("echo run2");
+        
+        assertTrue(out.contains("run1"), "out="+out);
+        assertTrue(out2.contains("run2"), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptChainOfCommands() throws Exception {
+        String out = execScript("export MYPROP=abc", "echo val is $MYPROP");
+
+        assertTrue(out.contains("val is abc"), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptReturningNonZeroExitCode() throws Exception {
+        int exitcode = tool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("exit 123"));
+        assertEquals(exitcode, 123);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptReturningZeroExitCode() throws Exception {
+        int exitcode = tool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date"));
+        assertEquals(exitcode, 0);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptCommandWithEnvVariables() throws Exception {
+        String out = execScript(ImmutableList.of("echo val is $MYPROP2"), ImmutableMap.of("MYPROP2", "myval"));
+
+        assertTrue(out.contains("val is myval"), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testScriptDataNotLost() throws Exception {
+        String out = execScript("echo `echo foo``echo bar`");
+
+        assertTrue(out.contains("foobar"), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptWithSleepThenExit() throws Exception {
+        Stopwatch watch = Stopwatch.createStarted();
+        execScript("sleep 1", "exit 0");
+        assertTrue(watch.elapsed(TimeUnit.MILLISECONDS) > 900, "only slept "+Time.makeTimeStringRounded(watch));
+    }
+
+    // Really just tests that it returns; the command will be echo'ed automatically so this doesn't assert the command will have been executed
+    @Test(groups = {"Integration"})
+    public void testExecScriptBigCommand() throws Exception {
+        String bigstring = Strings.repeat("a", 10000);
+        String out = execScript("echo "+bigstring);
+        
+        assertTrue(out.contains(bigstring), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptBigChainOfCommand() throws Exception {
+        String bigstring = Strings.repeat("abcdefghij", 100); // 1KB
+        List<String> cmds = Lists.newArrayList();
+        for (int i = 0; i < 10; i++) {
+            cmds.add("export MYPROP"+i+"="+bigstring);
+            cmds.add("echo val"+i+" is $MYPROP"+i);
+        }
+        String out = execScript(cmds);
+        
+        for (int i = 0; i < 10; i++) {
+            assertTrue(out.contains("val"+i+" is "+bigstring), "out="+out);
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecScriptAbortsOnCommandFailure() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        int exitcode = tool.execScript(ImmutableMap.of("out", out), ImmutableList.of("export MYPROP=myval", "acmdthatdoesnotexist", "echo val is $MYPROP"));
+        String outstr = new String(out.toByteArray());
+
+        assertFalse(outstr.contains("val is myval"), "out="+out);
+        assertNotEquals(exitcode,  0);
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testExecScriptWithSleepThenBigCommand() throws Exception {
+        String bigstring = Strings.repeat("abcdefghij", 1000); // 10KB
+        String out = execScript("sleep 2", "export MYPROP="+bigstring, "echo val is $MYPROP");
+        assertTrue(out.contains("val is "+bigstring), "out="+out);
+    }
+    
+    @Test(groups = {"WIP", "Integration"})
+    public void testExecScriptBigConcurrentCommand() throws Exception {
+        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        List<ListenableFuture<?>> futures = new ArrayList<ListenableFuture<?>>();
+        try {
+            for (int i = 0; i < 10; i++) {
+                final ShellTool localtool = newTool();
+                connect(localtool);
+                
+                futures.add(executor.submit(new Runnable() {
+                        public void run() {
+                            String bigstring = Strings.repeat("abcdefghij", 1000); // 10KB
+                            String out = execScript(localtool, ImmutableList.of("export MYPROP="+bigstring, "echo val is $MYPROP"));
+                            assertTrue(out.contains("val is "+bigstring), "outSize="+out.length()+"; out="+out);
+                        }}));
+            }
+            Futures.allAsList(futures).get();
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    @Test(groups = {"WIP", "Integration"})
+    public void testExecScriptBigConcurrentSleepyCommand() throws Exception {
+        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        List<ListenableFuture<?>> futures = new ArrayList<ListenableFuture<?>>();
+        try {
+            long starttime = System.currentTimeMillis();
+            for (int i = 0; i < 10; i++) {
+                final ShellTool localtool = newTool();
+                connect(localtool);
+                
+                futures.add(executor.submit(new Runnable() {
+                        public void run() {
+                            String bigstring = Strings.repeat("abcdefghij", 1000); // 10KB
+                            String out = execScript(localtool, ImmutableList.of("sleep 2", "export MYPROP="+bigstring, "echo val is $MYPROP"));
+                            assertTrue(out.contains("val is "+bigstring), "out="+out);
+                        }}));
+            }
+            Futures.allAsList(futures).get();
+            long runtime = System.currentTimeMillis() - starttime;
+            
+            long OVERHEAD = 20*1000;
+            assertTrue(runtime < 2000+OVERHEAD, "runtime="+runtime);
+            
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecChainOfCommands() throws Exception {
+        String out = execCommands("MYPROP=abc", "echo val is $MYPROP");
+
+        assertEquals(out, "val is abc\n");
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecReturningNonZeroExitCode() throws Exception {
+        int exitcode = tool.execCommands(MutableMap.<String,Object>of(), ImmutableList.of("exit 123"));
+        assertEquals(exitcode, 123);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecReturningZeroExitCode() throws Exception {
+        int exitcode = tool.execCommands(MutableMap.<String,Object>of(), ImmutableList.of("date"));
+        assertEquals(exitcode, 0);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecCommandWithEnvVariables() throws Exception {
+        String out = execCommands(ImmutableList.of("echo val is $MYPROP2"), ImmutableMap.of("MYPROP2", "myval"));
+
+        assertEquals(out, "val is myval\n");
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecBigCommand() throws Exception {
+        String bigstring = Strings.repeat("abcdefghij", 1000); // 10KB
+        String out = execCommands("echo "+bigstring);
+
+        assertEquals(out, bigstring+"\n", "actualSize="+out.length()+"; expectedSize="+bigstring.length());
+    }
+
+    @Test(groups = {"Integration"})
+    public void testExecBigConcurrentCommand() throws Exception {
+        runExecBigConcurrentCommand(10, 0L);
+    }
+    
+    // TODO Fails I believe due to synchronization model in SshjTool of calling connect/disconnect.
+    // Even with a retry-count of 4, it still fails because some commands are calling disconnect
+    // while another concurrently executing command expects to be still connected.
+    @Test(groups = {"Integration", "WIP"})
+    public void testExecBigConcurrentCommandWithStaggeredStart() throws Exception {
+        // This test is to vary the concurrency of concurrent actions
+        runExecBigConcurrentCommand(50, 100L);
+    }
+    
+    protected void runExecBigConcurrentCommand(int numCommands, long staggeredDelayBeforeStart) throws Exception {
+        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        List<ListenableFuture<?>> futures = new ArrayList<ListenableFuture<?>>();
+        try {
+            for (int i = 0; i < numCommands; i++) {
+                long delay = (long) (Math.random() * staggeredDelayBeforeStart);
+                if (i > 0) Time.sleep(delay);
+                
+                futures.add(executor.submit(new Runnable() {
+                        public void run() {
+                            String bigstring = Strings.repeat("abcdefghij", 1000); // 10KB
+                            String out = execCommands("echo "+bigstring);
+                            assertEquals(out, bigstring+"\n", "actualSize="+out.length()+"; expectedSize="+bigstring.length());
+                        }}));
+            }
+            Futures.allAsList(futures).get();
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    // fails if terminal enabled
+    @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
+    public void testExecScriptCapturesStderr() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        String nonExistantCmd = "acmdthatdoesnotexist";
+        tool.execScript(ImmutableMap.of("out", out, "err", err), ImmutableList.of(nonExistantCmd));
+        assertTrue(new String(err.toByteArray()).contains(nonExistantCmd+": command not found"), "out="+out+"; err="+err);
+    }
+
+    // fails if terminal enabled
+    @Test(groups = {"Integration"})
+    @Deprecated // tests deprecated code
+    public void testExecCapturesStderr() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        String nonExistantCmd = "acmdthatdoesnotexist";
+        tool.execCommands(ImmutableMap.of("out", out, "err", err), ImmutableList.of(nonExistantCmd));
+        String errMsg = new String(err.toByteArray());
+        assertTrue(errMsg.contains(nonExistantCmd+": command not found\n"), "errMsg="+errMsg+"; out="+out+"; err="+err);
+        
+    }
+
+    @Test(groups = {"Integration"})
+    public void testScriptHeader() {
+        final ShellTool localtool = newTool();
+        String out = execScript(MutableMap.of("scriptHeader", "#!/bin/bash -e\necho hello world\n"), 
+                localtool, Arrays.asList("echo goodbye world"), null);
+        assertTrue(out.contains("goodbye world"), "no goodbye in output: "+out);
+        assertTrue(out.contains("hello world"), "no hello in output: "+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testStdErr() {
+        final ShellTool localtool = newTool();
+        Map<String,Object> props = new LinkedHashMap<String, Object>();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        props.put("out", out);
+        props.put("err", err);
+        int exitcode = localtool.execScript(props, Arrays.asList("echo hello err > /dev/stderr"), null);
+        assertFalse(out.toString().contains("hello err"), "hello found where it shouldn't have been, in stdout: "+out);
+        assertTrue(err.toString().contains("hello err"), "no hello in stderr: "+err);
+        assertEquals(0, exitcode);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testRunAsRoot() {
+        final ShellTool localtool = newTool();
+        Map<String,Object> props = new LinkedHashMap<String, Object>();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        props.put("out", out);
+        props.put("err", err);
+        props.put(SshTool.PROP_RUN_AS_ROOT.getName(), true);
+        int exitcode = localtool.execScript(props, Arrays.asList("whoami"), null);
+        assertTrue(out.toString().contains("root"), "not running as root; whoami is: "+out+" (err is '"+err+"')");
+        assertEquals(0, exitcode);
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testExecScriptEchosExecute() throws Exception {
+        String out = execScript("date");
+        assertTrue(out.toString().contains("Executed"), "Executed did not display: "+out);
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testExecScriptEchosDontExecuteWhenToldNoExtraOutput() throws Exception {
+        final ShellTool localtool = newTool();
+        Map<String,Object> props = new LinkedHashMap<String, Object>();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        props.put("out", out);
+        props.put("err", err);
+        props.put(SshTool.PROP_NO_EXTRA_OUTPUT.getName(), true);
+        int exitcode = localtool.execScript(props, Arrays.asList("echo hello world"), null);
+        assertFalse(out.toString().contains("Executed"), "Executed should not have displayed: "+out);
+        assertEquals(out.toString().trim(), "hello world");
+        assertEquals(0, exitcode);
+    }
+    
+    protected String execCommands(String... cmds) {
+        return execCommands(Arrays.asList(cmds));
+    }
+    
+    protected String execCommands(List<String> cmds) {
+        return execCommands(cmds, ImmutableMap.<String,Object>of());
+    }
+
+    protected String execCommands(List<String> cmds, Map<String,?> env) {
+        return execCommands(null, cmds, env);
+    }
+
+    protected String execCommands(ConfigBag config, List<String> cmds, Map<String,?> env) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        MutableMap<String,Object> flags = MutableMap.<String,Object>of("out", out);
+        if (config!=null) flags.add(config.getAllConfig());
+        tool.execCommands(flags, cmds, env);
+        return new String(out.toByteArray());
+    }
+
+    protected String execScript(String... cmds) {
+        return execScript(tool, Arrays.asList(cmds));
+    }
+
+    protected String execScript(ShellTool t, List<String> cmds) {
+        return execScript(ImmutableMap.<String,Object>of(), t, cmds, ImmutableMap.<String,Object>of());
+    }
+
+    protected String execScript(List<String> cmds) {
+        return execScript(cmds, ImmutableMap.<String,Object>of());
+    }
+    
+    protected String execScript(List<String> cmds, Map<String,?> env) {
+        return execScript(MutableMap.<String,Object>of(), tool, cmds, env);
+    }
+    
+    protected String execScript(Map<String, ?> props, ShellTool tool, List<String> cmds, Map<String,?> env) {
+        Map<String, Object> props2 = new LinkedHashMap<String, Object>(props);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        props2.put("out", out);
+        int exitcode = tool.execScript(props2, cmds, env);
+        String outstr = new String(out.toByteArray());
+        assertEquals(exitcode, 0, outstr);
+        return outstr;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractIntegrationTest.java
new file mode 100644
index 0000000..309d4fb
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractIntegrationTest.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.internal.ssh.ShellTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshException;
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+/**
+ * Test the operation of the {@link SshTool} utility class; to be extended to test concrete implementations.
+ * 
+ * Requires keys set up, e.g. running:
+ * 
+ * <pre>
+ * cd ~/.ssh
+ * ssh-keygen
+ * id_rsa_with_passphrase
+ * mypassphrase
+ * mypassphrase
+ * </pre>
+ * 
+ */
+public abstract class SshToolAbstractIntegrationTest extends ShellToolAbstractTest {
+
+    private static final Logger log = LoggerFactory.getLogger(SshToolAbstractIntegrationTest.class);
+    
+    // FIXME need tests which take properties set in entities and brooklyn.properties;
+    // but not in this class because it is lower level than entities, Aled would argue.
+
+    // TODO No tests for retry logic and exception handing yet
+
+    public static final String SSH_KEY_WITH_PASSPHRASE = System.getProperty("sshPrivateKeyWithPassphrase", "~/.ssh/id_rsa_with_passphrase");
+    public static final String SSH_PASSPHRASE = System.getProperty("sshPrivateKeyPassphrase", "mypassphrase");
+
+    protected String remoteFilePath;
+
+    protected SshTool tool() { return (SshTool)tool; }
+    
+    protected abstract SshTool newUnregisteredTool(Map<String,?> flags);
+
+    @Override
+    protected SshTool newTool() {
+        return newTool(ImmutableMap.of("host", "localhost", "privateKeyFile", "~/.ssh/id_rsa"));
+    }
+    
+    @Override
+    protected SshTool newTool(Map<String,?> flags) {
+        return (SshTool) super.newTool(flags);
+    }
+    
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+        remoteFilePath = "/tmp/ssh-test-remote-"+Identifiers.makeRandomId(8);
+        filesCreated.add(remoteFilePath);
+    }
+    
+    protected void assertRemoteFileContents(String remotePath, String expectedContents) {
+        String catout = execCommands("cat "+remotePath);
+        assertEquals(catout, expectedContents);
+    }
+    
+    /**
+     * @param remotePath
+     * @param expectedPermissions Of the form, for example, "-rw-r--r--"
+     */
+    protected void assertRemoteFilePermissions(String remotePath, String expectedPermissions) {
+        String lsout = execCommands("ls -l "+remotePath);
+        assertTrue(lsout.contains(expectedPermissions), lsout);
+    }
+    
+    protected void assertRemoteFileLastModifiedIsNow(String remotePath) {
+        // Check default last-modified time is `now`.
+        // Be lenient in assertion, in case unlucky that clock ticked over to next hour/minute as test was running.
+        // TODO Code could be greatly improved, but low priority!
+        // Output format:
+        //   -rw-r--r--  1   aled  wheel  18  Apr 24  15:03 /tmp/ssh-test-remote-CvFN9zQA
+        //   [0]         [1] [2]   [3]    [4] [5] [6] [7]   [8]
+        
+        String lsout = execCommands("ls -l "+remotePath);
+        
+        String[] lsparts = lsout.split("\\s+");
+        int day = Integer.parseInt(lsparts[6]);
+        int hour = Integer.parseInt(lsparts[7].split(":")[0]);
+        int minute = Integer.parseInt(lsparts[7].split(":")[1]);
+        
+        Calendar expected = Calendar.getInstance();
+        int expectedDay = expected.get(Calendar.DAY_OF_MONTH);
+        int expectedHour = expected.get(Calendar.HOUR_OF_DAY);
+        int expectedMinute = expected.get(Calendar.MINUTE);
+        
+        assertEquals(day, expectedDay, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedDay="+expectedDay+"; day="+day+"; zone="+expected.getTimeZone());
+        assertTrue(Math.abs(hour - expectedHour) <= 1, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedHour="+expectedHour+"; hour="+hour+"; zone="+expected.getTimeZone());
+        assertTrue(Math.abs(minute - expectedMinute) <= 1, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedMinute="+expectedMinute+"; minute="+minute+"; zone="+expected.getTimeZone());
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyToServerFromBytes() throws Exception {
+        String contents = "echo hello world!\n";
+        byte[] contentBytes = contents.getBytes();
+        tool().copyToServer(MutableMap.<String,Object>of(), contentBytes, remoteFilePath);
+
+        assertRemoteFileContents(remoteFilePath, contents);
+        assertRemoteFilePermissions(remoteFilePath, "-rw-r--r--");
+        
+        // TODO would like to also assert lastModified time, but on jenkins the jvm locale
+        // and the OS locale are different (i.e. different timezones) so the file time-stamp 
+        // is several hours out.
+        //assertRemoteFileLastModifiedIsNow(remoteFilePath);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyToServerFromInputStream() throws Exception {
+        String contents = "echo hello world!\n";
+        ByteArrayInputStream contentsStream = new ByteArrayInputStream(contents.getBytes());
+        tool().copyToServer(MutableMap.<String,Object>of(), contentsStream, remoteFilePath);
+
+        assertRemoteFileContents(remoteFilePath, contents);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyToServerWithPermissions() throws Exception {
+        tool().copyToServer(ImmutableMap.of("permissions","0754"), "echo hello world!\n".getBytes(), remoteFilePath);
+
+        assertRemoteFilePermissions(remoteFilePath, "-rwxr-xr--");
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testCopyToServerWithLastModifiedDate() throws Exception {
+        long lastModificationTime = 1234567;
+        tool().copyToServer(ImmutableMap.of("lastModificationDate", lastModificationTime), "echo hello world!\n".getBytes(), remoteFilePath);
+
+        String lsout = execCommands("ls -l "+remoteFilePath);//+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
+        //execCommands([ "ls -l "+remoteFilePath+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
+        //varies depending on timezone
+        assertTrue(lsout.contains("Jan 15  1970") || lsout.contains("Jan 14  1970") || lsout.contains("Jan 16  1970"), lsout);
+        //assertLastModified(lsout, lastModifiedDate)
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testCopyFileToServerWithPermissions() throws Exception {
+        String contents = "echo hello world!\n";
+        Files.write(contents, new File(localFilePath), Charsets.UTF_8);
+        tool().copyToServer(ImmutableMap.of("permissions", "0754"), new File(localFilePath), remoteFilePath);
+
+        assertRemoteFileContents(remoteFilePath, contents);
+
+        String lsout = execCommands("ls -l "+remoteFilePath);
+        assertTrue(lsout.contains("-rwxr-xr--"), lsout);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testCopyFromServer() throws Exception {
+        String contentsWithoutLineBreak = "echo hello world!";
+        String contents = contentsWithoutLineBreak+"\n";
+        tool().copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFilePath);
+        
+        tool().copyFromServer(MutableMap.<String,Object>of(), remoteFilePath, new File(localFilePath));
+
+        List<String> actual = Files.readLines(new File(localFilePath), Charsets.UTF_8);
+        assertEquals(actual, ImmutableList.of(contentsWithoutLineBreak));
+    }
+    
+    // TODO No config options in sshj or scp for auto-creating the parent directories
+    @Test(enabled=false, groups = {"Integration"})
+    public void testCopyFileToNonExistantDir() throws Exception {
+        String contents = "echo hello world!\n";
+        String remoteFileDirPath = "/tmp/ssh-test-remote-dir-"+Identifiers.makeRandomId(8);
+        String remoteFileInDirPath = remoteFileDirPath + File.separator + "ssh-test-remote-"+Identifiers.makeRandomId(8);
+        filesCreated.add(remoteFileInDirPath);
+        filesCreated.add(remoteFileDirPath);
+        
+        tool().copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFileInDirPath);
+
+        assertRemoteFileContents(remoteFileInDirPath, contents);
+    }
+    
+
+    @Test(groups = {"Integration"})
+    public void testAllocatePty() {
+        final ShellTool localtool = newTool(MutableMap.of("host", "localhost", SshTool.PROP_ALLOCATE_PTY.getName(), true));
+        Map<String,Object> props = new LinkedHashMap<String, Object>();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        props.put("out", out);
+        props.put("err", err);
+        int exitcode = localtool.execScript(props, Arrays.asList("echo hello err > /dev/stderr"), null);
+        assertTrue(out.toString().contains("hello err"), "no hello in output: "+out+" (err is '"+err+"')");
+        assertFalse(err.toString().contains("hello err"), "hello found in stderr: "+err);
+        assertEquals(0, exitcode);
+    }
+
+    // Requires setting up an extra ssh key, with a passphrase, and adding it to ~/.ssh/authorized_keys
+    @Test(groups = {"Integration"})
+    public void testSshKeyWithPassphrase() throws Exception {
+        final SshTool localtool = newTool(ImmutableMap.<String,Object>builder()
+                .put(SshTool.PROP_HOST.getName(), "localhost")
+                .put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
+                .put(SshTool.PROP_PRIVATE_KEY_PASSPHRASE.getName(), SSH_PASSPHRASE)
+                .build());
+        localtool.connect();
+        
+        assertEquals(tool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date")), 0);
+
+        // Also needs the negative test to prove that we're really using an ssh-key with a passphrase
+        try {
+            final SshTool localtool2 = newTool(ImmutableMap.<String,Object>builder()
+                    .put(SshTool.PROP_HOST.getName(), "localhost")
+                    .put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
+                    .build());
+            localtool2.connect();
+            fail();
+        } catch (Exception e) {
+            SshException se = Exceptions.getFirstThrowableOfType(e, SshException.class);
+            if (se == null) throw e;
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConnectWithInvalidUserThrowsException() throws Exception {
+        final ShellTool localtool = newTool(ImmutableMap.of("user", "wronguser", "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa"));
+        tools.add(localtool);
+        try {
+            connect(localtool);
+            fail();
+        } catch (SshException e) {
+            if (!e.toString().contains("failed to connect")) throw e;
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testOutputAsExpected() throws Exception {
+        final String CONTENTS = "hello world\n"
+            + "bye bye\n";
+        execCommands("cat > "+Os.mergePaths(Os.tmp(), "test1")+" << X\n"
+            + CONTENTS
+            + "X\n");
+        String read = execCommands("echo START_FOO", "cat "+Os.mergePaths(Os.tmp(), "test1"), "echo END_FOO");
+        log.debug("read back data written, as:\n"+read);
+        String contents = Strings.getFragmentBetween(read, "START_FOO", "END_FOO");
+        Assert.assertEquals(CONTENTS.trim(), contents.trim());
+    }
+
+    @Test(groups = {"Integration"})
+    public void testScriptDirPropertiesIsRespected() {
+        // For explanation of (some of) the magic behind this command, see http://stackoverflow.com/a/229606/68898
+        final String command = "if [[ \"$0\" == \"/var/tmp/\"* ]]; then true; else false; fi";
+
+        SshTool sshTool = newTool(ImmutableMap.<String, Object>builder()
+                .put(SshTool.PROP_HOST.getName(), "localhost")
+                .build());
+        int rc = sshTool.execScript(ImmutableMap.<String, Object>builder()
+                .put(SshTool.PROP_SCRIPT_DIR.getName(), "/var/tmp")
+                .build(), ImmutableList.of(command));
+        assertEquals(rc, 0);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractPerformanceTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractPerformanceTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractPerformanceTest.java
new file mode 100644
index 0000000..1730816
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/SshToolAbstractPerformanceTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.management.ManagementFactory;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Test the performance of different variants of invoking the sshj tool.
+ * 
+ * Intended for human-invocation and inspection, to see which parts are most expensive.
+ */
+public abstract class SshToolAbstractPerformanceTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SshToolAbstractPerformanceTest.class);
+    
+    private SshTool tool;
+    
+    protected abstract SshTool newSshTool(Map<String,?> flags);
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (tool != null) tool.disconnect();
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveConnectAndDisconnect() throws Exception {
+        Runnable task = new Runnable() {
+            public void run() {
+                tool = newSshTool(MutableMap.of("host", "localhost"));
+                tool.connect();
+                tool.disconnect();
+            }
+        };
+        runMany(task, "connect-disconnect", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveSmallCommands() throws Exception {
+        runExecManyCommands(ImmutableList.of("true"), false, "small-cmd", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveSmallCommandsWithStdouterr() throws Exception {
+        runExecManyCommands(ImmutableList.of("true"), true, "small-cmd-with-stdout", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveBigStdoutCommands() throws Exception {
+        runExecManyCommands(ImmutableList.of("head -c 100000 /dev/urandom"), true, "big-stdout", 10);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testConsecutiveBigStdinCommands() throws Exception {
+        String bigstr = Identifiers.makeRandomId(100000);
+        runExecManyCommands(ImmutableList.of("echo "+bigstr+" | wc -c"), true, "big-stdin", 10);
+    }
+
+    private void runExecManyCommands(final List<String> cmds, final boolean captureOutAndErr, String context, int iterations) throws Exception {
+        Runnable task = new Runnable() {
+                @Override public void run() {
+                    execScript(cmds, captureOutAndErr);
+                }};
+        runMany(task, context, iterations);
+    }
+
+    private void runMany(Runnable task, String context, int iterations) throws Exception {
+        MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+        ObjectName osMBeanName = ObjectName.getInstance(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME);
+        long preCpuTime = (Long) mbeanServer.getAttribute(osMBeanName, "ProcessCpuTime");
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        
+        for (int i = 0; i < iterations; i++) {
+            task.run();
+            
+            long postCpuTime = (Long) mbeanServer.getAttribute(osMBeanName, "ProcessCpuTime");
+            long elapsedTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+            double fractionCpu = (elapsedTime > 0) ? ((double)postCpuTime-preCpuTime) / TimeUnit.MILLISECONDS.toNanos(elapsedTime) : -1;
+            LOG.info("Executing {}; completed {}; took {}; fraction cpu {}", new Object[] {context, (i+1), Time.makeTimeStringRounded(elapsedTime), fractionCpu});
+        }
+    }
+
+    private int execScript(List<String> cmds, boolean captureOutandErr) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        MutableMap<String,?> flags = (captureOutandErr) ? MutableMap.of("out", out, "err", err) : MutableMap.<String,Object>of();
+        
+        tool = newSshTool(MutableMap.of("host", "localhost"));
+        tool.connect();
+        int result = tool.execScript(flags, cmds);
+        tool.disconnect();
+        
+        int outlen = out.toByteArray().length;
+        int errlen = out.toByteArray().length;
+        if (LOG.isTraceEnabled()) LOG.trace("Executed: result={}; stdout={}; stderr={}", new Object[] {result, outlen, errlen});
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolIntegrationTest.java
new file mode 100644
index 0000000..f8efa87
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolIntegrationTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.cli;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.internal.ssh.SshException;
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshToolAbstractIntegrationTest;
+import org.apache.brooklyn.core.util.internal.ssh.cli.SshCliTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test the operation of the {@link SshJschTool} utility class.
+ */
+public class SshCliToolIntegrationTest extends SshToolAbstractIntegrationTest {
+
+    private static final Logger log = LoggerFactory.getLogger(SshCliToolIntegrationTest.class);
+    
+    protected SshTool newUnregisteredTool(Map<String,?> flags) {
+        return new SshCliTool(flags);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testFlags() throws Exception {
+        final SshTool localtool = newTool(ImmutableMap.of("sshFlags", "-vvv -tt", "host", "localhost"));
+        tools.add(localtool);
+        try {
+            localtool.connect();
+            Map<String,Object> props = new LinkedHashMap<String, Object>();
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            ByteArrayOutputStream err = new ByteArrayOutputStream();
+            props.put("out", out);
+            props.put("err", err);
+            int exitcode = localtool.execScript(props, Arrays.asList("echo hello err > /dev/stderr"), null);
+            Assert.assertEquals(0, exitcode, "exitCode="+exitcode+", but expected 0");
+            log.debug("OUT from ssh -vvv command is: "+out);
+            log.debug("ERR from ssh -vvv command is: "+err);
+            assertFalse(err.toString().contains("hello err"), "hello found where it shouldn't have been, in stderr (should have been tty merged to stdout): "+err);
+            assertTrue(out.toString().contains("hello err"), "no hello in stdout: "+err);
+            // look for word 'ssh' to confirm we got verbose output
+            assertTrue(err.toString().toLowerCase().contains("ssh"), "no mention of ssh in stderr: "+err);
+        } catch (SshException e) {
+            if (!e.toString().contains("failed to connect")) throw e;
+        }
+    }
+
+    // Need to have at least one test method here (rather than just inherited) for eclipse to recognize it
+    @Test(enabled = false)
+    public void testDummy() throws Exception {
+    }
+    
+    // TODO When running mvn on the command line (for Aled), this test hangs when prompting for a password (but works in the IDE!)
+    // Doing .connect() isn't enough; need to cause ssh or scp to be invoked
+    @Test(enabled=false, groups = {"Integration"})
+    public void testConnectWithInvalidUserThrowsException() throws Exception {
+        final SshTool localtool = newTool(ImmutableMap.of("user", "wronguser", "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa"));
+        tools.add(localtool);
+        try {
+            localtool.connect();
+            int result = localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("date"));
+            fail("exitCode="+result+", but expected exception");
+        } catch (SshException e) {
+            if (!e.toString().contains("failed to connect")) throw e;
+        }
+    }
+    
+    // TODO ssh-cli doesn't support pass-phrases yet
+    @Test(enabled=false, groups = {"Integration"})
+    public void testSshKeyWithPassphrase() throws Exception {
+        super.testSshKeyWithPassphrase();
+    }
+
+    // Setting last modified date not yet supported for cli-based ssh
+    @Override
+    @Test(enabled=false, groups = {"Integration"})
+    public void testCopyToServerWithLastModifiedDate() throws Exception {
+        super.testCopyToServerWithLastModifiedDate();
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testExecReturningNonZeroExitCode() throws Exception {
+        int exitcode = tool.execCommands(MutableMap.<String,Object>of(), ImmutableList.of("exit 123"));
+        assertEquals(exitcode, 123);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolPerformanceTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolPerformanceTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolPerformanceTest.java
new file mode 100644
index 0000000..fd01180
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/cli/SshCliToolPerformanceTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.cli;
+
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshToolAbstractPerformanceTest;
+import org.apache.brooklyn.core.util.internal.ssh.cli.SshCliTool;
+import org.testng.annotations.Test;
+
+/**
+ * Test the performance of different variants of invoking the sshj tool.
+ * 
+ * Intended for human-invocation and inspection, to see which parts are most expensive.
+ */
+public class SshCliToolPerformanceTest extends SshToolAbstractPerformanceTest {
+
+    @Override
+    protected SshTool newSshTool(Map<String,?> flags) {
+        return new SshCliTool(flags);
+    }
+    
+    // Need to have at least one test method here (rather than just inherited) for eclipse to recognize it
+    @Test(enabled = false)
+    public void testDummy() throws Exception {
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolIntegrationTest.java
new file mode 100644
index 0000000..3dd558d
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolIntegrationTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.process;
+
+import static org.testng.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.internal.ssh.ShellToolAbstractTest;
+import org.apache.brooklyn.core.util.internal.ssh.process.ProcessTool;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Test the operation of the {@link ProcessTool} utility class.
+ */
+public class ProcessToolIntegrationTest extends ShellToolAbstractTest {
+
+    @Override
+    protected ProcessTool newUnregisteredTool(Map<String,?> flags) {
+        return new ProcessTool(flags);
+    }
+
+    // ones here included as *non*-integration tests. must run on windows and linux.
+    // (also includes integration tests from parent)
+
+    @Test(groups="UNIX")
+    public void testPortableCommand() throws Exception {
+        String out = execScript("echo hello world");
+        assertTrue(out.contains("hello world"), "out="+out);
+    }
+
+    @Test(groups="Integration")
+    public void testLoginShell() {
+        // this detection scheme only works for commands; can't test whether it works for scripts without 
+        // requiring stuff in bash_profile / profile / etc, which gets hard to make portable;
+        // it is nearly the same code path on the impl so this is probably enough 
+        
+        final String LOGIN_SHELL_CHECK = "shopt -q login_shell && echo 'yes, login shell' || echo 'no, not login shell'";
+        ConfigBag config = ConfigBag.newInstance().configure(ProcessTool.PROP_NO_EXTRA_OUTPUT, true);
+        String out;
+        
+        out = execCommands(config, Arrays.asList(LOGIN_SHELL_CHECK), null);
+        Assert.assertEquals(out.trim(), "no, not login shell", "out = "+out);
+        
+        config.configure(ProcessTool.PROP_LOGIN_SHELL, true);
+        out = execCommands(config, Arrays.asList(LOGIN_SHELL_CHECK), null);
+        Assert.assertEquals(out.trim(), "yes, login shell", "out = "+out);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolStaticsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolStaticsTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolStaticsTest.java
new file mode 100644
index 0000000..eacd761
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/process/ProcessToolStaticsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.process;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.brooklyn.core.util.internal.ssh.process.ProcessTool;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.os.Os;
+
+public class ProcessToolStaticsTest {
+
+    ByteArrayOutputStream out;
+    ByteArrayOutputStream err;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void clear() {
+        out = new ByteArrayOutputStream();
+        err = new ByteArrayOutputStream();
+    }
+    
+    private List<String> getTestCommand() {
+        if(Os.isMicrosoftWindows()) {
+            return Arrays.asList("cmd", "/c", "echo", "hello", "world");
+        } else {
+            return Arrays.asList("echo", "hello", "world");
+        }
+    }
+
+    @Test
+    public void testRunsWithStdout() throws Exception {
+        int code = ProcessTool.execSingleProcess(getTestCommand(), null, (File)null, out, err, this);
+        Assert.assertEquals(err.toString().trim(), "");
+        Assert.assertEquals(out.toString().trim(), "hello world");
+        Assert.assertEquals(code, 0);
+    }
+
+    @Test(groups="Integration") // *nix only
+    public void testRunsWithBashEnvVarAndStderr() throws Exception {
+        int code = ProcessTool.execSingleProcess(Arrays.asList("/bin/bash", "-c", "echo hello $NAME | tee /dev/stderr"), 
+                MutableMap.of("NAME", "BOB"), (File)null, out, err, this);
+        Assert.assertEquals(err.toString().trim(), "hello BOB", "err is: "+err);
+        Assert.assertEquals(out.toString().trim(), "hello BOB", "out is: "+out);
+        Assert.assertEquals(code, 0);
+    }
+
+    @Test(groups="Integration") // *nix only
+    public void testRunsManyCommandsWithBashEnvVarAndStderr() throws Exception {
+        int code = ProcessTool.execProcesses(Arrays.asList("echo hello $NAME", "export NAME=JOHN", "echo goodbye $NAME | tee /dev/stderr"), 
+                MutableMap.of("NAME", "BOB"), (File)null, out, err, " ; ", false, this);
+        Assert.assertEquals(err.toString().trim(), "goodbye JOHN", "err is: "+err);
+        Assert.assertEquals(out.toString().trim(), "hello BOB\ngoodbye JOHN", "out is: "+out);
+        Assert.assertEquals(code, 0);
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolAsyncStubIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolAsyncStubIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolAsyncStubIntegrationTest.java
new file mode 100644
index 0000000..e3b4b7d
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolAsyncStubIntegrationTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.sshj;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.brooklyn.core.internal.BrooklynFeatureEnablement;
+import org.apache.brooklyn.core.util.internal.ssh.SshAbstractTool.SshAction;
+import org.apache.brooklyn.core.util.internal.ssh.sshj.SshjTool;
+import org.apache.brooklyn.core.util.internal.ssh.sshj.SshjTool.ShellAction;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for async-exec with {@link SshjTool}, where it stubs out the actual ssh commands
+ * to return a controlled sequence of responses.
+ */
+public class SshjToolAsyncStubIntegrationTest {
+
+    static class InjectedResult {
+        Predicate<SshjTool.ShellAction> expected;
+        Function<SshjTool.ShellAction, Integer> result;
+        
+        InjectedResult(Predicate<SshjTool.ShellAction> expected, Function<SshjTool.ShellAction, Integer> result) {
+            this.expected = expected;
+            this.result = result;
+        }
+    }
+    
+    private SshjTool tool;
+    private List<InjectedResult> sequence;
+    int counter = 0;
+    private boolean origFeatureEnablement;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
+        sequence = Lists.newArrayList();
+        counter = 0;
+        
+        tool = new SshjTool(ImmutableMap.<String,Object>of("host", "localhost")) {
+            @SuppressWarnings("unchecked")
+            protected <T, C extends SshAction<T>> T acquire(C action, int sshTries, Duration sshTriesTimeout) {
+                if (action instanceof SshjTool.ShellAction) {
+                    SshjTool.ShellAction shellAction = (SshjTool.ShellAction) action;
+                    InjectedResult injectedResult = sequence.get(counter);
+                    assertTrue(injectedResult.expected.apply(shellAction), "counter="+counter+"; cmds="+shellAction.commands);
+                    counter++;
+                    return (T) injectedResult.result.apply(shellAction);
+                }
+                return super.acquire(action, sshTries, sshTriesTimeout);
+            }
+        };
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (tool != null) tool.disconnect();
+        } finally {
+            BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
+        }
+    }
+    
+    private Predicate<SshjTool.ShellAction> containsCmd(final String cmd) {
+        return new Predicate<SshjTool.ShellAction>() {
+            @Override public boolean apply(ShellAction input) {
+                return input != null && input.commands.toString().contains(cmd);
+            }
+        };
+    }
+    
+    private Function<SshjTool.ShellAction, Integer> returning(final int result, final String stdout, final String stderr) {
+        return new Function<SshjTool.ShellAction, Integer>() {
+            @Override public Integer apply(ShellAction input) {
+                try {
+                    if (stdout != null && input.out != null) input.out.write(stdout.getBytes());
+                    if (stderr != null && input.err != null) input.err.write(stderr.getBytes());
+                } catch (IOException e) {
+                    throw Exceptions.propagate(e);
+                }
+                return result;
+            }
+        };
+    }
+    
+    @Test(groups="Integration")
+    public void testPolls() throws Exception {
+        sequence = ImmutableList.of(
+                new InjectedResult(containsCmd("nohup"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(0, "mystringToStdout", "mystringToStderr")));
+
+        runTest(0, "mystringToStdout", "mystringToStderr");
+        assertEquals(counter, sequence.size());
+    }
+    
+    @Test(groups="Integration")
+    public void testPollsAndReturnsNonZeroExitCode() throws Exception {
+        sequence = ImmutableList.of(
+                new InjectedResult(containsCmd("nohup"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(123, "mystringToStdout", "mystringToStderr")),
+                new InjectedResult(containsCmd("# Retrieve status"), returning(0, "123", "")));
+
+        runTest(123, "mystringToStdout", "mystringToStderr");
+        assertEquals(counter, sequence.size());
+    }
+    
+    @Test(groups="Integration")
+    public void testPollsRepeatedly() throws Exception {
+        sequence = ImmutableList.of(
+                new InjectedResult(containsCmd("nohup"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(125, "mystringToStdout", "mystringToStderr")),
+                new InjectedResult(containsCmd("# Retrieve status"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(125, "mystringToStdout2", "mystringToStderr2")),
+                new InjectedResult(containsCmd("# Retrieve status"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(-1, "mystringToStdout3", "mystringToStderr3")),
+                new InjectedResult(containsCmd("# Long poll"), returning(125, "mystringToStdout4", "mystringToStderr4")),
+                new InjectedResult(containsCmd("# Retrieve status"), returning(0, "", "")),
+                new InjectedResult(containsCmd("# Long poll"), returning(0, "mystringToStdout5", "mystringToStderr5")));
+
+        runTest(0,
+                "mystringToStdout"+"mystringToStdout2"+"mystringToStdout3"+"mystringToStdout4"+"mystringToStdout5",
+                "mystringToStderr"+"mystringToStderr2"+"mystringToStderr3"+"mystringToStderr4"+"mystringToStderr5");
+        assertEquals(counter, sequence.size());
+    }
+    
+    protected void runTest(int expectedExit, String expectedStdout, String expectedStderr) throws Exception {
+        List<String> cmds = ImmutableList.of("abc");
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        int exitCode = tool.execScript(
+                ImmutableMap.of(
+                        "out", out, 
+                        "err", err, 
+                        SshjTool.PROP_EXEC_ASYNC.getName(), true, 
+                        SshjTool.PROP_NO_EXTRA_OUTPUT.getName(), true,
+                        SshjTool.PROP_EXEC_ASYNC_POLLING_TIMEOUT.getName(), Duration.ONE_MILLISECOND), 
+                cmds, 
+                ImmutableMap.<String,String>of());
+        String outStr = new String(out.toByteArray());
+        String errStr = new String(err.toByteArray());
+
+        assertEquals(exitCode, expectedExit);
+        assertEquals(outStr.trim(), expectedStdout);
+        assertEquals(errStr.trim(), expectedStderr);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolIntegrationTest.java
new file mode 100644
index 0000000..bf1aafb
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolIntegrationTest.java
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.sshj;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.schmizz.sshj.connection.channel.direct.Session;
+
+import org.apache.brooklyn.core.internal.BrooklynFeatureEnablement;
+import org.apache.brooklyn.core.util.internal.ssh.SshException;
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshToolAbstractIntegrationTest;
+import org.apache.brooklyn.core.util.internal.ssh.sshj.SshjTool;
+import org.testng.annotations.Test;
+
+import brooklyn.test.Asserts;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.exceptions.RuntimeTimeoutException;
+import brooklyn.util.os.Os;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test the operation of the {@link SshJschTool} utility class.
+ */
+public class SshjToolIntegrationTest extends SshToolAbstractIntegrationTest {
+
+    @Override
+    protected SshTool newUnregisteredTool(Map<String,?> flags) {
+        return new SshjTool(flags);
+    }
+
+    // TODO requires vt100 terminal emulation to work?
+    @Test(enabled = false, groups = {"Integration"})
+    public void testExecShellWithCommandTakingStdin() throws Exception {
+        // Uses `tee` to redirect stdin to the given file; cntr-d (i.e. char 4) stops tee with exit code 0
+        String content = "blah blah";
+        String out = execShellDirectWithTerminalEmulation("tee "+remoteFilePath, content, ""+(char)4, "echo file contents: `cat "+remoteFilePath+"`");
+
+        assertTrue(out.contains("file contents: blah blah"), "out="+out);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testGivesUpAfterMaxRetries() throws Exception {
+        final AtomicInteger callCount = new AtomicInteger();
+        
+        final SshTool localtool = new SshjTool(ImmutableMap.of("sshTries", 3, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
+            protected SshAction<Session> newSessionAction() {
+                callCount.incrementAndGet();
+                throw new RuntimeException("Simulating ssh execution failure");
+            }
+        };
+        
+        tools.add(localtool);
+        try {
+            localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
+            fail();
+        } catch (SshException e) {
+            if (!e.toString().contains("out of retries")) throw e;
+            assertEquals(callCount.get(), 3);
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testReturnsOnSuccessWhenRetrying() throws Exception {
+        final AtomicInteger callCount = new AtomicInteger();
+        final int successOnAttempt = 2;
+        final SshTool localtool = new SshjTool(ImmutableMap.of("sshTries", 3, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
+            protected SshAction<Session> newSessionAction() {
+                callCount.incrementAndGet();
+                if (callCount.incrementAndGet() >= successOnAttempt) {
+                    return super.newSessionAction();
+                } else {
+                    throw new RuntimeException("Simulating ssh execution failure");
+                }
+            }
+        };
+        
+        tools.add(localtool);
+        localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
+        assertEquals(callCount.get(), successOnAttempt);
+    }
+
+    @Test(groups = {"Integration"})
+    public void testGivesUpAfterMaxTime() throws Exception {
+        final AtomicInteger callCount = new AtomicInteger();
+        final SshTool localtool = new SshjTool(ImmutableMap.of("sshTriesTimeout", 1000, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
+            protected SshAction<Session> newSessionAction() {
+                callCount.incrementAndGet();
+                try {
+                    Thread.sleep(600);
+                } catch (InterruptedException e) {
+                    throw Exceptions.propagate(e);
+                }
+                throw new RuntimeException("Simulating ssh execution failure");
+            }
+        };
+        
+        tools.add(localtool);
+        try {
+            localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
+            fail();
+        } catch (RuntimeTimeoutException e) {
+            if (!e.toString().contains("out of time")) throw e;
+            assertEquals(callCount.get(), 2);
+        }
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testUsesCustomLocalTempDir() throws Exception {
+        class SshjToolForTest extends SshjTool {
+            public SshjToolForTest(Map<String, ?> map) {
+                super(map);
+            }
+            public File getLocalTempDir() {
+                return localTempDir;
+            }
+        };
+        
+        final SshjToolForTest localtool = new SshjToolForTest(ImmutableMap.<String, Object>of("host", "localhost"));
+        assertNotNull(localtool.getLocalTempDir());
+        assertEquals(localtool.getLocalTempDir(), new File(Os.tidyPath(SshjTool.PROP_LOCAL_TEMP_DIR.getDefaultValue())));
+        
+        String customTempDir = Os.tmp();
+        final SshjToolForTest localtool2 = new SshjToolForTest(ImmutableMap.of(
+                "host", "localhost", 
+                SshjTool.PROP_LOCAL_TEMP_DIR.getName(), customTempDir));
+        assertEquals(localtool2.getLocalTempDir(), new File(customTempDir));
+        
+        String customRelativeTempDir = "~/tmp";
+        final SshjToolForTest localtool3 = new SshjToolForTest(ImmutableMap.of(
+                "host", "localhost", 
+                SshjTool.PROP_LOCAL_TEMP_DIR.getName(), customRelativeTempDir));
+        assertEquals(localtool3.getLocalTempDir(), new File(Os.tidyPath(customRelativeTempDir)));
+    }
+
+    @Test(groups = {"Integration"})
+    public void testAsyncExecStdoutAndStderr() throws Exception {
+        boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
+        try {
+            // Include a sleep, to ensure that the contents retrieved in first poll and subsequent polls are appended
+            List<String> cmds = ImmutableList.of(
+                    "echo mystringToStdout",
+                    "echo mystringToStderr 1>&2",
+                    "sleep 5",
+                    "echo mystringPostSleepToStdout",
+                    "echo mystringPostSleepToStderr 1>&2");
+            
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            ByteArrayOutputStream err = new ByteArrayOutputStream();
+            int exitCode = tool.execScript(
+                    ImmutableMap.of(
+                            "out", out, 
+                            "err", err, 
+                            SshjTool.PROP_EXEC_ASYNC.getName(), true, 
+                            SshjTool.PROP_NO_EXTRA_OUTPUT.getName(), true,
+                            SshjTool.PROP_EXEC_ASYNC_POLLING_TIMEOUT.getName(), Duration.ONE_SECOND), 
+                    cmds, 
+                    ImmutableMap.<String,String>of());
+            String outStr = new String(out.toByteArray());
+            String errStr = new String(err.toByteArray());
+    
+            assertEquals(exitCode, 0);
+            assertEquals(outStr.trim(), "mystringToStdout\nmystringPostSleepToStdout");
+            assertEquals(errStr.trim(), "mystringToStderr\nmystringPostSleepToStderr");
+        } finally {
+            BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testAsyncExecReturnsExitCode() throws Exception {
+        boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
+        try {
+            int exitCode = tool.execScript(
+                    ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true), 
+                    ImmutableList.of("exit 123"), 
+                    ImmutableMap.<String,String>of());
+            assertEquals(exitCode, 123);
+        } finally {
+            BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
+        }
+    }
+
+    @Test(groups = {"Integration"})
+    public void testAsyncExecTimesOut() throws Exception {
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
+        try {
+            tool.execScript(
+                ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true, SshjTool.PROP_EXEC_TIMEOUT.getName(), Duration.millis(1)), 
+                ImmutableList.of("sleep 60"), 
+                ImmutableMap.<String,String>of());
+            fail();
+        } catch (Exception e) {
+            TimeoutException te = Exceptions.getFirstThrowableOfType(e, TimeoutException.class);
+            if (te == null) throw e;
+        } finally {
+            BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
+        }
+        
+        long seconds = stopwatch.elapsed(TimeUnit.SECONDS);
+        assertTrue(seconds < 30, "exec took "+seconds+" seconds");
+    }
+
+    @Test(groups = {"Integration"})
+    public void testAsyncExecAbortsIfProcessFails() throws Exception {
+        final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
+        Thread thread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    Stopwatch stopwatch = Stopwatch.createStarted();
+                    int exitStatus = tool.execScript(
+                        ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true, SshjTool.PROP_EXEC_TIMEOUT.getName(), Duration.millis(1)), 
+                        ImmutableList.of("sleep 63"), 
+                        ImmutableMap.<String,String>of());
+                    
+                    assertEquals(exitStatus, 143 /* 128 + Signal number (SIGTERM) */);
+                    
+                    long seconds = stopwatch.elapsed(TimeUnit.SECONDS);
+                    assertTrue(seconds < 30, "exec took "+seconds+" seconds");
+                } catch (Throwable t) {
+                    error.set(t);
+                }
+            }});
+        
+        boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
+        try {
+            thread.start();
+            
+            Asserts.succeedsEventually(new Runnable() {
+                @Override
+                public void run() {
+                    int exitStatus = tool.execCommands(ImmutableMap.<String,Object>of(), ImmutableList.of("ps aux| grep \"sleep 63\" | grep -v grep"));
+                    assertEquals(exitStatus, 0);
+                }});
+            
+            tool.execCommands(ImmutableMap.<String,Object>of(), ImmutableList.of("ps aux| grep \"sleep 63\" | grep -v grep | awk '{print($2)}' | xargs kill"));
+            
+            thread.join(30*1000);
+            assertFalse(thread.isAlive());
+            if (error.get() != null) {
+                throw Exceptions.propagate(error.get());
+            }
+        } finally {
+            thread.interrupt();
+            BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
+        }
+    }
+
+    
+    protected String execShellDirect(List<String> cmds) {
+        return execShellDirect(cmds, ImmutableMap.<String,Object>of());
+    }
+    
+    protected String execShellDirect(List<String> cmds, Map<String,?> env) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        int exitcode = ((SshjTool)tool).execShellDirect(ImmutableMap.of("out", out), cmds, env);
+        String outstr = new String(out.toByteArray());
+        assertEquals(exitcode, 0, outstr);
+        return outstr;
+    }
+
+    private String execShellDirectWithTerminalEmulation(String... cmds) {
+        return execShellDirectWithTerminalEmulation(Arrays.asList(cmds));
+    }
+    
+    private String execShellDirectWithTerminalEmulation(List<String> cmds) {
+        return execShellDirectWithTerminalEmulation(cmds, ImmutableMap.<String,Object>of());
+    }
+    
+    private String execShellDirectWithTerminalEmulation(List<String> cmds, Map<String,?> env) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        int exitcode = ((SshjTool)tool).execShellDirect(ImmutableMap.of("allocatePTY", true, "out", out), cmds, env);
+        String outstr = new String(out.toByteArray());
+        assertEquals(exitcode, 0, outstr);
+        return outstr;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolPerformanceTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolPerformanceTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolPerformanceTest.java
new file mode 100644
index 0000000..71ea4e6
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/sshj/SshjToolPerformanceTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.internal.ssh.sshj;
+
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+import org.apache.brooklyn.core.util.internal.ssh.SshToolAbstractPerformanceTest;
+import org.apache.brooklyn.core.util.internal.ssh.sshj.SshjTool;
+import org.testng.annotations.Test;
+
+/**
+ * Test the performance of different variants of invoking the sshj tool.
+ * 
+ * Intended for human-invocation and inspection, to see which parts are most expensive.
+ */
+public class SshjToolPerformanceTest extends SshToolAbstractPerformanceTest {
+
+    @Override
+    protected SshTool newSshTool(Map<String,?> flags) {
+        return new SshjTool(flags);
+    }
+    
+    // Need to have at least one test method here (rather than just inherited) for eclipse to recognize it
+    @Test(enabled = false)
+    public void testDummy() throws Exception {
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/699b3f65/core/src/test/java/org/apache/brooklyn/core/util/mutex/WithMutexesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/mutex/WithMutexesTest.java b/core/src/test/java/org/apache/brooklyn/core/util/mutex/WithMutexesTest.java
new file mode 100644
index 0000000..c37ab30
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/mutex/WithMutexesTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.core.util.mutex;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.mutex.MutexSupport;
+import org.apache.brooklyn.core.util.mutex.SemaphoreWithOwners;
+import org.apache.brooklyn.core.util.mutex.WithMutexes;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class WithMutexesTest {
+
+    @Test
+    public void testOneAcquisitionAndRelease() throws InterruptedException {
+        MutexSupport m = new MutexSupport();
+        Map<String, SemaphoreWithOwners> sems;
+        SemaphoreWithOwners s;
+        try {
+            m.acquireMutex("foo", "something foo");
+            sems = m.getAllSemaphores();
+            Assert.assertEquals(sems.size(), 1);
+            s = sems.get("foo");
+            Assert.assertEquals(s.getDescription(), "something foo");
+            Assert.assertEquals(s.getOwningThreads(), Arrays.asList(Thread.currentThread()));
+            Assert.assertEquals(s.getRequestingThreads(), Collections.emptyList());
+            Assert.assertTrue(s.isInUse());
+            Assert.assertTrue(s.isCallingThreadAnOwner());
+        } finally {
+            m.releaseMutex("foo");
+        }
+        Assert.assertFalse(s.isInUse());
+        Assert.assertFalse(s.isCallingThreadAnOwner());
+        Assert.assertEquals(s.getDescription(), "something foo");
+        Assert.assertEquals(s.getOwningThreads(), Collections.emptyList());
+        Assert.assertEquals(s.getRequestingThreads(), Collections.emptyList());
+        
+        sems = m.getAllSemaphores();
+        Assert.assertEquals(sems, Collections.emptyMap());
+    }
+
+    @Test(groups = "Integration")  //just because it takes a wee while
+    public void testBlockingAcquisition() throws InterruptedException {
+        final MutexSupport m = new MutexSupport();
+        m.acquireMutex("foo", "something foo");
+        
+        Assert.assertFalse(m.tryAcquireMutex("foo", "something else"));
+
+        Thread t = new Thread() {
+            public void run() {
+                try {
+                    m.acquireMutex("foo", "thread 2 foo");
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                m.releaseMutex("foo");
+            }
+        };
+        t.start();
+        
+        t.join(500);
+        Assert.assertTrue(t.isAlive());
+        Assert.assertEquals(m.getSemaphore("foo").getRequestingThreads(), Arrays.asList(t));
+
+        m.releaseMutex("foo");
+        
+        t.join(1000);
+        Assert.assertFalse(t.isAlive());
+
+        Assert.assertEquals(m.getAllSemaphores(), Collections.emptyMap());
+    }
+
+    
+    public static class SampleWithMutexesDelegatingMixin implements WithMutexes {
+        
+        /* other behaviour would typically go here... */
+        
+        WithMutexes mutexSupport = new MutexSupport();
+        
+        @Override
+        public void acquireMutex(String mutexId, String description) throws InterruptedException {
+            mutexSupport.acquireMutex(mutexId, description);
+        }
+
+        @Override
+        public boolean tryAcquireMutex(String mutexId, String description) {
+            return mutexSupport.tryAcquireMutex(mutexId, description);
+        }
+
+        @Override
+        public void releaseMutex(String mutexId) {
+            mutexSupport.releaseMutex(mutexId);
+        }
+
+        @Override
+        public boolean hasMutex(String mutexId) {
+            return mutexSupport.hasMutex(mutexId);
+        }
+    }
+    
+    @Test
+    public void testDelegatingMixinPattern() throws InterruptedException {
+        WithMutexes m = new SampleWithMutexesDelegatingMixin();
+        m.acquireMutex("foo", "sample");
+        Assert.assertTrue(m.hasMutex("foo"));
+        Assert.assertFalse(m.hasMutex("bar"));
+        m.releaseMutex("foo");
+        Assert.assertFalse(m.hasMutex("foo"));
+    }
+}