You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ha...@apache.org on 2015/08/17 21:17:44 UTC

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

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveBuilderTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveBuilderTest.java b/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveBuilderTest.java
new file mode 100644
index 0000000..0ce8298
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveBuilderTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.file;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.core.util.file.ArchiveBuilder;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.Identifiers;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+/**
+ * Test the operation of the {@link ArchiveBuilder} class.
+ */
+@Test
+public class ArchiveBuilderTest {
+
+    private File parentDir, tmpDir, tmpDir2;
+    private Predicate<ZipEntry> isDirectory = new Predicate<ZipEntry>() {
+                @Override
+                public boolean apply(@Nullable ZipEntry input) {
+                    return input.isDirectory();
+                }
+            };
+
+    @BeforeClass
+    public void createTmpDirAndFiles() throws IOException {
+        parentDir = Os.newTempDir(getClass().getSimpleName());
+        Os.deleteOnExitRecursively(parentDir);
+        tmpDir = new File(parentDir, Identifiers.makeRandomId(4));
+        Os.mkdirs(tmpDir);
+        Files.write("abcdef", new File(tmpDir, "data01.txt"), Charsets.US_ASCII);
+        Files.write("123456", new File(tmpDir, "data02.txt"), Charsets.US_ASCII);
+        Files.write("qqqqqq", new File(tmpDir, "data03.txt"), Charsets.US_ASCII);
+        
+        tmpDir2 = new File(parentDir, Identifiers.makeRandomId(4));
+        Os.mkdirs(tmpDir2);
+        Files.write("zzzzzz", new File(tmpDir2, "data04.txt"), Charsets.US_ASCII);
+    }
+    
+    @Test
+    public void testCreateZipFromDir() throws Exception {
+        File archive = ArchiveBuilder.zip().addDirContentsAt(tmpDir, ".").create();
+        archive.deleteOnExit();
+
+        List<ZipEntry> entries = Lists.newArrayList();
+        ZipInputStream input = new ZipInputStream(new FileInputStream(archive));
+        ZipEntry entry = input.getNextEntry();
+        while (entry != null) {
+            entries.add(entry);
+            entry = input.getNextEntry();
+        }
+        assertEquals(entries.size(), 4);
+        Iterable<ZipEntry> directories = Iterables.filter(entries, isDirectory);
+        Iterable<ZipEntry> files = Iterables.filter(entries, Predicates.not(isDirectory));
+        assertEquals(Iterables.size(directories), 1);
+        assertEquals(Iterables.size(files), 3);
+        String dirName = Iterables.getOnlyElement(directories).getName();
+        assertEquals(dirName, "./");
+        
+        Set<String> names = MutableSet.of();
+        for (ZipEntry file : files) {
+            assertTrue(file.getName().startsWith(dirName));
+            names.add(file.getName());
+        }
+        assertTrue(names.contains("./data01.txt"));
+        assertFalse(names.contains("./data04.txt"));
+        input.close();
+    }
+
+    @Test
+    public void testCreateZipFromTwoDirs() throws Exception {
+        File archive = ArchiveBuilder.zip().addDirContentsAt(tmpDir, ".").addDirContentsAt(tmpDir2, ".").create();
+        archive.deleteOnExit();
+
+        List<ZipEntry> entries = Lists.newArrayList();
+        ZipInputStream input = new ZipInputStream(new FileInputStream(archive));
+        ZipEntry entry = input.getNextEntry();
+        while (entry != null) {
+            entries.add(entry);
+            entry = input.getNextEntry();
+        }
+        assertEquals(entries.size(), 5);
+        Iterable<ZipEntry> directories = Iterables.filter(entries, isDirectory);
+        Iterable<ZipEntry> files = Iterables.filter(entries, Predicates.not(isDirectory));
+        assertEquals(Iterables.size(directories), 1);
+        assertEquals(Iterables.size(files), 4);
+        String dirName = Iterables.getOnlyElement(directories).getName();
+        assertEquals(dirName, "./");
+        
+        Set<String> names = MutableSet.of();
+        for (ZipEntry file : files) {
+            assertTrue(file.getName().startsWith(dirName));
+            names.add(file.getName());
+        }
+        assertTrue(names.contains("./data01.txt"));
+        assertTrue(names.contains("./data04.txt"));
+        input.close();
+    }
+    @Test
+    public void testCreateZipFromFiles() throws Exception {
+        ArchiveBuilder builder = ArchiveBuilder.zip();
+        for (String fileName : Arrays.asList("data01.txt", "data02.txt", "data03.txt")) {
+            builder.addAt(new File(tmpDir, fileName), ".");
+        }
+        File archive = builder.create();
+        archive.deleteOnExit();
+
+        List<ZipEntry> entries = Lists.newArrayList();
+        ZipInputStream input = new ZipInputStream(new FileInputStream(archive));
+        ZipEntry entry = input.getNextEntry();
+        while (entry != null) {
+            entries.add(entry);
+            entry = input.getNextEntry();
+        }
+        assertEquals(entries.size(), 3);
+        Iterable<ZipEntry> directories = Iterables.filter(entries, isDirectory);
+        Iterable<ZipEntry> files = Iterables.filter(entries, Predicates.not(isDirectory));
+        assertTrue(Iterables.isEmpty(directories));
+        assertEquals(Iterables.size(files), 3);
+        for (ZipEntry file : files) {
+            assertTrue(file.getName().startsWith(Os.mergePathsUnix(".", "data")));
+        }
+        input.close();
+    }
+
+    @Test
+    public void testCreateZipFromFilesWithBaseDir() throws Exception {
+        ArchiveBuilder builder = ArchiveBuilder.zip();
+        String baseDir = tmpDir.getName();
+        for (String fileName : Arrays.asList("data01.txt", "data02.txt", "data03.txt")) {
+            builder.addFromLocalBaseDir(parentDir, Os.mergePaths(baseDir, fileName));
+        }
+        File archive = builder.create();
+        archive.deleteOnExit();
+
+        List<ZipEntry> entries = Lists.newArrayList();
+        ZipInputStream input = new ZipInputStream(new FileInputStream(archive));
+        ZipEntry entry = input.getNextEntry();
+        while (entry != null) {
+            entries.add(entry);
+            entry = input.getNextEntry();
+        }
+        assertEquals(entries.size(), 3);
+        Iterable<ZipEntry> directories = Iterables.filter(entries, isDirectory);
+        Iterable<ZipEntry> files = Iterables.filter(entries, Predicates.not(isDirectory));
+        assertTrue(Iterables.isEmpty(directories));
+        assertEquals(Iterables.size(files), 3);
+        for (ZipEntry file : files) {
+            assertTrue(file.getName().startsWith(Os.mergePathsUnix(".", baseDir)));
+        }
+        input.close();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveUtilsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveUtilsTest.java b/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveUtilsTest.java
new file mode 100644
index 0000000..a13ef63
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/file/ArchiveUtilsTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.file;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Map;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppUnitTestSupport;
+
+import org.apache.brooklyn.core.util.ResourceUtils;
+import org.apache.brooklyn.core.util.file.ArchiveBuilder;
+import org.apache.brooklyn.core.util.file.ArchiveUtils;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import brooklyn.util.os.Os;
+
+import com.google.api.client.repackaged.com.google.common.base.Joiner;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+// Test are integration, because relies on ssh/scp via SshMachineLocation
+public class ArchiveUtilsTest extends BrooklynAppUnitTestSupport {
+    
+    private SshMachineLocation machine;
+    private ResourceUtils resourceUtils;
+
+    private Map<String, String> archiveContents = ImmutableMap.of("a.txt", "mya");
+    private File destDir;
+    private File origZip;
+    private File origJar;
+
+    @BeforeClass(alwaysRun=true)
+    public void setUpClass() throws Exception {
+        origZip = newZip(archiveContents);
+        origJar = Os.newTempFile(ArchiveUtilsTest.class, ".jar");
+        Files.copy(origZip, origJar);
+    }
+    
+    @AfterClass(alwaysRun=true)
+    public void tearDownClass() throws Exception {
+        if (origZip != null) origZip.delete();
+        if (origJar != null) origJar.delete();
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        machine = app.newLocalhostProvisioningLocation().obtain();
+        resourceUtils = ResourceUtils.create(ArchiveUtilsTest.class);
+        destDir = Os.newTempDir(getClass().getSimpleName());
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        if (destDir != null) Os.deleteRecursively(destDir);
+    }
+    
+    @Test(groups="Integration")
+    public void testDeployZipWithNoOptionalArgsSupplied() throws Exception {
+        boolean result = ArchiveUtils.deploy(resourceUtils, ImmutableMap.<String, Object>of(), origZip.getAbsolutePath(), machine, destDir.getAbsolutePath(), true, null, null);
+        assertTrue(result);
+        assertFilesEqual(new File(destDir, origZip.getName()), origZip);
+        assertSubFilesEqual(destDir, archiveContents);
+    }
+    
+    @Test(groups="Integration")
+    public void testDeployZipDeletingArchiveAfterUnpack() throws Exception {
+        boolean result = ArchiveUtils.deploy(resourceUtils, ImmutableMap.<String, Object>of(), origZip.getAbsolutePath(), machine, destDir.getAbsolutePath(), false, null, null);
+        assertTrue(result);
+        assertFalse(new File(destDir, origZip.getName()).exists());
+        assertSubFilesEqual(destDir, archiveContents);
+    }
+    
+    @Test(groups="Integration")
+    public void testDeployJarNotUnpacked() throws Exception {
+        ArchiveUtils.deploy(origJar.getAbsolutePath(), machine, destDir.getAbsolutePath());
+        assertFilesEqual(new File(destDir, origJar.getName()), origJar);
+    }
+    
+    @Test(groups="Integration")
+    public void testDeployExplicitDestFile() throws Exception {
+        String destFile = "custom-destFile.jar";
+        ArchiveUtils.deploy(origJar.getAbsolutePath(), machine, destDir.getAbsolutePath(), destFile);
+        assertFilesEqual(new File(destDir, destFile), origJar);
+    }
+    
+    private File newZip(Map<String, String> files) throws Exception {
+        File parentDir = Os.newTempDir(getClass().getSimpleName()+"-archive");
+        for (Map.Entry<String, String> entry : files.entrySet()) {
+            File subFile = new File(Os.mergePaths(parentDir.getAbsolutePath(), entry.getKey()));
+            subFile.getParentFile().mkdirs();
+            Files.write(entry.getValue(), subFile, Charsets.UTF_8);
+        }
+        return ArchiveBuilder.zip().addDirContentsAt(parentDir, ".").create();
+    }
+    
+    private void assertFilesEqual(File f1, File f2) throws Exception {
+        byte[] bytes1 = Files.asByteSource(f1).read();
+        byte[] bytes2 = Files.asByteSource(f1).read();
+        assertEquals(bytes1, bytes2, "f1="+f1+"; f2="+f2);
+    }
+    
+    private void assertSubFilesEqual(File parentDir, Map<String, String> files) throws Exception {
+        for (Map.Entry<String, String> entry : archiveContents.entrySet()) {
+            File subFile = new File(Os.mergePaths(parentDir.getAbsolutePath(), entry.getKey()));
+            assertEquals(Joiner.on("\n").join(Files.readLines(subFile, Charsets.UTF_8)), entry.getValue());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/flags/MethodCoercionsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/flags/MethodCoercionsTest.java b/core/src/test/java/org/apache/brooklyn/core/util/flags/MethodCoercionsTest.java
new file mode 100644
index 0000000..2d1efa3
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/flags/MethodCoercionsTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.flags;
+
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.guava.Maybe;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.brooklyn.core.util.flags.MethodCoercions;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+public class MethodCoercionsTest {
+
+    private Method singleParameterMethod;
+    private Method multiParameterMethod;
+    private Method singleCollectionParameterMethod;
+
+    @BeforeClass
+    public void testFixtureSetUp() {
+        try {
+            singleParameterMethod = TestClass.class.getMethod("singleParameterMethod", int.class);
+            multiParameterMethod = TestClass.class.getMethod("multiParameterMethod", boolean.class, int.class);
+            singleCollectionParameterMethod = TestClass.class.getMethod("singleCollectionParameterMethod", List.class);
+        } catch (NoSuchMethodException e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Test
+    public void testMatchSingleParameterMethod() throws Exception {
+        Predicate<Method> predicate = MethodCoercions.matchSingleParameterMethod("singleParameterMethod", "42");
+        assertTrue(predicate.apply(singleParameterMethod));
+        assertFalse(predicate.apply(multiParameterMethod));
+        assertFalse(predicate.apply(singleCollectionParameterMethod));
+    }
+
+    @Test
+    public void testTryFindAndInvokeSingleParameterMethod() throws Exception {
+        TestClass instance = new TestClass();
+        Maybe<?> maybe = MethodCoercions.tryFindAndInvokeSingleParameterMethod(instance, "singleParameterMethod", "42");
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasSingleParameterMethodCalled());
+    }
+
+    @Test
+    public void testMatchMultiParameterMethod() throws Exception {
+        Predicate<Method> predicate = MethodCoercions.matchMultiParameterMethod("multiParameterMethod", ImmutableList.of("true", "42"));
+        assertFalse(predicate.apply(singleParameterMethod));
+        assertTrue(predicate.apply(multiParameterMethod));
+        assertFalse(predicate.apply(singleCollectionParameterMethod));
+    }
+
+    @Test
+    public void testTryFindAndInvokeMultiParameterMethod() throws Exception {
+        TestClass instance = new TestClass();
+        Maybe<?> maybe = MethodCoercions.tryFindAndInvokeMultiParameterMethod(instance, "multiParameterMethod", ImmutableList.of("true", "42"));
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasMultiParameterMethodCalled());
+    }
+
+    @Test
+    public void testTryFindAndInvokeBestMatchingMethod() throws Exception {
+        TestClass instance = new TestClass();
+        Maybe<?> maybe = MethodCoercions.tryFindAndInvokeBestMatchingMethod(instance, "singleParameterMethod", "42");
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasSingleParameterMethodCalled());
+
+        instance = new TestClass();
+        maybe = MethodCoercions.tryFindAndInvokeBestMatchingMethod(instance, "multiParameterMethod", ImmutableList.of("true", "42"));
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasMultiParameterMethodCalled());
+
+        instance = new TestClass();
+        maybe = MethodCoercions.tryFindAndInvokeBestMatchingMethod(instance, "singleCollectionParameterMethod", ImmutableList.of("fred", "joe"));
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasSingleCollectionParameterMethodCalled());
+    }
+/*
+    @Test
+    public void testMatchSingleCollectionParameterMethod() throws Exception {
+        Predicate<Method> predicate = MethodCoercions.matchSingleCollectionParameterMethod("singleCollectionParameterMethod", ImmutableList.of("42"));
+        assertFalse(predicate.apply(singleParameterMethod));
+        assertFalse(predicate.apply(multiParameterMethod));
+        assertTrue(predicate.apply(singleCollectionParameterMethod));
+    }
+
+    @Test
+    public void testTryFindAndInvokeSingleCollectionParameterMethod() throws Exception {
+        TestClass instance = new TestClass();
+        Maybe<?> maybe = MethodCoercions.tryFindAndInvokeSingleCollectionParameterMethod(instance, "singleCollectionParameterMethod", ImmutableList.of("42"));
+        assertTrue(maybe.isPresent());
+        assertTrue(instance.wasSingleCollectionParameterMethodCalled());
+    }
+*/
+    public static class TestClass {
+
+        private boolean singleParameterMethodCalled;
+        private boolean multiParameterMethodCalled;
+        private boolean singleCollectionParameterMethodCalled;
+
+        public void singleParameterMethod(int parameter) {
+            singleParameterMethodCalled = true;
+        }
+
+        public void multiParameterMethod(boolean parameter1, int parameter2) {
+            multiParameterMethodCalled = true;
+        }
+
+        public void singleCollectionParameterMethod(List<String> parameter) {
+            singleCollectionParameterMethodCalled = true;
+        }
+
+        public boolean wasSingleParameterMethodCalled() {
+            return singleParameterMethodCalled;
+        }
+
+        public boolean wasMultiParameterMethodCalled() {
+            return multiParameterMethodCalled;
+        }
+
+        public boolean wasSingleCollectionParameterMethodCalled() {
+            return singleCollectionParameterMethodCalled;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/http/BetterMockWebServer.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/http/BetterMockWebServer.java b/core/src/test/java/org/apache/brooklyn/core/util/http/BetterMockWebServer.java
new file mode 100644
index 0000000..f02689b
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/http/BetterMockWebServer.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.http;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URL;
+
+import javax.net.ssl.SSLSocketFactory;
+
+import com.google.common.base.Throwables;
+import com.google.mockwebserver.Dispatcher;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.RecordedRequest;
+
+/** like MockWebServer (and delegating) but:
+ * <li> allows subclassing
+ * <li> easy way to create instance which returns localhost for {@link #getHostName()}
+ *      (since otherwise you can get failures on networks which misconfigure hostname) 
+ * */
+public class BetterMockWebServer {
+
+    final MockWebServer delegate = new MockWebServer();
+    String hostname = null;
+    boolean isHttps = false;
+    
+    public static BetterMockWebServer newInstanceLocalhost() {
+        return new BetterMockWebServer().setHostName("localhost");
+    }
+    
+    /** use {@link #newInstanceLocalhost()} or subclass */
+    protected BetterMockWebServer() {}
+
+    public BetterMockWebServer setHostName(String hostname) {
+        this.hostname = hostname;
+        return this;
+    }
+
+
+    // --- delegate methods (unchanged)
+    
+    public void enqueue(MockResponse response) {
+        delegate.enqueue(response);
+    }
+
+    public boolean equals(Object obj) {
+        return delegate.equals(obj);
+    }
+
+    public String getCookieDomain() {
+        return delegate.getCookieDomain();
+    }
+
+    public String getHostName() {
+        if (hostname!=null) return hostname;
+        return delegate.getHostName();
+    }
+
+    public int getPort() {
+        return delegate.getPort();
+    }
+
+    public int getRequestCount() {
+        return delegate.getRequestCount();
+    }
+
+    public URL getUrl(String path) {
+        try {
+            return isHttps
+                ? new URL("https://" + getHostName() + ":" + getPort() + path)
+                : new URL("http://" + getHostName() + ":" + getPort() + path);
+        } catch (MalformedURLException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    public int hashCode() {
+        return delegate.hashCode();
+    }
+
+    public void play() throws IOException {
+        delegate.play();
+    }
+
+    public void play(int port) throws IOException {
+        delegate.play(port);
+    }
+
+    public void setBodyLimit(int maxBodyLength) {
+        delegate.setBodyLimit(maxBodyLength);
+    }
+
+    public void setDispatcher(Dispatcher dispatcher) {
+        delegate.setDispatcher(dispatcher);
+    }
+
+    public void shutdown() throws IOException {
+        delegate.shutdown();
+    }
+
+    public RecordedRequest takeRequest() throws InterruptedException {
+        return delegate.takeRequest();
+    }
+
+    public Proxy toProxyAddress() {
+        return delegate.toProxyAddress();
+    }
+
+    public String toString() {
+        return delegate.toString();
+    }
+
+    public void useHttps(SSLSocketFactory sslSocketFactory, boolean tunnelProxy) {
+        isHttps = true;
+        delegate.useHttps(sslSocketFactory, tunnelProxy);
+    }
+    
+    
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/http/HttpToolIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/http/HttpToolIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/util/http/HttpToolIntegrationTest.java
new file mode 100644
index 0000000..f557955
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/http/HttpToolIntegrationTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.http;
+
+import static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+
+import org.apache.http.client.HttpClient;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.core.util.http.HttpTool;
+import org.apache.brooklyn.core.util.http.HttpToolResponse;
+import org.apache.brooklyn.location.basic.PortRanges;
+
+import brooklyn.test.HttpService;
+
+import com.google.common.collect.ImmutableMap;
+
+public class HttpToolIntegrationTest {
+
+    // TODO Expand test coverage for credentials etc
+    
+    private HttpService httpService;
+    private HttpService httpsService;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        httpService = new HttpService(PortRanges.fromString("9000+"), false).start();
+        httpsService = new HttpService(PortRanges.fromString("9000+"), true).start();
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (httpService != null) httpService.shutdown();
+        if (httpsService != null) httpsService.shutdown();
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testHttpGet() throws Exception {
+        URI baseUri = new URI(httpService.getUrl());
+
+        HttpClient client = HttpTool.httpClientBuilder().build();
+        HttpToolResponse result = HttpTool.httpGet(client, baseUri, ImmutableMap.<String,String>of());
+        assertTrue(new String(result.getContent()).contains("Hello, World"), "val="+new String(result.getContent()));
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testHttpRedirect() throws Exception {
+        URI baseUri = new URI(httpService.getUrl() + "hello/redirectAbsolute");
+
+        HttpClient client = HttpTool.httpClientBuilder().laxRedirect(true).build();
+        HttpToolResponse result = HttpTool.httpGet(client, baseUri, ImmutableMap.<String,String>of());
+        assertTrue(new String(result.getContent()).contains("Hello, World"), "val="+new String(result.getContent()));
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testHttpPost() throws Exception {
+        URI baseUri = new URI(httpService.getUrl());
+
+        HttpClient client = HttpTool.httpClientBuilder().build();
+        HttpToolResponse result = HttpTool.httpPost(client, baseUri, ImmutableMap.<String,String>of(), new byte[0]);
+        assertTrue(new String(result.getContent()).contains("Hello, World"), "val="+new String(result.getContent()));
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testHttpsGetWithTrustAll() throws Exception {
+        URI baseUri = new URI(httpsService.getUrl());
+
+        HttpClient client = HttpTool.httpClientBuilder().https(true).trustAll().build();
+        HttpToolResponse result = HttpTool.httpGet(client, baseUri, ImmutableMap.<String,String>of());
+        assertTrue(new String(result.getContent()).contains("Hello, World"), "val="+new String(result.getContent()));
+    }
+    
+    @Test(groups = {"Integration"})
+    public void testHttpsPostWithTrustSelfSigned() throws Exception {
+        URI baseUri = new URI(httpsService.getUrl());
+
+        HttpClient client = HttpTool.httpClientBuilder().https(true).trustSelfSigned().build();
+        HttpToolResponse result = HttpTool.httpPost(client, baseUri, ImmutableMap.<String,String>of(), new byte[0]);
+        assertTrue(new String(result.getContent()).contains("Hello, World"), "val="+new String(result.getContent()));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/internal/FlagUtilsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/FlagUtilsTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/FlagUtilsTest.java
new file mode 100644
index 0000000..02becf6
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/FlagUtilsTest.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;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.trait.Configurable;
+import org.apache.brooklyn.api.management.Task;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.flags.FlagUtils;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.config.ConfigKey.HasConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class FlagUtilsTest {
+
+    public static final Logger log = LoggerFactory.getLogger(FlagUtilsTest.class);
+    
+    @Test
+    public void testGetAllFields() {
+        log.info("types {}", FlagUtils.getAllAssignableTypes(Baz.class));
+        assertEquals(FlagUtils.getAllAssignableTypes(Baz.class), ImmutableList.of(Baz.class, Foo.class, Bar.class));
+        List<Field> fs = FlagUtils.getAllFields(Baz.class);
+        for (Field f : fs) {
+            log.info("field {}    {}", f.getName(), f);
+        }
+        List<String> fsn = ImmutableList.copyOf(Iterables.transform(fs, new Function<Field, String>() {
+            @Override public String apply(Field f) {
+                return f.getName();
+            }}));
+        assertTrue(fsn.indexOf("A") >= 0);
+        assertTrue(fsn.indexOf("w") > fsn.indexOf("A")); 
+        assertTrue(fsn.indexOf("x") > fsn.indexOf("A") );
+        assertTrue(fsn.indexOf("yNotY") > fsn.indexOf("A")); 
+        assertTrue(fsn.indexOf("Z") > fsn.indexOf("yNotY") );
+    }    
+    
+    @Test
+    public void testSetFieldsFromFlags() {
+        Foo f = new Foo();
+        Map<?,?> m = MutableMap.of("w", 3, "x", 1, "y", 7, "z", 9);
+        Map<?, ?> unused = FlagUtils.setFieldsFromFlags(m, f);
+        assertEquals(f.w, 3);
+        assertEquals(f.x, 1);
+        assertEquals(f.yNotY, 7);
+        assertEquals(unused, ImmutableMap.of("z", 9));
+        Map<?,?> m2 = FlagUtils.getFieldsWithValues(f);
+        m.remove("z");
+        assertEquals(m2, m);
+    }
+    
+    @Test
+    public void testCollectionCoercionOnSetFromFlags() {
+        WithSpecialFieldTypes s = new WithSpecialFieldTypes();
+        Map<?,?> m = ImmutableMap.of("set", ImmutableSet.of(1));
+        FlagUtils.setFieldsFromFlags(m, s);
+        assertEquals(s.set, ImmutableSet.of(1));
+    }
+
+    @Test
+    public void testInetAddressCoercionOnSetFromFlags() {
+        WithSpecialFieldTypes s = new WithSpecialFieldTypes();
+        Map<?,?> m = ImmutableMap.of("inet", "127.0.0.1");
+        FlagUtils.setFieldsFromFlags(m, s);
+        assertEquals(s.inet.getHostAddress(), "127.0.0.1");
+    }
+
+    @Test
+    public void testNonImmutableField() {
+        Foo f = new Foo();
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("w", 8), f);
+        assertEquals(f.w, 8);
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("w", 9), f);
+        assertEquals(f.w, 9);
+    }
+
+    @Test
+    public void testImmutableIntField() {
+        Foo f = new Foo();
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("x", 8), f);
+        assertEquals(f.x, 8);
+        boolean succeededWhenShouldntHave = false; 
+        try {
+            FlagUtils.setFieldsFromFlags(ImmutableMap.of("x", 9), f);
+            succeededWhenShouldntHave = true;
+        } catch (IllegalStateException e) {
+            //expected
+        }
+        assertFalse(succeededWhenShouldntHave);
+        assertEquals(f.x, 8);
+    }
+
+    @Test
+    public void testImmutableObjectField() {
+        WithImmutableNonNullableObject o = new WithImmutableNonNullableObject();
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a", "b", "b"), o);
+        assertEquals(o.a, "a");
+        assertEquals(o.b, "b");
+        
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a2"), o);
+        assertEquals(o.a, "a2");
+        
+        boolean succeededWhenShouldntHave = false;
+        try {
+            FlagUtils.setFieldsFromFlags(ImmutableMap.of("b", "b2"), o);
+            succeededWhenShouldntHave = true;
+        } catch (IllegalStateException e) {
+            //expected
+        }
+        assertFalse(succeededWhenShouldntHave);
+        assertEquals(o.b, "b");
+    }
+
+    @Test
+    public void testNonNullable() {
+        WithImmutableNonNullableObject o = new WithImmutableNonNullableObject();
+        //allowed
+        FlagUtils.setFieldsFromFlags(MutableMap.of("a", null), o);
+        assertEquals(o.a, null);
+        assertEquals(o.b, null);
+        //not allowed
+        boolean succeededWhenShouldntHave = false;
+        try {
+            FlagUtils.setFieldsFromFlags(MutableMap.of("b", null), o);
+            succeededWhenShouldntHave = true;
+        } catch (IllegalArgumentException e) {
+            //expected
+        }
+        assertFalse(succeededWhenShouldntHave);
+        assertEquals(o.b, null);
+    }
+    
+    @Test
+    public void testGetAnnotatedFields() throws Exception {
+        Map<Field, SetFromFlag> fm = FlagUtils.getAnnotatedFields(WithImmutableNonNullableObject.class);
+        assertEquals(fm.keySet().size(), 2);
+        assertTrue(fm.get(WithImmutableNonNullableObject.class.getDeclaredField("b")).immutable());
+    }
+
+    @Test
+    public void testCheckRequired() {
+        WithImmutableNonNullableObject f = new WithImmutableNonNullableObject();
+        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a is a"), f);
+        assertEquals(f.a, "a is a");
+        assertEquals(f.b, null);
+        int exceptions = 0;
+        try {
+            FlagUtils.checkRequiredFields(f);
+        } catch (IllegalStateException e) {
+            exceptions++;
+        }
+        assertEquals(exceptions, 1);
+    }
+
+    @Test
+    public void testSetConfigKeys() {
+        FooCK f = new FooCK();
+        Map<?,?> unused = FlagUtils.setFieldsFromFlags(ImmutableMap.of("f1", 9, "ck1", "do-set", "ck2", "dont-set", "c3", "do-set"), f);
+        assertEquals(f.bag.get(FooCK.CK1), "do-set");
+        assertEquals(f.bag.get(FooCK.CK3), "do-set");
+        assertEquals(f.f1, 9);
+        assertEquals(f.bag.containsKey(FooCK.CK2), false);
+        assertEquals(unused, ImmutableMap.of("ck2", "dont-set"));
+    }
+    
+    @Test
+    public void testSetAllConfigKeys() {
+        FooCK f = new FooCK();
+        Map<?,?> unused = FlagUtils.setAllConfigKeys(ImmutableMap.of("f1", 9, "ck1", "do-set", "ck2", "do-set-2", "c3", "do-set"), f, true);
+        assertEquals(f.bag.get(FooCK.CK1), "do-set");
+        assertEquals(f.bag.get(FooCK.CK3), "do-set");
+        assertEquals(f.bag.containsKey(FooCK.CK2), true);
+        assertEquals(f.bag.get(FooCK.CK2), "do-set-2");
+        assertEquals(unused, ImmutableMap.of("f1", 9));
+    }
+
+    @Test
+    public void testSetFromConfigKeys() {
+        FooCK f = new FooCK();
+        Map<?, ?> unused = FlagUtils.setFieldsFromFlags(ImmutableMap.of(new BasicConfigKey<Integer>(Integer.class, "f1"), 9, "ck1", "do-set", "ck2", "dont-set"), f);
+        assertEquals(f.bag.get(FooCK.CK1), "do-set");
+        assertEquals(f.f1, 9);
+        assertEquals(f.bag.containsKey(FooCK.CK2), false);
+        assertEquals(unused, ImmutableMap.of("ck2", "dont-set"));
+    }
+
+    public static class Foo {
+        @SetFromFlag
+        int w;
+        
+        @SetFromFlag(immutable=true)
+        private int x;
+        
+        @SetFromFlag("y")
+        public int yNotY;
+    }
+    
+    public static interface Bar {
+        static final String Z = "myzval";
+    }
+    
+    public static class Baz extends Foo implements Bar {
+        @SuppressWarnings("unused")  //inspected by reflection
+        private static int A;
+    }
+    
+    public static class WithImmutableNonNullableObject {
+        @SetFromFlag
+        Object a;
+        @SetFromFlag(immutable=true, nullable=false)
+        public Object b;
+    }
+    
+    public static class WithSpecialFieldTypes {
+        @SetFromFlag Set<?> set;
+        @SetFromFlag InetAddress inet;
+    }
+    
+    public static class FooCK implements Configurable {
+        @SetFromFlag
+        public static ConfigKey<String> CK1 = ConfigKeys.newStringConfigKey("ck1");
+        
+        public static ConfigKey<String> CK2 = ConfigKeys.newStringConfigKey("ck2");
+
+        @SetFromFlag("c3")
+        public static ConfigKey<String> CK3 = ConfigKeys.newStringConfigKey("ck3");
+
+        @SetFromFlag
+        int f1;
+        
+        ConfigBag bag = new ConfigBag();
+        BasicConfigurationSupport configSupport = new BasicConfigurationSupport();
+        
+        @Override
+        public ConfigurationSupport config() {
+            return configSupport;
+        }
+        
+        public <T> T setConfig(ConfigKey<T> key, T val) {
+            return config().set(key, val);
+        }
+        
+        private class BasicConfigurationSupport implements ConfigurationSupport {
+            @Override
+            public <T> T get(ConfigKey<T> key) {
+                return bag.get(key);
+            }
+
+            @Override
+            public <T> T get(HasConfigKey<T> key) {
+                return get(key.getConfigKey());
+            }
+
+            @Override
+            public <T> T set(ConfigKey<T> key, T val) {
+                T old = bag.get(key);
+                bag.configure(key, val);
+                return old;
+            }
+
+            @Override
+            public <T> T set(HasConfigKey<T> key, T val) {
+                return set(key.getConfigKey(), val);
+            }
+
+            @Override
+            public <T> T set(ConfigKey<T> key, Task<T> val) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public <T> T set(HasConfigKey<T> key, Task<T> val) {
+                return set(key.getConfigKey(), val);
+            }
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/internal/RepeaterTest.groovy
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/RepeaterTest.groovy b/core/src/test/java/org/apache/brooklyn/core/util/internal/RepeaterTest.groovy
new file mode 100644
index 0000000..08ef0da
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/RepeaterTest.groovy
@@ -0,0 +1,257 @@
+/*
+ * 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
+
+import static java.util.concurrent.TimeUnit.*
+import static org.testng.Assert.*
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit
+
+import org.apache.brooklyn.core.util.internal.Repeater;
+import org.testng.annotations.Test
+
+import brooklyn.util.internal.TimeExtras;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Stopwatch
+
+public class RepeaterTest {
+    static { TimeExtras.init() }
+
+    @Test
+    public void sanityTest() {
+        new Repeater("Sanity test")
+            .repeat()
+            .until { true }
+            .every(10 * MILLISECONDS);
+    }
+
+    @Test
+    public void sanityTestDescription() {
+        new Repeater()
+            .repeat()
+            .until { true }
+            .every(10 * MILLISECONDS);
+    }
+
+    @Test
+    public void sanityTestBuilder() {
+        Repeater.create("Sanity test")
+            .repeat()
+            .until { true }
+            .every(10 * MILLISECONDS);
+    }
+
+    @Test
+    public void sanityTestBuilderDescription() {
+        Repeater.create()
+            .repeat()
+            .until { true }
+            .every(10 * MILLISECONDS);
+    }
+
+    @Test(expectedExceptions = [ NullPointerException.class ])
+    public void repeatFailsIfClosureIsNull() {
+        new Repeater("repeatFailsIfClosureIsNull").repeat((Callable<?>)null);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test
+    public void repeatSucceedsIfClosureIsNonNull() {
+        new Repeater("repeatSucceedsIfClosureIsNonNull").repeat { true };
+    }
+
+    @Test(expectedExceptions = [ NullPointerException.class ])
+    public void untilFailsIfClosureIsNull() {
+        new Repeater("untilFailsIfClosureIsNull").until(null);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test
+    public void untilSucceedsIfClosureIsNonNull() {
+        new Repeater("untilSucceedsIfClosureIsNonNull").until { true };
+    }
+
+    @Test(expectedExceptions = [ IllegalArgumentException.class ])
+    public void everyFailsIfPeriodIsZero() {
+        new Repeater("everyFailsIfPeriodIsZero").every(0 * MILLISECONDS);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ IllegalArgumentException.class ])
+    public void everyFailsIfPeriodIsNegative() {
+        new Repeater("everyFailsIfPeriodIsNegative").every(-1 * MILLISECONDS);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ NullPointerException.class ])
+    public void everyFailsIfUnitsIsNull() {
+        new Repeater("everyFailsIfUnitsIsNull").every(10, null);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test
+    public void everySucceedsIfPeriodIsPositiveAndUnitsIsNonNull() {
+        new Repeater("repeatSucceedsIfClosureIsNonNull").every(10 * MILLISECONDS);
+    }
+
+    @Test(expectedExceptions = [ IllegalArgumentException.class ])
+    public void limitTimeToFailsIfPeriodIsZero() {
+        new Repeater("limitTimeToFailsIfPeriodIsZero").limitTimeTo(0, TimeUnit.MILLISECONDS);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ IllegalArgumentException.class ])
+    public void limitTimeToFailsIfPeriodIsNegative() {
+        new Repeater("limitTimeToFailsIfPeriodIsNegative").limitTimeTo(-1, TimeUnit.MILLISECONDS);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ NullPointerException.class ])
+    public void limitTimeToFailsIfUnitsIsNull() {
+        new Repeater("limitTimeToFailsIfUnitsIsNull").limitTimeTo(10, null);
+        fail "Expected exception was not thrown"
+    }
+
+    @Test
+    public void limitTimeToSucceedsIfPeriodIsPositiveAndUnitsIsNonNull() {
+        new Repeater("limitTimeToSucceedsIfClosureIsNonNull").limitTimeTo(10, TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void everyAcceptsDuration() {
+        new Repeater("everyAcceptsDuration").every(Duration.ONE_SECOND);
+    }
+
+    @Test
+    public void everyAcceptsLong() {
+        new Repeater("everyAcceptsLong").every(1000L);
+    }
+
+    @Test
+    public void everyAcceptsTimeUnit() {
+        new Repeater("everyAcceptsTimeUnit").every(1000000L, TimeUnit.MICROSECONDS);
+    }
+
+    @Test
+    public void runReturnsTrueIfExitConditionIsTrue() {
+        assertTrue new Repeater("runReturnsTrueIfExitConditionIsTrue")
+            .repeat()
+            .every(1 * MILLISECONDS)
+            .until { true }
+            .run();
+    }
+
+    @Test
+    public void runRespectsMaximumIterationLimitAndReturnsFalseIfReached() {
+        int iterations = 0;
+        assertFalse new Repeater("runRespectsMaximumIterationLimitAndReturnsFalseIfReached")
+            .repeat { iterations++ }
+            .every(1 * MILLISECONDS)
+            .until { false }
+            .limitIterationsTo(5)
+            .run();
+        assertEquals 5, iterations;
+    }
+
+    /**
+     * Check that the {@link Repeater} will stop after a time limit.
+     *
+     * The repeater is configured to run every 100ms and never stop until the limit is reached.
+     * This is given as {@link Repeater#limitTimeTo(groovy.time.Duration)} and the execution time
+     * is then checked to ensure it is between 100% and 400% of the specified value. Due to scheduling
+     * delays and other factors in a non RTOS system it is expected that the repeater will take much
+     * longer to exit occasionally.
+     *
+     * @see #runRespectsMaximumIterationLimitAndReturnsFalseIfReached()
+     */
+    @Test(groups="Integration")
+    public void runRespectsTimeLimitAndReturnsFalseIfReached() {
+        final long LIMIT = 2000l;
+        Repeater repeater = new Repeater("runRespectsTimeLimitAndReturnsFalseIfReached")
+            .repeat()
+            .every(100 * MILLISECONDS)
+            .until { false }
+            .limitTimeTo(LIMIT, TimeUnit.MILLISECONDS);
+
+        Stopwatch stopwatch = new Stopwatch().start();
+        boolean result = repeater.run();
+        stopwatch.stop();
+
+        assertFalse result;
+
+        long difference = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        assertTrue(difference >= LIMIT, "Difference was: " + difference);
+        assertTrue(difference < 4 * LIMIT, "Difference was: " + difference);
+    }
+
+    @Test(expectedExceptions = [ IllegalStateException.class ])
+    public void runFailsIfUntilWasNotSet() {
+        new Repeater("runFailsIfUntilWasNotSet")
+            .repeat()
+            .every(10 * MILLISECONDS)
+            .run();
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ IllegalStateException.class ])
+    public void runFailsIfEveryWasNotSet() {
+        new Repeater("runFailsIfEveryWasNotSet")
+            .repeat()
+            .until { true }
+            .run();
+        fail "Expected exception was not thrown"
+    }
+
+    @Test(expectedExceptions = [ UnsupportedOperationException.class ])
+    public void testRethrowsException() {
+        boolean result = new Repeater("throwRuntimeException")
+            .repeat()
+            .every(10 * MILLISECONDS)
+            .until { throw new UnsupportedOperationException("fail") }
+            .rethrowException()
+            .limitIterationsTo(2)
+            .run();
+        fail "Expected exception was not thrown"
+    }
+
+    @Test
+    public void testNoRethrowsException() {
+        try {
+	        boolean result = new Repeater("throwRuntimeException")
+	            .repeat()
+	            .every(10 * MILLISECONDS)
+	            .until { throw new UnsupportedOperationException("fail") }
+	            .limitIterationsTo(2)
+	            .run();
+	        assertFalse result
+        } catch (RuntimeException re) {
+            fail "Exception should not have been thrown: " + re.getMessage()
+        }
+    }
+	
+	public void testFlags() {
+		int count=0;
+		new Repeater(period: 5*MILLISECONDS, timeout: 100*MILLISECONDS).repeat({ count++ }).until({ count>100}).run();
+		assertTrue count>10
+		assertTrue count<30
+	}
+	
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/internal/TypeCoercionsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/TypeCoercionsTest.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/TypeCoercionsTest.java
new file mode 100644
index 0000000..f1bb3f9
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/TypeCoercionsTest.java
@@ -0,0 +1,360 @@
+/*
+ * 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;
+
+import static org.testng.Assert.assertEquals;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.core.util.flags.ClassCoercionException;
+import org.apache.brooklyn.core.util.flags.TypeCoercions;
+import org.codehaus.groovy.runtime.GStringImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.text.StringPredicates;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+
+public class TypeCoercionsTest {
+
+    private static final Logger log = LoggerFactory.getLogger(TypeCoercionsTest.class);
+    
+    @Test
+    public void testCoerceCharSequenceToString() {
+        assertEquals(TypeCoercions.coerce(new StringBuilder("abc"), String.class), "abc");
+        assertEquals(TypeCoercions.coerce(new GStringImpl(new Object[0], new String[0]), String.class), "");
+    }
+    
+    @Test
+    public void testCoerceStringToPrimitive() {
+        assertEquals(TypeCoercions.coerce("1", Character.class), (Character)'1');
+        assertEquals(TypeCoercions.coerce(" ", Character.class), (Character)' ');
+        assertEquals(TypeCoercions.coerce("1", Short.class), (Short)((short)1));
+        assertEquals(TypeCoercions.coerce("1", Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce("1", Long.class), (Long)1l);
+        assertEquals(TypeCoercions.coerce("1", Float.class), (Float)1f);
+        assertEquals(TypeCoercions.coerce("1", Double.class), (Double)1d);
+        assertEquals(TypeCoercions.coerce("true", Boolean.class), (Boolean)true);
+        assertEquals(TypeCoercions.coerce("False", Boolean.class), (Boolean)false);
+        assertEquals(TypeCoercions.coerce("true ", Boolean.class), (Boolean)true);
+
+        assertEquals(TypeCoercions.coerce("1", char.class), (Character)'1');
+        assertEquals(TypeCoercions.coerce("1", short.class), (Short)((short)1));
+        assertEquals(TypeCoercions.coerce("1", int.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce("1", long.class), (Long)1l);
+        assertEquals(TypeCoercions.coerce("1", float.class), (Float)1f);
+        assertEquals(TypeCoercions.coerce("1", double.class), (Double)1d);
+        assertEquals(TypeCoercions.coerce("TRUE", boolean.class), (Boolean)true);
+        assertEquals(TypeCoercions.coerce("false", boolean.class), (Boolean)false);
+    }
+
+    @Test
+    public void testCoercePrimitivesToSameType() {
+        assertEquals(TypeCoercions.coerce('1', Character.class), (Character)'1');
+        assertEquals(TypeCoercions.coerce((short)1, Short.class), (Short)((short)1));
+        assertEquals(TypeCoercions.coerce(1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce(1l, Long.class), (Long)1l);
+        assertEquals(TypeCoercions.coerce(1f, Float.class), (Float)1f);
+        assertEquals(TypeCoercions.coerce(1d, Double.class), (Double)1d);
+        assertEquals(TypeCoercions.coerce(true, Boolean.class), (Boolean)true);
+    }
+    
+    @Test
+    public void testCastPrimitives() {
+        assertEquals(TypeCoercions.coerce(1L, Character.class), (Character)(char)1);
+        assertEquals(TypeCoercions.coerce(1L, Byte.class), (Byte)(byte)1);
+        assertEquals(TypeCoercions.coerce(1L, Short.class), (Short)(short)1);
+        assertEquals(TypeCoercions.coerce(1L, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce(1L, Long.class), (Long)(long)1);
+        assertEquals(TypeCoercions.coerce(1L, Float.class), (Float)(float)1);
+        assertEquals(TypeCoercions.coerce(1L, Double.class), (Double)(double)1);
+        
+        assertEquals(TypeCoercions.coerce(1L, char.class), (Character)(char)1);
+        assertEquals(TypeCoercions.coerce(1L, byte.class), (Byte)(byte)1);
+        assertEquals(TypeCoercions.coerce(1L, short.class), (Short)(short)1);
+        assertEquals(TypeCoercions.coerce(1L, int.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce(1L, long.class), (Long)(long)1);
+        assertEquals(TypeCoercions.coerce(1L, float.class), (Float)(float)1);
+        assertEquals(TypeCoercions.coerce(1L, double.class), (Double)(double)1);
+        
+        assertEquals(TypeCoercions.coerce((char)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((byte)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((short)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((int)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((long)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((float)1, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce((double)1, Integer.class), (Integer)1);
+    }
+    
+    @Test
+    public void testCoercePrimitiveFailures() {
+        // error messages don't have to be this exactly, but they should include sufficient information...
+        assertCoercionFailsWithErrorMatching("maybe", boolean.class, StringPredicates.containsAllLiterals("String", "boolean", "maybe"));
+        assertCoercionFailsWithErrorMatching("NaN", int.class, StringPredicates.containsAllLiterals("int", "NaN"));
+        assertCoercionFailsWithErrorMatching('c', boolean.class, StringPredicates.containsAllLiterals("boolean", "(c)"));  // will say 'string' rather than 'char'
+        assertCoercionFailsWithErrorMatching(0, boolean.class, StringPredicates.containsAllLiterals("Integer", "boolean", "0"));
+    }
+    
+    protected void assertCoercionFailsWithErrorMatching(Object input, Class<?> type, Predicate<? super String> errorMessageRequirement) {
+        try {
+            Object result = TypeCoercions.coerce(input, type);
+            Assert.fail("Should have failed type coercion of "+input+" to "+type+", instead got: "+result);
+        } catch (Exception e) {
+            if (errorMessageRequirement==null || errorMessageRequirement.apply(e.toString()))
+                log.info("Primitive coercion failed as expected, with: "+e);
+            else
+                Assert.fail("Error from type coercion of "+input+" to "+type+" failed with wrong exception; expected match of "+errorMessageRequirement+" but got: "+e);
+        }
+        
+    }
+
+    @Test
+    public void testCastToNumericPrimitives() {
+        assertEquals(TypeCoercions.coerce(BigInteger.ONE, Integer.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce(BigInteger.ONE, int.class), (Integer)1);
+        assertEquals(TypeCoercions.coerce(BigInteger.valueOf(Long.MAX_VALUE), Long.class), (Long)Long.MAX_VALUE);
+        assertEquals(TypeCoercions.coerce(BigInteger.valueOf(Long.MAX_VALUE), long.class), (Long)Long.MAX_VALUE);
+        
+        assertEquals(TypeCoercions.coerce(BigDecimal.valueOf(0.5), Double.class), 0.5d, 0.00001d);
+        assertEquals(TypeCoercions.coerce(BigDecimal.valueOf(0.5), double.class), 0.5d, 0.00001d);
+    }
+
+    @Test
+    public void testCoerceStringToBigNumber() {
+    	assertEquals(TypeCoercions.coerce("0.5", BigDecimal.class), BigDecimal.valueOf(0.5));
+    	assertEquals(TypeCoercions.coerce("1", BigInteger.class), BigInteger.valueOf(1));
+    }
+
+    @Test
+    public void testCoerceStringToEnum() {
+        assertEquals(TypeCoercions.coerce("STARTING", Lifecycle.class), Lifecycle.STARTING);
+        assertEquals(TypeCoercions.coerce("Starting", Lifecycle.class), Lifecycle.STARTING);
+        assertEquals(TypeCoercions.coerce("starting", Lifecycle.class), Lifecycle.STARTING);
+        
+        assertEquals(TypeCoercions.coerce("LOWERCASE", PerverseEnum.class), PerverseEnum.lowercase);
+        assertEquals(TypeCoercions.coerce("CAMELCASE", PerverseEnum.class), PerverseEnum.camelCase);
+        assertEquals(TypeCoercions.coerce("upper", PerverseEnum.class), PerverseEnum.UPPER);
+        assertEquals(TypeCoercions.coerce("upper_with_underscore", PerverseEnum.class), PerverseEnum.UPPER_WITH_UNDERSCORE);
+        assertEquals(TypeCoercions.coerce("LOWER_WITH_UNDERSCORE", PerverseEnum.class), PerverseEnum.lower_with_underscore);
+    }
+    public static enum PerverseEnum {
+        lowercase,
+        camelCase,
+        UPPER,
+        UPPER_WITH_UNDERSCORE,
+        lower_with_underscore;
+    }
+    
+    @Test(expectedExceptions = ClassCoercionException.class)
+    public void testCoerceStringToEnumFailure() {
+        TypeCoercions.coerce("scrambled-eggs", Lifecycle.class);
+    }
+
+    @Test
+    public void testListToSetCoercion() {
+        Set<?> s = TypeCoercions.coerce(ImmutableList.of(1), Set.class);
+        Assert.assertEquals(s, ImmutableSet.of(1));
+    }
+    
+    @Test
+    public void testSetToListCoercion() {
+        List<?> s = TypeCoercions.coerce(ImmutableSet.of(1), List.class);
+        Assert.assertEquals(s, ImmutableList.of(1));
+    }
+    
+    @Test
+    public void testIterableToArrayCoercion() {
+        String[] s = TypeCoercions.coerce(ImmutableList.of("a", "b"), String[].class);
+        Assert.assertTrue(Arrays.equals(s, new String[] {"a", "b"}), "result="+Arrays.toString(s));
+        
+        Integer[] i = TypeCoercions.coerce(ImmutableList.of(1, 2), Integer[].class);
+        Assert.assertTrue(Arrays.equals(i, new Integer[] {1, 2}), "result="+Arrays.toString(i));
+        
+        int[] i2 = TypeCoercions.coerce(ImmutableList.of(1, 2), int[].class);
+        Assert.assertTrue(Arrays.equals(i2, new int[] {1, 2}), "result="+Arrays.toString(i2));
+        
+        int[] i3 = TypeCoercions.coerce(MutableSet.of("1", 2), int[].class);
+        Assert.assertTrue(Arrays.equals(i3, new int[] {1, 2}), "result="+Arrays.toString(i3));
+    }
+
+    @Test
+    public void testListEntryCoercion() {
+        List<?> s = TypeCoercions.coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<List<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class));
+    }
+    
+    @Test
+    public void testListEntryToSetCoercion() {
+        Set<?> s = TypeCoercions.coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<Set<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableSet.of(Integer.class, Double.class));
+    }
+    
+    @Test
+    public void testListEntryToCollectionCoercion() {
+        Collection<?> s = TypeCoercions.coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<Collection<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class));
+    }
+
+    @Test
+    public void testMapValueCoercion() {
+        Map<?,?> s = TypeCoercions.coerce(ImmutableMap.of("int", "java.lang.Integer", "double", "java.lang.Double"), new TypeToken<Map<String, Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableMap.of("int", Integer.class, "double", Double.class));
+    }
+    
+    @Test
+    public void testMapKeyCoercion() {
+        Map<?,?> s = TypeCoercions.coerce(ImmutableMap.of("java.lang.Integer", "int", "java.lang.Double", "double"), new TypeToken<Map<Class<?>, String>>() { });
+        Assert.assertEquals(s, ImmutableMap.of(Integer.class, "int", Double.class, "double"));
+    }
+
+    @Test
+    public void testStringToListCoercion() {
+        List<?> s = TypeCoercions.coerce("a,b,c", List.class);
+        Assert.assertEquals(s, ImmutableList.of("a", "b", "c"));
+    }
+
+    @Test
+    @SuppressWarnings("serial")
+    public void testCoerceRecursivelyStringToGenericsCollection() {
+        assertEquals(TypeCoercions.coerce("1,2", new TypeToken<List<Integer>>() {}), ImmutableList.of(1, 2));
+    }
+    
+    @Test
+    public void testJsonStringToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("{ \"a\" : \"1\", b : 2 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testJsonStringWithoutQuotesToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("{ a : 1 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1));
+    }
+
+    @Test
+    public void testJsonComplexTypesToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("{ a : [1, \"2\", '\"3\"'], b: { c: d, 'e': \"f\" } }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", ImmutableList.<Object>of(1, "2", "\"3\""), 
+            "b", ImmutableMap.of("c", "d", "e", "f")));
+    }
+
+    @Test
+    public void testJsonStringWithoutBracesToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("a : 1", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1));
+    }
+
+    @Test
+    public void testJsonStringWithoutBracesWithMultipleToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("a : 1, b : 2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", 2));
+    }
+
+    @Test
+    public void testKeyEqualsValueStringToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("a=1,b=2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", "2"));
+    }
+
+    @Test(expectedExceptions=IllegalArgumentException.class)
+    public void testJsonStringWithoutBracesOrSpaceDisallowedAsMapCoercion() {
+        // yaml requires spaces after the colon
+        Map<?,?> s = TypeCoercions.coerce("a:1,b:2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", 2));
+    }
+    
+    @Test
+    public void testEqualsInBracesMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("{ a = 1, b = '2' }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", "2"));
+    }
+
+    @Test
+    public void testKeyEqualsOrColonValueWithBracesStringToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("{ a=1, b: 2 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testKeyEqualsOrColonValueWithoutBracesStringToMapCoercion() {
+        Map<?,?> s = TypeCoercions.coerce("a=1, b: 2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testAs() {
+        Integer x = TypeCoercions.coerce(new WithAs("3"), Integer.class);
+        Assert.assertEquals(x, (Integer)3);
+    }
+
+    @Test
+    public void testFrom() {
+        WithFrom x = TypeCoercions.coerce("3", WithFrom.class);
+        Assert.assertEquals(x.value, 3);
+    }
+
+    @Test
+    public void testCoerceStringToNumber() {
+        assertEquals(TypeCoercions.coerce("1", Number.class), (Number) Double.valueOf(1));
+        assertEquals(TypeCoercions.coerce("1.0", Number.class), (Number) Double.valueOf(1.0));
+    }
+
+    @Test(expectedExceptions = ClassCoercionException.class)
+    public void testInvalidCoercionThrowsClassCoercionException() {
+        TypeCoercions.coerce(new Object(), TypeToken.of(Integer.class));
+    }
+
+    @Test
+    public void testCoercionFunction() {
+        assertEquals(TypeCoercions.function(Double.class).apply("1"), Double.valueOf(1));
+    }
+
+    public static class WithAs {
+        String value;
+        public WithAs(Object x) { value = ""+x; }
+        public Integer asInteger() {
+            return Integer.parseInt(value);
+        }
+    }
+
+    public static class WithFrom {
+        int value;
+        public static WithFrom fromString(String s) {
+            WithFrom result = new WithFrom();
+            result.value = Integer.parseInt(s);
+            return result;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a4c0e5fd/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/RecordingSshTool.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/RecordingSshTool.java b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/RecordingSshTool.java
new file mode 100644
index 0000000..fdf3e73
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/util/internal/ssh/RecordingSshTool.java
@@ -0,0 +1,97 @@
+/*
+ * 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.File;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+/** Mock tool */
+public class RecordingSshTool implements SshTool {
+    
+    public static class ExecCmd {
+        public final Map<String,?> props;
+        public final String summaryForLogging;
+        public final List<String> commands;
+        public final Map<?,?> env;
+        
+        ExecCmd(Map<String,?> props, String summaryForLogging, List<String> commands, Map env) {
+            this.props = props;
+            this.summaryForLogging = summaryForLogging;
+            this.commands = commands;
+            this.env = env;
+        }
+        
+        @Override
+        public String toString() {
+            return "ExecCmd["+summaryForLogging+": "+commands+"; "+props+"; "+env+"]";
+        }
+    }
+    
+    public static List<ExecCmd> execScriptCmds = Lists.newCopyOnWriteArrayList();
+    
+    private boolean connected;
+    
+    public RecordingSshTool(Map<?,?> props) {
+    }
+    @Override public void connect() {
+        connected = true;
+    }
+    @Override public void connect(int maxAttempts) {
+        connected = true;
+    }
+    @Override public void disconnect() {
+        connected = false;
+    }
+    @Override public boolean isConnected() {
+        return connected;
+    }
+    @Override public int execScript(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
+        execScriptCmds.add(new ExecCmd(props, "", commands, env));
+        return 0;
+    }
+    @Override public int execScript(Map<String, ?> props, List<String> commands) {
+        return execScript(props, commands, ImmutableMap.<String,Object>of());
+    }
+    @Override public int execCommands(Map<String, ?> props, List<String> commands, Map<String, ?> env) {
+        execScriptCmds.add(new ExecCmd(props, "", commands, env));
+        return 0;
+    }
+    @Override public int execCommands(Map<String, ?> props, List<String> commands) {
+        return execCommands(props, commands, ImmutableMap.<String,Object>of());
+    }
+    @Override public int copyToServer(Map<String, ?> props, File localFile, String pathAndFileOnRemoteServer) {
+        return 0;
+    }
+    @Override public int copyToServer(Map<String, ?> props, InputStream contents, String pathAndFileOnRemoteServer) {
+        return 0;
+    }
+    @Override public int copyToServer(Map<String, ?> props, byte[] contents, String pathAndFileOnRemoteServer) {
+        return 0;
+    }
+    @Override public int copyFromServer(Map<String, ?> props, String pathAndFileOnRemoteServer, File local) {
+        return 0;
+    }
+}
\ No newline at end of file