brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [05/35] incubator-brooklyn git commit: [BROOKLYN-162] package rename to org.apache.brooklyn: software/webapp
Date Wed, 12 Aug 2015 15:55:23 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
new file mode 100644
index 0000000..ed1d47a
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxLightIntegrationTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.testng.Assert.assertEquals;
+
+import java.net.URL;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.proxy.StubAppServer;
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.BasicConfigurableEntityFactory;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.test.Asserts;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+
+public class NginxLightIntegrationTest extends BrooklynAppUnitTestSupport {
+
+    private NginxController nginx;
+    private DynamicCluster cluster;
+
+    // FIXME Fails because getting addEntity callback for group members while nginx is still starting,
+    // so important nginx fields are still null. Therefore get NPE for cluster members, and thus targets
+    // is of size zero.
+    @Test(groups = {"Integration", "WIP"})
+    public void testNginxTargetsMatchesClusterMembers() {
+        EntityFactory<StubAppServer> serverFactory = new BasicConfigurableEntityFactory<StubAppServer>(StubAppServer.class);
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 2)
+                .configure("factory", serverFactory));
+                
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", cluster)
+                .configure("domain", "localhost"));
+        
+        app.start(ImmutableList.of(new LocalhostMachineProvisioningLocation()));
+        
+        // Wait for url-mapping to update its TARGET_ADDRESSES (through async subscription)
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                Map<Entity, String> expectedTargets = Maps.newLinkedHashMap();
+                for (Entity member : cluster.getMembers()) {
+                    expectedTargets.put(member, member.getAttribute(Attributes.HOSTNAME)+":"+member.getAttribute(Attributes.HTTP_PORT));
+                }                
+                assertEquals(nginx.getAttribute(NginxController.SERVER_POOL_TARGETS).size(), 2);
+                assertEquals(nginx.getAttribute(NginxController.SERVER_POOL_TARGETS), expectedTargets);
+            }});
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindIntegrationTest.java
new file mode 100644
index 0000000..353790d
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindIntegrationTest.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.apache.brooklyn.test.EntityTestUtils.assertAttributeEqualsEventually;
+import static org.apache.brooklyn.test.HttpTestUtils.assertHttpStatusCodeEquals;
+import static org.apache.brooklyn.test.HttpTestUtils.assertHttpStatusCodeEventuallyEquals;
+import static org.testng.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.proxy.nginx.UrlMapping;
+import org.apache.brooklyn.entity.proxy.nginx.UrlRewriteRule;
+import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8Server;
+import org.apache.brooklyn.management.ManagementContext;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.apache.brooklyn.test.WebAppMonitor;
+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.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.BasicGroup;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.rebind.RebindOptions;
+import brooklyn.entity.rebind.RebindTestFixtureWithApp;
+import brooklyn.location.LocationSpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.test.Asserts;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+/**
+ * Test the operation of the {@link NginxController} class.
+ */
+public class NginxRebindIntegrationTest extends RebindTestFixtureWithApp {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NginxRebindIntegrationTest.class);
+
+    private LocalhostMachineProvisioningLocation localhostProvisioningLocation;
+    private List<WebAppMonitor> webAppMonitors = new CopyOnWriteArrayList<WebAppMonitor>();
+    private ExecutorService executor;
+    
+    @Override
+    protected boolean useLiveManagementContext() {
+        // For Aled, the test failed without own ~/.brooklyn/brooklyn.properties.
+        // Suspect that was caused by local environment, with custom brooklyn.ssh.config.scriptHeader
+        // to set things like correct Java on path.
+        return true;
+    }
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+        localhostProvisioningLocation = origManagementContext.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+        executor = Executors.newCachedThreadPool();
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        for (WebAppMonitor monitor : webAppMonitors) {
+            monitor.terminate();
+        }
+        webAppMonitors.clear();
+        if (executor != null) executor.shutdownNow();
+        super.tearDown();
+    }
+
+    private WebAppMonitor newWebAppMonitor(String url, int expectedResponseCode) {
+        WebAppMonitor monitor = new WebAppMonitor(url)
+//                .delayMillis(0) FIXME Re-enable to fast polling
+                .expectedResponseCode(expectedResponseCode)
+                .logFailures(LOG);
+        webAppMonitors.add(monitor);
+        executor.execute(monitor);
+        return monitor;
+    }
+    
+    /**
+     * Test can rebind to the simplest possible nginx configuration (i.e. no server pool).
+     */
+    @Test(groups = "Integration")
+    public void testRebindsWithEmptyServerPool() throws Exception {
+        
+        // Set up nginx with a server pool
+        DynamicCluster origServerPool = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class))
+                .configure("initialSize", 0));
+        
+        NginxController origNginx = origApp.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", origServerPool)
+                .configure("domain", "localhost"));
+        
+        // Start the app, and ensure reachable; start polling the URL
+        origApp.start(ImmutableList.of(localhostProvisioningLocation));
+        
+        String rootUrl = origNginx.getAttribute(NginxController.ROOT_URL);
+        int nginxPort = origNginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        assertHttpStatusCodeEventuallyEquals(rootUrl, 404);
+        WebAppMonitor monitor = newWebAppMonitor(rootUrl, 404);
+        final String origConfigFile = origNginx.getConfigFile();
+        
+        newApp = rebind(RebindOptions.create().terminateOrigManagementContext(true));
+        final NginxController newNginx = (NginxController) Iterables.find(newApp.getChildren(), Predicates.instanceOf(NginxController.class));
+
+        assertEquals(newNginx.getConfigFile(), origConfigFile);
+        
+        EntityTestUtils.assertAttributeEqualsEventually(newNginx, NginxController.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        assertEquals(newNginx.getAttribute(NginxController.PROXY_HTTP_PORT), (Integer)nginxPort);
+        assertEquals(newNginx.getAttribute(NginxController.ROOT_URL), rootUrl);
+        assertEquals(newNginx.getAttribute(NginxController.PROXY_HTTP_PORT), origNginx.getAttribute(NginxController.PROXY_HTTP_PORT));
+        assertEquals(newNginx.getConfig(NginxController.STICKY), origNginx.getConfig(NginxController.STICKY));
+        
+        assertAttributeEqualsEventually(newNginx, SoftwareProcess.SERVICE_UP, true);
+        assertHttpStatusCodeEventuallyEquals(rootUrl, 404);
+        
+        assertEquals(monitor.getFailures(), 0);
+    }
+    
+    /**
+     * Test can rebind with an active server pool.
+     */
+    @Test(groups = "Integration")
+    public void testRebindsWithoutLosingServerPool() throws Exception {
+        
+        // Set up nginx with a server pool
+        DynamicCluster origServerPool = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("war", getTestWar()))
+                .configure("initialSize", 1));
+        
+        NginxController origNginx = origApp.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", origServerPool)
+                .configure("domain", "localhost"));
+        
+        // Start the app, and ensure reachable; start polling the URL
+        origApp.start(ImmutableList.of(localhostProvisioningLocation));
+        
+        String rootUrl = origNginx.getAttribute(NginxController.ROOT_URL);
+        Tomcat8Server origServer = (Tomcat8Server) Iterables.getOnlyElement(origServerPool.getMembers());
+        assertEquals(origNginx.getAttribute(NginxController.SERVER_POOL_TARGETS).keySet(), ImmutableSet.of(origServer));
+        
+        assertHttpStatusCodeEventuallyEquals(rootUrl, 200);
+        WebAppMonitor monitor = newWebAppMonitor(rootUrl, 200);
+        final String origConfigFile = origNginx.getConfigFile();
+        
+        // Rebind
+        newApp = rebind(RebindOptions.create().terminateOrigManagementContext(true));
+        ManagementContext newManagementContext = newApp.getManagementContext();
+        final NginxController newNginx = (NginxController) Iterables.find(newApp.getChildren(), Predicates.instanceOf(NginxController.class));
+        final DynamicCluster newServerPool = (DynamicCluster) newManagementContext.getEntityManager().getEntity(origServerPool.getId());
+        final Tomcat8Server newServer = (Tomcat8Server) Iterables.getOnlyElement(newServerPool.getMembers());
+
+        // Expect continually to have same nginx members; should not lose them temporarily!
+        Asserts.succeedsContinually(new Runnable() {
+            public void run() {
+                Map<Entity, String> newNginxMemebers = newNginx.getAttribute(NginxController.SERVER_POOL_TARGETS);
+                assertEquals(newNginxMemebers.keySet(), ImmutableSet.of(newServer));
+            }});
+        
+        
+        assertAttributeEqualsEventually(newNginx, SoftwareProcess.SERVICE_UP, true);
+        assertHttpStatusCodeEventuallyEquals(rootUrl, 200);
+
+        assertEquals(newNginx.getConfigFile(), origConfigFile);
+        
+        // Check that an update doesn't break things
+        newNginx.update();
+
+        assertHttpStatusCodeEquals(rootUrl, 200);
+
+        // Resize new cluster, and confirm change takes affect.
+        //  - Increase size
+        //  - wait for nginx to definitely be updates (TODO nicer way to wait for updated?)
+        //  - terminate old server
+        //  - confirm can still route messages
+        newServerPool.resize(2);
+        
+        Thread.sleep(10*1000);
+        
+        newServer.stop();
+
+        assertHttpStatusCodeEventuallyEquals(rootUrl, 200);
+
+        // Check that URLs have been constantly reachable
+        assertEquals(monitor.getFailures(), 0);
+    }
+    
+    
+    /**
+     * Test can rebind to the with server pool and URL remappings.
+     * NOTE: This requires a redirection from localhost1 to 127.0.0.1 in your /etc/hosts file
+     */
+    @Test(groups = "Integration")
+    public void testRebindsWithoutLosingUrlMappings() throws Exception {
+        
+        // Set up nginx with a url-mapping
+        Group origUrlMappingsGroup = origApp.createAndManageChild(EntitySpec.create(BasicGroup.class)
+                .configure("childrenAsMembers", true));
+        
+        DynamicCluster origServerPool = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("war", getTestWar()))
+                .configure("initialSize", 1)); 
+
+        UrlMapping origMapping = origApp.getManagementContext().getEntityManager().createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", origServerPool)
+                .configure("rewrites", ImmutableList.of(new UrlRewriteRule("/foo/(.*)", "/$1")))
+                .parent(origUrlMappingsGroup));
+        Entities.manage(origMapping);
+        
+        NginxController origNginx = origApp.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("domain", "localhost")
+                .configure("urlMappings", origUrlMappingsGroup));
+
+        // Start the app, and ensure reachable; start polling the URL
+        origApp.start(ImmutableList.of(localhostProvisioningLocation));
+        
+        String mappingGroupUrl = "http://localhost1:"+origNginx.getAttribute(NginxController.PROXY_HTTP_PORT)+"/foo/";
+
+        assertHttpStatusCodeEventuallyEquals(mappingGroupUrl, 200);
+        WebAppMonitor monitor = newWebAppMonitor(mappingGroupUrl, 200);
+        final String origConfigFile = origNginx.getConfigFile();
+        
+        // Create a rebinding
+        newApp = rebind(RebindOptions.create().terminateOrigManagementContext(true));
+        ManagementContext newManagementContext = newApp.getManagementContext();
+        final NginxController newNginx = (NginxController) Iterables.find(newApp.getChildren(), Predicates.instanceOf(NginxController.class));
+        DynamicCluster newServerPool = (DynamicCluster) newManagementContext.getEntityManager().getEntity(origServerPool.getId());
+        Tomcat8Server newServer = (Tomcat8Server) Iterables.getOnlyElement(newServerPool.getMembers());
+        
+        assertAttributeEqualsEventually(newNginx, SoftwareProcess.SERVICE_UP, true);
+        assertHttpStatusCodeEventuallyEquals(mappingGroupUrl, 200);
+        
+        assertEquals(newNginx.getConfigFile(), origConfigFile);
+        
+        // Check that an update doesn't break things
+        newNginx.update();
+
+        assertHttpStatusCodeEquals(mappingGroupUrl, 200);
+
+        // Resize new cluster, and confirm change takes affect.
+        //  - Increase size
+        //  - wait for nginx to definitely be updates (TODO nicer way to wait for updated?)
+        //  - terminate old server
+        //  - confirm can still route messages
+        newServerPool.resize(2);
+        
+        Thread.sleep(10*1000);
+        
+        newServer.stop();
+
+        assertHttpStatusCodeEquals(mappingGroupUrl, 200);
+
+        // Check that URLs have been constantly reachable
+        assertEquals(monitor.getFailures(), 0);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindWithHaIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindWithHaIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindWithHaIntegrationTest.java
new file mode 100644
index 0000000..a283bba
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxRebindWithHaIntegrationTest.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.tomcat.TomcatServer;
+import org.apache.brooklyn.management.Task;
+import org.apache.brooklyn.management.ha.HighAvailabilityMode;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.apache.brooklyn.test.WebAppMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.Feed;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.rebind.RebindTestFixtureWithApp;
+import brooklyn.entity.rebind.RebindTestUtils;
+import brooklyn.internal.BrooklynFeatureEnablement;
+import brooklyn.location.LocationSpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.location.basic.SshMachineLocationReuseIntegrationTest.RecordingSshjTool;
+import brooklyn.management.internal.LocalManagementContext;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.internal.ssh.SshTool;
+import brooklyn.util.net.Networking;
+import brooklyn.util.repeat.Repeater;
+import brooklyn.util.task.BasicExecutionManager;
+import brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+/**
+ * Test the operation of the {@link NginxController} class.
+ */
+public class NginxRebindWithHaIntegrationTest extends RebindTestFixtureWithApp {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NginxRebindWithHaIntegrationTest.class);
+
+    private List<WebAppMonitor> webAppMonitors = new CopyOnWriteArrayList<WebAppMonitor>();
+    private ExecutorService executor;
+    private LocalhostMachineProvisioningLocation loc;
+
+    private Boolean feedRegistration;
+    
+    @Override
+    protected boolean useLiveManagementContext() {
+        // For Aled, the test failed without own ~/.brooklyn/brooklyn.properties.
+        // Suspect that was caused by local environment, with custom brooklyn.ssh.config.scriptHeader
+        // to set things like correct Java on path.
+        return true;
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+            .configure("address", Networking.getLocalHost())
+            .configure(SshTool.PROP_TOOL_CLASS, RecordingSshjTool.class.getName()));
+        executor = Executors.newCachedThreadPool();
+        
+        feedRegistration = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_FEED_REGISTRATION_PROPERTY);
+        BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_FEED_REGISTRATION_PROPERTY, true);
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        try {
+            if (feedRegistration!=null)
+                BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_FEED_REGISTRATION_PROPERTY, feedRegistration);
+
+            for (WebAppMonitor monitor : webAppMonitors) {
+                monitor.terminate();
+            }
+            webAppMonitors.clear();
+            if (executor != null) executor.shutdownNow();
+            super.tearDown();
+        } finally {
+            RecordingSshjTool.reset();
+        }
+    }
+    
+    @Override
+    protected TestApplication createApp() {
+        origManagementContext.getHighAvailabilityManager().changeMode(HighAvailabilityMode.MASTER);
+        return super.createApp();
+    }
+
+    @Test(groups = "Integration")
+    public void testChangeModeFailureStopsTasksButHappyUponResumption() throws Exception {
+        DynamicCluster origServerPool = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TomcatServer.class).configure("war", getTestWar()))
+                .configure("initialSize", 1));
+        
+        NginxController origNginx = origApp.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", origServerPool)
+                .configure("domain", "localhost"));
+        
+        origApp.start(ImmutableList.of(loc));
+        Assert.assertTrue(RecordingSshjTool.connectionCount.get()>0);
+
+        Collection<Feed> origFeeds = ((EntityInternal)origNginx).feeds().getFeeds();
+        LOG.info("feeds before rebind are: "+origFeeds);
+        Assert.assertTrue(origFeeds.size() >= 1);
+
+        origManagementContext.getRebindManager().forcePersistNow();
+
+        List<Task<?>> tasksBefore = ((BasicExecutionManager)origManagementContext.getExecutionManager()).getAllTasks();
+        LOG.info("tasks before disabling HA, "+tasksBefore.size()+": "+tasksBefore);
+        Assert.assertFalse(tasksBefore.isEmpty());
+        origManagementContext.getHighAvailabilityManager().changeMode(HighAvailabilityMode.DISABLED);
+        origApp = null;
+        
+        Repeater.create().every(Duration.millis(20)).backoffTo(Duration.ONE_SECOND).limitTimeTo(Duration.THIRTY_SECONDS).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                origManagementContext.getGarbageCollector().gcIteration();
+                List<Task<?>> tasksAfter = ((BasicExecutionManager)origManagementContext.getExecutionManager()).getAllTasks();
+                LOG.info("tasks after disabling HA, "+tasksAfter.size()+": "+tasksAfter);
+                return tasksAfter.isEmpty();
+            }
+        }).runRequiringTrue();
+        
+        // disable SSH to localhost to ensure we don't try to ssh while rebinding
+        
+        RecordingSshjTool.forbidden.set(true);
+        newManagementContext = createNewManagementContext();
+        newApp = (TestApplication) RebindTestUtils.rebind((LocalManagementContext)newManagementContext, classLoader);
+
+        NginxController newNginx = Iterables.getOnlyElement(Entities.descendants(newApp, NginxController.class));
+        
+        Collection<Feed> newFeeds = ((EntityInternal)newNginx).feeds().getFeeds();
+        LOG.info("feeds after rebind are: "+newFeeds);
+        Assert.assertTrue(newFeeds.size() >= 1);
+        
+        // eventually goes on fire, because we disabled ssh
+        EntityTestUtils.assertAttributeEqualsEventually(newNginx, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        
+        // re-enable SSH and it should right itself
+        RecordingSshjTool.forbidden.set(false);
+        EntityTestUtils.assertAttributeEqualsEventually(newNginx, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
new file mode 100644
index 0000000..822c16d
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxUrlMappingIntegrationTest.java
@@ -0,0 +1,528 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.proxy.nginx.UrlMapping;
+import org.apache.brooklyn.entity.proxy.nginx.UrlRewriteRule;
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
+import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8Server;
+import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8ServerImpl;
+import org.apache.brooklyn.management.EntityManager;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.BrooklynAppLiveTestSupport;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.BasicGroup;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.test.Asserts;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+/**
+ * Test the operation of the {@link NginxController} class, for URL mapped groups (two different pools).
+ * 
+ * These tests require that /etc/hosts contains some extra entries, such as:
+ *     127.0.0.1       localhost localhost1 localhost2 localhost3 localhost4
+ */
+public class NginxUrlMappingIntegrationTest extends BrooklynAppLiveTestSupport {
+    
+    // TODO Make JBoss7Server.deploy wait for the web-app to actually be deployed.
+    // That may simplify some of the tests, because we can assert some things immediately rather than in a succeedsEventually.
+    
+    private static final Logger log = LoggerFactory.getLogger(NginxUrlMappingIntegrationTest.class);
+
+    private NginxController nginx;
+    private Group urlMappingsGroup;
+    private EntityManager entityManager;
+    private LocalhostMachineProvisioningLocation localLoc;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        urlMappingsGroup = app.createAndManageChild(EntitySpec.create(BasicGroup.class)
+                .configure("childrenAsMembers", true));
+        entityManager = app.getManagementContext().getEntityManager();
+        
+        localLoc = new LocalhostMachineProvisioningLocation();
+    }
+
+    public String getTestWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
+        return "classpath://hello-world.war";
+    }
+
+    protected void checkExtraLocalhosts() throws Exception {
+        Set<String> failedHosts = Sets.newLinkedHashSet();
+        List<String> allHosts = ImmutableList.of("localhost", "localhost1", "localhost2", "localhost3", "localhost4");
+        for (String host : allHosts) {
+            try {
+                InetAddress i = InetAddress.getByName(host);
+                byte[] b = ((Inet4Address)i).getAddress();
+                if (b[0]!=127 || b[1]!=0 || b[2]!=0 || b[3]!=1) {
+                    log.warn("Failed to resolve "+host+" (test will subsequently fail, but looking for more errors first; see subsequent failure for more info): wrong IP "+Arrays.asList(b));
+                    failedHosts.add(host);
+                }
+            } catch (Exception e) {
+                log.warn("Failed to resolve "+host+" (test will subsequently fail, but looking for more errors first; see subsequent failure for more info): "+e, e);
+                failedHosts.add(host);
+            }
+        }
+        if (!failedHosts.isEmpty()) {
+            fail("These tests (in "+this+") require special hostnames to map to 127.0.0.1, in /etc/hosts: "+failedHosts);
+        }
+    }
+    
+    @Test(groups = "Integration")
+    public void testUrlMappingServerNameAndPath() throws Exception {
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        //cluster 0 mounted at localhost1 /
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        Entities.manage(u0);
+        
+        //cluster 1 at localhost2 /hello-world/
+        DynamicCluster c1 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.NAMED_WARS, ImmutableList.of(getTestWar())));
+        UrlMapping u1 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost2")
+                .configure("path", "/hello-world($|/.*)")
+                .configure("target", c1)
+                .parent(urlMappingsGroup));
+        Entities.manage(u1);
+
+        // cluster 2 at localhost3 /c2/  and mapping /hello/xxx to /hello/new xxx
+        DynamicCluster c2 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+")));
+        UrlMapping u2 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost3")
+                .configure("path", "/c2($|/.*)")
+                .configure("target", c2)
+                .configure("rewrites", ImmutableList.of(new UrlRewriteRule("(.*/|)(hello/)(.*)", "$1$2new $3").setBreak()))
+                .parent(urlMappingsGroup));
+        Entities.manage(u2);
+        // FIXME rewrite not a config
+        
+        app.start(ImmutableList.of(localLoc));
+        final int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        for (Entity member : c2.getMembers()) {
+            ((Tomcat8Server)member).deploy(getTestWar(), "c2.war");
+        }
+    
+        Entities.dumpInfo(app);
+        
+        // Confirm routes requests to the correct cluster
+        // Do more than one request for each in-case just lucky with round-robin...
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                //cluster 0
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost1:"+port, "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/frank", "http://localhost1:"+port+"/hello/frank");
+                }
+                //cluster 1
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost2:"+port+"/hello-world", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost2:"+port+"/hello-world/", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost2:"+port+"/hello-world/hello/bob", "http://localhost2:"+port+"/hello-world/hello/bob");
+                }
+                //cluster 2
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost3:"+port+"/c2", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost3:"+port+"/c2/", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost3:"+port+"/c2/hello/joe", "http://localhost3:"+port+"/c2/hello/new%20joe");
+                }
+            }});
+        
+        //these should *not* be available
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost:"+port+"/", 404);
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost1:"+port+"/hello-world", 404);
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost2:"+port+"/", 404);
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost2:"+port+"/hello-world/notexists", 404);
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost3:"+port+"/", 404);
+        
+        // TODO previously said "make sure nginx default welcome page isn't displayed",
+        // but the assertion only worked because threw exception on 404 trying to read
+        // stdin of http connection. If reading stderr of http connection, we do see
+        // "ginx" in the output. Why were we asserting this? Can we just delete it?
+        // Previous code was:
+        //     Asserts.assertFails { HttpTestUtils.assertContentContainsText([timeout:1], "http://localhost:${port}/", "ginx"); }
+    }
+
+    @Test(groups = "Integration")
+    public void testUrlMappingRoutesRequestByPathToCorrectGroup() throws Exception {
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+")));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost")
+                .configure("path", "/atC0($|/.*)")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        Entities.manage(u0);
+
+        DynamicCluster c1 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+")));
+        UrlMapping u1 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost")
+                .configure("path", "/atC1($|/.*)")
+                .configure("target", c1)
+                .parent(urlMappingsGroup));
+        Entities.manage(u1);
+
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("domain", "localhost")
+                .configure("port", "8000+")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        app.start(ImmutableList.of(localLoc));
+        final int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        for (Entity child : c0.getMembers()) {
+            ((Tomcat8Server)child).deploy(getTestWar(), "atC0.war");
+        }
+        for (Entity child : c1.getMembers()) {
+            ((Tomcat8Server)child).deploy(getTestWar(), "atC1.war");
+        }
+
+        // Confirm routes requests to the correct cluster
+        // Do more than one request for each in-case just lucky with round-robin...
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC0", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC0/", "Hello");
+                }
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC1", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC1/", "Hello");
+                }
+            }});
+    }
+    
+    @Test(groups = "Integration")
+    public void testUrlMappingRemovedWhenMappingEntityRemoved() throws Exception {
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost2")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        Entities.manage(u0);
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("domain", "localhost")
+                .configure("port", "8000+")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        app.start(ImmutableList.of(localLoc));
+        int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        // Wait for deployment to be successful
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals("http://localhost2:"+port+"/", 200);
+        
+        // Now remove mapping; will no longer route requests
+        Entities.unmanage(u0);
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals("http://localhost2:"+port+"/", 404);
+    }
+    
+    @Test(groups = "Integration")
+    public void testWithCoreClusterAndUrlMappedGroup() throws Exception {
+        // TODO Should use different wars, so can confirm content from each cluster
+        // TODO Could also assert on: nginx.getConfigFile()
+        
+        checkExtraLocalhosts();
+        
+        DynamicCluster coreCluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        
+        DynamicCluster c1 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.NAMED_WARS, ImmutableList.of(getTestWar())));
+        UrlMapping u1 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", c1)
+                .parent(urlMappingsGroup));
+        Entities.manage(u1);
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("serverPool", coreCluster)
+                .configure("domain", "localhost")
+                .configure("port", "8000+")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("urlMappings", urlMappingsGroup));
+                
+        app.start(ImmutableList.of(localLoc));
+        final int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        // check nginx forwards localhost1 to c1, and localhost to core group 
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello-world", "Hello");
+                HttpTestUtils.assertHttpStatusCodeEquals("http://localhost1:"+port+"", 404);
+                
+                HttpTestUtils.assertContentContainsText("http://localhost:"+port+"", "Hello");
+                HttpTestUtils.assertHttpStatusCodeEquals("http://localhost:"+port+"/hello-world", 404);
+            }});
+    }
+    
+    @Test(groups = "Integration")
+    public void testUrlMappingMultipleRewrites() throws Exception {
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("urlMappings", urlMappingsGroup));
+    
+        //cluster 0 mounted at localhost1 /
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        u0.addRewrite("/goodbye/al(.*)", "/hello/al$1");
+        u0.addRewrite(new UrlRewriteRule("/goodbye(|/.*)$", "/hello$1").setBreak());
+        u0.addRewrite("(.*)/hello/al(.*)", "$1/hello/Big Al$2");
+        u0.addRewrite("/hello/an(.*)", "/hello/Sir An$1");
+        Entities.manage(u0);
+
+        app.start(ImmutableList.of(localLoc));
+        final int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        // Confirm routes requests to the correct cluster
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                // health check
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"", "Hello");
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/frank", "http://localhost1:"+port+"/hello/frank");
+                
+                // goodbye rewritten to hello
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/goodbye/frank", "http://localhost1:"+port+"/hello/frank");
+                // hello al rewritten to hello Big Al
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/aled", "http://localhost1:"+port+"/hello/Big%20Aled");
+                // hello andrew rewritten to hello Sir Andrew
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/andrew", "http://localhost1:"+port+"/hello/Sir%20Andrew");
+                
+                // goodbye alex rewritten to hello Big Alex (two rewrites)
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/goodbye/alex", "http://localhost1:"+port+"/hello/Big%20Alex");
+                // but goodbye andrew rewritten only to hello Andrew -- test the "break" logic above (won't continue rewriting)
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/goodbye/andrew", "http://localhost1:"+port+"/hello/andrew");
+                
+                // al rewrite can be anywhere
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/hello/alex", "http://localhost1:"+port+"/hello/hello/Big%20Alex");
+                // but an rewrite must be at beginning
+                HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"/hello/hello/andrew", "http://localhost1:"+port+"/hello/hello/andrew");
+            }});
+    }
+    
+    @Test(groups = "Integration")
+    public void testUrlMappingGroupRespondsToScaleOut() throws Exception {
+        checkExtraLocalhosts();
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("domain", "localhost")
+                .configure("port", "8000+")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        final DynamicCluster c1 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        final UrlMapping u1 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", c1)
+                .parent(urlMappingsGroup));
+        Entities.manage(u1);
+        
+        app.start(ImmutableList.of(localLoc));
+        int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        Entity c1jboss = Iterables.getOnlyElement(c1.getMembers());
+        
+        // Wait for app-server to be responsive, and url-mapping to update its TARGET_ADDRESSES (through async subscription)
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                // Entities.dumpInfo(app);
+                assertEquals(u1.getAttribute(UrlMapping.TARGET_ADDRESSES).size(), 1);
+            }});
+
+        // check nginx forwards localhost1 to c1
+        HttpTestUtils.assertContentEventuallyContainsText("http://localhost1:"+port+"", "Hello");
+        
+        // Resize target cluster of url-mapping
+        c1.resize(2);
+        List c1jbosses = new ArrayList(c1.getMembers());
+        c1jbosses.remove(c1jboss);
+        // the unnecessary (Entity) cast is required as a work-around to an IntelliJ issue that prevents Brooklyn from launching from the IDE
+        Entity c1jboss2 = (Entity)Iterables.getOnlyElement(c1jbosses);
+
+        // TODO Have to wait for new app-server; should fix app-servers to block
+        // Also wait for TARGET_ADDRESSES to update
+        assertAppServerRespondsEventually(c1jboss2);
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                assertEquals(u1.getAttribute(UrlMapping.TARGET_ADDRESSES).size(), 2);
+            }});
+
+        // check jboss2 is included in nginx rules
+        // TODO Should getConfigFile return the current config file, rather than recalculate?
+        //      This assertion isn't good enough to tell if it's been deployed.
+        final String c1jboss2addr = c1jboss2.getAttribute(Attributes.HOSTNAME)+":"+c1jboss2.getAttribute(Attributes.HTTP_PORT);
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                String conf = nginx.getConfigFile();
+                assertTrue(conf.contains(c1jboss2addr), "could not find "+c1jboss2addr+" in:\n"+conf);
+            }});
+        
+        // and check forwarding to c1 by nginx still works
+        for (int i = 0; i < 2; i++) {
+            HttpTestUtils.assertContentContainsText("http://localhost1:"+port+"", "Hello");
+        }
+    }
+    
+    @Test(groups = "Integration")
+    public void testUrlMappingWithEmptyCoreCluster() throws Exception {
+        DynamicCluster nullCluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+            .configure("initialSize", 0)
+            .configure("factory", new EntityFactory<Entity>() {
+                public Entity newEntity(Map flags, Entity parent) {
+                    throw new UnsupportedOperationException();
+                }}));
+
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+")));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost")
+                .configure("path", "/atC0($|/.*)")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        Entities.manage(u0);
+
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("cluster", nullCluster)
+                .configure("domain", "localhost")
+                .configure("port", "8000+")
+                .configure("portNumberSensor", WebAppService.HTTP_PORT)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        app.start(ImmutableList.of(localLoc));
+        final int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        for (Entity child : c0.getMembers()) {
+            ((Tomcat8Server)child).deploy(getTestWar(), "atC0.war");
+        }
+
+        // Confirm routes requests to the correct cluster
+        // Do more than one request for each in-case just lucky with round-robin...
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                for (int i = 0; i < 2; i++) {
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC0/", "Hello");
+                    HttpTestUtils.assertContentContainsText("http://localhost:"+port+"/atC0", "Hello");
+                }
+            }});
+
+        // And empty-core should return 404
+        HttpTestUtils.assertHttpStatusCodeEquals("http://localhost:"+port+"", 404);
+    }
+    
+    @Test(groups = "Integration")
+    public void testDiscardUrlMapping() throws Exception {
+        //cluster 0 mounted at localhost1 /
+        DynamicCluster c0 = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(Tomcat8Server.class).configure("httpPort", "8100+"))
+                .configure(JavaWebAppService.ROOT_WAR, getTestWar()));
+        UrlMapping u0 = entityManager.createEntity(EntitySpec.create(UrlMapping.class)
+                .configure("domain", "localhost1")
+                .configure("target", c0)
+                .parent(urlMappingsGroup));
+        Entities.manage(u0);
+
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("urlMappings", urlMappingsGroup));
+        
+        app.start(ImmutableList.of(localLoc));
+        int port = nginx.getAttribute(NginxController.PROXY_HTTP_PORT);
+        
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals("http://localhost1:"+port+"", 200);
+
+        // Discard, and confirm that subsequently get a 404 instead
+        u0.discard();
+        
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals("http://localhost1:"+port+"", 404);
+    }
+
+    private void assertAppServerRespondsEventually(Entity server) {
+        String hostname = server.getAttribute(Attributes.HOSTNAME);
+        int port = server.getAttribute(Attributes.HTTP_PORT);
+        HttpTestUtils.assertHttpStatusCodeEventuallyEquals("http://"+hostname+":"+port, 200);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxWebClusterEc2LiveTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxWebClusterEc2LiveTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxWebClusterEc2LiveTest.java
new file mode 100644
index 0000000..76c4bb5
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/proxy/nginx/NginxWebClusterEc2LiveTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.proxy.nginx;
+
+import static org.testng.Assert.assertNotNull;
+
+import java.net.URL;
+
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
+import org.apache.brooklyn.management.ManagementContext;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+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.entity.Entity;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.location.Location;
+import brooklyn.location.MachineLocation;
+import brooklyn.location.basic.Machines;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.TestApplication;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test Nginx proxying a cluster of JBoss7Server entities on AWS for ENGR-1689.
+ *
+ * This test is a proof-of-concept for the Brooklyn demo application, with each
+ * service running on a separate Amazon EC2 instance.
+ */
+public class NginxWebClusterEc2LiveTest {
+    private static final Logger LOG = LoggerFactory.getLogger(NginxWebClusterEc2LiveTest.class);
+    
+    private TestApplication app;
+    private NginxController nginx;
+    private DynamicCluster cluster;
+    private Location loc;
+
+    @BeforeMethod(alwaysRun = true)
+    public void setUp() {
+        ManagementContext managementContext = Entities.newManagementContext(
+                ImmutableMap.of("brooklyn.location.jclouds.aws-ec2.image-id", "us-east-1/ami-2342a94a"));
+        
+        loc = managementContext.getLocationRegistry().resolve("aws-ec2:us-east-1");
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void shutdown() {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test(groups = "Live")
+    public void testProvisionAwsCluster() {
+        String warName = "/hello-world.war";
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), warName);
+        URL war = getClass().getResource(warName);
+        assertNotNull(war, "Unable to locate resource "+warName);
+        
+        cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class))
+                .configure("initialSize", 2)
+                .configure("httpPort", 8080)
+                .configure(JavaWebAppService.ROOT_WAR, war.getPath()));
+        
+        nginx = app.createAndManageChild(EntitySpec.create(NginxController.class)
+                .configure("cluster", cluster)
+                .configure("domain", "localhost")
+                .configure("port", 8000)
+                .configure("portNumberSensor", WebAppService.HTTP_PORT));
+
+        app.start(ImmutableList.of(loc));
+        
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                // Nginx URL is available
+                MachineLocation machine = Machines.findUniqueMachineLocation(nginx.getLocations()).get();
+                String url = "http://" + machine.getAddress().getHostName() + ":" + nginx.getAttribute(NginxController.PROXY_HTTP_PORT) + "/swf-booking-mvc";
+                HttpTestUtils.assertHttpStatusCodeEquals(url, 200);
+    
+                // Web-app URL is available
+                for (Entity member : cluster.getMembers()) {
+                    HttpTestUtils.assertHttpStatusCodeEquals(member.getAttribute(JavaWebAppService.ROOT_URL) + "swf-booking-mvc", 200);
+                }
+            }});
+
+        nginx.stop();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
new file mode 100644
index 0000000..f21a14a
--- /dev/null
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
@@ -0,0 +1,506 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.webapp;
+
+import static org.apache.brooklyn.test.HttpTestUtils.connectToUrl;
+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 java.io.File;
+import java.io.FileOutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import brooklyn.entity.basic.SoftwareProcessDriver;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import brooklyn.config.BrooklynProperties;
+import brooklyn.entity.Application;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.drivers.DriverDependentEntity;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.location.LocationSpec;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.test.Asserts;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppService;
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcess;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.entity.webapp.WebAppServiceMethods;
+import org.apache.brooklyn.management.ManagementContext;
+import org.apache.brooklyn.management.SubscriptionContext;
+import org.apache.brooklyn.management.SubscriptionHandle;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.test.TestResourceUnavailableException;
+
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.crypto.FluentKeySigner;
+import brooklyn.util.crypto.SecureKeys;
+import brooklyn.util.net.Urls;
+import brooklyn.util.stream.Streams;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+/**
+ * Test fixture for implementations of JavaWebApp, checking start up and shutdown, 
+ * post request and error count metrics and deploy wars, etc.
+ */
+public abstract class AbstractWebAppFixtureIntegrationTest {
+    
+    private static final Logger log = LoggerFactory.getLogger(AbstractWebAppFixtureIntegrationTest.class);
+    
+    // Don't use 8080 since that is commonly used by testing software
+    public static final String DEFAULT_HTTP_PORT = "7880+";
+    
+    // Port increment for JBoss 6.
+    public static final int PORT_INCREMENT = 400;
+
+    // The parent application entity for these tests
+    protected ManagementContext mgmt;
+    protected List<TestApplication> applications = Lists.newArrayList();
+    protected SoftwareProcess entity;
+    protected LocalhostMachineProvisioningLocation loc;
+
+    protected synchronized ManagementContext getMgmt() {
+        if (mgmt==null)
+            mgmt = new LocalManagementContextForTests(BrooklynProperties.Factory.newDefault());
+        return mgmt;
+    }
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        loc = getMgmt().getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class));
+    }
+    
+    /*
+     * Use of @DataProvider with test methods gives surprising behaviour with @AfterMethod.
+     * Unless careful, this causes problems when trying to ensure everything is shutdown cleanly.
+     *
+     * Empirically, the rules seem to be...
+     *  - @DataProvider method is called first; it creates a bunch of cases to run 
+     *    (all sharing the same instance of WebAppIntegrationTest).
+     *  - It runs the test method for the first time with the first @DataProvider value
+     *  - It runs @AfterMethod
+     *  - It runs the test method for the second @DataProvider value
+     *  - It runs @AfterMethod
+     *  - etc...
+     *
+     * Previously shutdownApp was calling stop on each app in applications, and clearing the applications set;
+     * but then the second invocation of the method was starting an entity that was never stopped. Until recently,
+     * every test method was also terminating the entity (belt-and-braces, but also brittle for if the method threw
+     * an exception earlier). When that "extra" termination was removed, it meant the second and subsequent 
+     * entities were never being stopped.
+     *
+     * Now we rely on having the test method set the entity field, so we can find out which application instance 
+     * it is and calling stop on just that app + entity.
+     */
+    @AfterMethod(alwaysRun=true)
+    public void shutdownApp() {
+        if (entity != null) {
+            Application app = entity.getApplication();
+            if (app != null) Entities.destroy(app);
+        }
+    }
+
+    @AfterClass(alwaysRun=true)
+    public synchronized void shutdownMgmt() {
+        try {
+            if (mgmt != null) Entities.destroyAll(mgmt);
+        } finally {
+            mgmt = null;
+        }
+    }
+
+    public static File createTemporaryKeyStore(String alias, String password) throws Exception {
+        FluentKeySigner signer = new FluentKeySigner("brooklyn-test").selfsign();
+
+        KeyStore ks = SecureKeys.newKeyStore();
+        ks.setKeyEntry(
+                alias,
+                signer.getKey().getPrivate(),
+                password.toCharArray(),
+                new Certificate[]{signer.getAuthorityCertificate()});
+
+        File file = File.createTempFile("test", "keystore");
+        FileOutputStream fos = new FileOutputStream(file);
+        try {
+            ks.store(fos, password.toCharArray());
+            return file;
+        } finally {
+            Streams.closeQuietly(fos);
+        }
+    }
+
+    /** 
+     * Create a new instance of TestApplication and append it to applications list
+     * so it can be terminated suitable after each test has run.
+     * @return
+     */
+    protected TestApplication newTestApplication() {
+        TestApplication ta = ApplicationBuilder.newManagedApp(TestApplication.class, getMgmt());
+        applications.add(ta);
+        return ta;
+    }
+
+    /**
+     * Provides instances of the WebAppServer to test
+     * (arrays of 1-element array arguments to some of the other methods) 
+     *
+     * NB annotation must be placed on concrete impl method
+     * 
+     * TODO combine the data provider here with live integration test
+     * @see WebAppLiveIntegrationTest#basicEntities()
+     */
+    @DataProvider(name = "basicEntities")
+    public abstract Object[][] basicEntities();
+
+    /**
+     * Checks an entity can start, set SERVICE_UP to true and shutdown again.
+     */
+    @Test(groups = "Integration", dataProvider = "basicEntities")
+    public void canStartAndStop(final SoftwareProcess entity) {
+        this.entity = entity;
+        log.info("test=canStartAndStop; entity="+entity+"; app="+entity.getApplication());
+        
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        Asserts.succeedsEventually(MutableMap.of("timeout", 120*1000), new Runnable() {
+            public void run() {
+                assertTrue(entity.getAttribute(Startable.SERVICE_UP));
+            }});
+        
+        entity.stop();
+        assertFalse(entity.getAttribute(Startable.SERVICE_UP));
+    }
+    
+    /**
+     * Checks an entity can start, set SERVICE_UP to true and shutdown again.
+     */
+    @Test(groups = "Integration", dataProvider = "basicEntities")
+    public void testReportsServiceDownWhenKilled(final SoftwareProcess entity) throws Exception {
+        this.entity = entity;
+        log.info("test=testReportsServiceDownWithKilled; entity="+entity+"; app="+entity.getApplication());
+        
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        EntityTestUtils.assertAttributeEqualsEventually(MutableMap.of("timeout", 120*1000), entity, Startable.SERVICE_UP, true);
+
+        // Stop the underlying entity, but without our entity instance being told!
+        killEntityBehindBack(entity);
+        log.info("Killed {} behind mgmt's back, waiting for service up false in mgmt context", entity);
+        
+        EntityTestUtils.assertAttributeEqualsEventually(entity, Startable.SERVICE_UP, false);
+        
+        log.info("success getting service up false in primary mgmt universe");
+    }
+    
+    /**
+     * Stop the given underlying entity, but without our entity instance being told!
+     */
+    protected void killEntityBehindBack(Entity tokill) throws Exception {
+        ((SoftwareProcessDriver)((DriverDependentEntity<?>) Entities.deproxy(entity)).getDriver()).stop();
+        // old method of doing this did some dodgy legacy rebind and failed due to too many dangling refs; above is better in any case
+        // but TODO we should have some rebind tests for these!
+    }
+    
+    /**
+     * Checks that an entity correctly sets request and error count metrics by
+     * connecting to a non-existent URL several times.
+     */
+    @Test(groups = "Integration", dataProvider = "basicEntities")
+    public void publishesRequestAndErrorCountMetrics(final SoftwareProcess entity) throws Exception {
+        this.entity = entity;
+        log.info("test=publishesRequestAndErrorCountMetrics; entity="+entity+"; app="+entity.getApplication());
+        
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", 10*1000), new Runnable() {
+            public void run() {
+                assertTrue(entity.getAttribute(SoftwareProcess.SERVICE_UP));
+            }});
+        
+        String url = entity.getAttribute(WebAppService.ROOT_URL) + "does_not_exist";
+        
+        final int n = 10;
+        for (int i = 0; i < n; i++) {
+            URLConnection connection = HttpTestUtils.connectToUrl(url);
+            int status = ((HttpURLConnection) connection).getResponseCode();
+            log.info("connection to {} gives {}", url, status);
+        }
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", 20*1000), new Runnable() {
+            public void run() {
+                Integer requestCount = entity.getAttribute(WebAppService.REQUEST_COUNT);
+                Integer errorCount = entity.getAttribute(WebAppService.ERROR_COUNT);
+                log.info("req={}, err={}", requestCount, errorCount);
+                
+                assertNotNull(errorCount, "errorCount not set yet ("+errorCount+")");
+    
+                // AS 7 seems to take a very long time to report error counts,
+                // hence not using ==.  >= in case error pages include a favicon, etc.
+                assertEquals(errorCount, (Integer)n);
+                assertTrue(requestCount >= errorCount);
+            }});
+    }
+    
+    /**
+     * Checks an entity publishes correct requests/second figures and that these figures
+     * fall to zero after a period of no activity.
+     */
+    @Test(groups = "Integration", dataProvider = "basicEntities")
+    public void publishesRequestsPerSecondMetric(final SoftwareProcess entity) throws Exception {
+        this.entity = entity;
+        log.info("test=publishesRequestsPerSecondMetric; entity="+entity+"; app="+entity.getApplication());
+        
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+
+        log.info("Entity "+entity+" started");
+        
+        try {
+            // reqs/sec initially zero
+            log.info("Waiting for initial avg-requests to be zero...");
+            Asserts.succeedsEventually(MutableMap.of("timeout", 20*1000), new Runnable() {
+                public void run() {
+                    Double activityValue = entity.getAttribute(WebAppService.REQUESTS_PER_SECOND_IN_WINDOW);
+                    assertNotNull(activityValue, "activity not set yet "+activityValue+")");
+                    assertEquals(activityValue.doubleValue(), 0.0d, 0.000001d);
+                }});
+            
+            // apply workload on 1 per sec; reqs/sec should update
+            Asserts.succeedsEventually(MutableMap.of("timeout", 30*1000), new Callable<Void>() {
+                public Void call() throws Exception {
+                    String url = entity.getAttribute(WebAppService.ROOT_URL) + "does_not_exist";
+                    final int desiredMsgsPerSec = 10;
+                    
+                    Stopwatch stopwatch = Stopwatch.createStarted();
+                    final AtomicInteger reqsSent = new AtomicInteger();
+                    final Integer preRequestCount = entity.getAttribute(WebAppService.REQUEST_COUNT);
+                    
+                    // need to maintain n requests per second for the duration of the window size
+                    log.info("Applying load for "+WebAppServiceMethods.DEFAULT_WINDOW_DURATION);
+                    while (stopwatch.elapsed(TimeUnit.MILLISECONDS) < WebAppServiceMethods.DEFAULT_WINDOW_DURATION.toMilliseconds()) {
+                        long preReqsTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+                        for (int i = 0; i < desiredMsgsPerSec; i++) { connectToUrl(url); }
+                        Time.sleep(1000 - (stopwatch.elapsed(TimeUnit.MILLISECONDS)-preReqsTime));
+                        reqsSent.addAndGet(desiredMsgsPerSec);
+                    }
+    
+                    Asserts.succeedsEventually(MutableMap.of("timeout", 4000), new Runnable() {
+                        public void run() {
+                            Double avgReqs = entity.getAttribute(WebAppService.REQUESTS_PER_SECOND_IN_WINDOW);
+                            Integer requestCount = entity.getAttribute(WebAppService.REQUEST_COUNT);
+                            
+                            log.info("avg-requests="+avgReqs+"; total-requests="+requestCount);
+                            assertEquals(avgReqs.doubleValue(), (double)desiredMsgsPerSec, 3.0d);
+                            assertEquals(requestCount.intValue(), preRequestCount+reqsSent.get());
+                        }});
+                    
+                    return null;
+                }});
+            
+            // After suitable delay, expect to again get zero msgs/sec
+            log.info("Waiting for avg-requests to drop to zero, for "+WebAppServiceMethods.DEFAULT_WINDOW_DURATION);
+            Thread.sleep(WebAppServiceMethods.DEFAULT_WINDOW_DURATION.toMilliseconds());
+            
+            Asserts.succeedsEventually(MutableMap.of("timeout", 10*1000), new Runnable() {
+                public void run() {
+                    Double avgReqs = entity.getAttribute(WebAppService.REQUESTS_PER_SECOND_IN_WINDOW);
+                    assertNotNull(avgReqs);
+                    assertEquals(avgReqs.doubleValue(), 0.0d, 0.00001d);
+                }});
+        } finally {
+            entity.stop();
+        }
+    }
+
+    /**
+     * Tests that we get consecutive events with zero workrate, and with suitably small timestamps between them.
+     */
+    @Test(groups = "Integration", dataProvider = "basicEntities")
+    @SuppressWarnings("rawtypes")
+    public void publishesZeroRequestsPerSecondMetricRepeatedly(final SoftwareProcess entity) {
+        this.entity = entity;
+        log.info("test=publishesZeroRequestsPerSecondMetricRepeatedly; entity="+entity+"; app="+entity.getApplication());
+        
+        final int MAX_INTERVAL_BETWEEN_EVENTS = 4000; // TomcatServerImpl publishes events every 3000ms so this should be enough overhead
+        final int NUM_CONSECUTIVE_EVENTS = 3;
+
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        
+        SubscriptionHandle subscriptionHandle = null;
+        SubscriptionContext subContext = ((EntityInternal)entity).getSubscriptionContext();
+
+        try {
+            final List<SensorEvent> events = new CopyOnWriteArrayList<SensorEvent>();
+            subscriptionHandle = subContext.subscribe(entity, WebAppService.REQUESTS_PER_SECOND_IN_WINDOW, new SensorEventListener<Double>() {
+                public void onEvent(SensorEvent<Double> event) {
+                    log.info("publishesRequestsPerSecondMetricRepeatedly.onEvent: {}", event);
+                    events.add(event);
+                }});
+            
+            
+            Asserts.succeedsEventually(new Runnable() {
+                public void run() {
+                    assertTrue(events.size() > NUM_CONSECUTIVE_EVENTS, "events "+events.size()+" > "+NUM_CONSECUTIVE_EVENTS);
+                    long eventTime = 0;
+                    
+                    for (SensorEvent event : events.subList(events.size()-NUM_CONSECUTIVE_EVENTS, events.size())) {
+                        assertEquals(event.getSource(), entity);
+                        assertEquals(event.getSensor(), WebAppService.REQUESTS_PER_SECOND_IN_WINDOW);
+                        assertEquals(event.getValue(), 0.0d);
+                        if (eventTime > 0) assertTrue(event.getTimestamp()-eventTime < MAX_INTERVAL_BETWEEN_EVENTS,
+                            "events at "+eventTime+" and "+event.getTimestamp()+" exceeded maximum allowable interval "+MAX_INTERVAL_BETWEEN_EVENTS);
+                        eventTime = event.getTimestamp();
+                    }
+                }});
+        } finally {
+            if (subscriptionHandle != null) subContext.unsubscribe(subscriptionHandle);
+            entity.stop();
+        }
+    }
+
+    /**
+     * Twins the entities given by basicEntities() with links to WAR files
+     * they should be able to deploy.  Correct deployment can be checked by
+     * pinging the given URL.
+     *
+     * Everything can deploy hello world. Some subclasses deploy add'l apps.
+     * We're using the simplest hello-world (with no URL mapping) because JBoss 6 does not
+     * support URL mappings.
+     */
+    @DataProvider(name = "entitiesWithWarAndURL")
+    public Object[][] entitiesWithWar() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world-no-mapping.war");
+        List<Object[]> result = Lists.newArrayList();
+        
+        for (Object[] entity : basicEntities()) {
+            result.add(new Object[] {
+                    entity[0],
+                    "hello-world-no-mapping.war",
+                    "hello-world-no-mapping/",
+                    "" // no sub-page path
+                    });
+        }
+        
+        return result.toArray(new Object[][] {});
+    }
+
+    /**
+     * Tests given entity can deploy the given war.  Checks given httpURL to confirm success.
+     */
+    @Test(groups = "Integration", dataProvider = "entitiesWithWarAndURL")
+    public void initialRootWarDeployments(final SoftwareProcess entity, final String war, 
+            final String urlSubPathToWebApp, final String urlSubPathToPageToQuery) {
+        this.entity = entity;
+        log.info("test=initialRootWarDeployments; entity="+entity+"; app="+entity.getApplication());
+        
+        URL resource = getClass().getClassLoader().getResource(war);
+        assertNotNull(resource);
+        
+        ((EntityLocal)entity).setConfig(JavaWebAppService.ROOT_WAR, resource.toString());
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        
+        //tomcat may need a while to unpack everything
+        Asserts.succeedsEventually(MutableMap.of("timeout", 60*1000), new Runnable() {
+            public void run() {
+                // TODO get this URL from a WAR file entity
+                HttpTestUtils.assertHttpStatusCodeEquals(Urls.mergePaths(entity.getAttribute(WebAppService.ROOT_URL), urlSubPathToPageToQuery), 200);
+                
+                assertEquals(entity.getAttribute(JavaWebAppSoftwareProcess.DEPLOYED_WARS), ImmutableSet.of("/"));
+            }});
+    }
+    
+    @Test(groups = "Integration", dataProvider = "entitiesWithWarAndURL")
+    public void initialNamedWarDeployments(final SoftwareProcess entity, final String war, 
+            final String urlSubPathToWebApp, final String urlSubPathToPageToQuery) {
+        this.entity = entity;
+        log.info("test=initialNamedWarDeployments; entity="+entity+"; app="+entity.getApplication());
+        
+        URL resource = getClass().getClassLoader().getResource(war);
+        assertNotNull(resource);
+        
+        ((EntityLocal)entity).setConfig(JavaWebAppService.NAMED_WARS, ImmutableList.of(resource.toString()));
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+
+        Asserts.succeedsEventually(MutableMap.of("timeout", 60*1000), new Runnable() {
+            public void run() {
+                // TODO get this URL from a WAR file entity
+                HttpTestUtils.assertHttpStatusCodeEquals(Urls.mergePaths(entity.getAttribute(WebAppService.ROOT_URL), urlSubPathToWebApp, urlSubPathToPageToQuery), 200);
+            }});
+    }
+    
+    @Test(groups = "Integration", dataProvider = "entitiesWithWarAndURL")
+    public void testWarDeployAndUndeploy(final JavaWebAppSoftwareProcess entity, final String war, 
+            final String urlSubPathToWebApp, final String urlSubPathToPageToQuery) {
+        this.entity = entity;
+        log.info("test=testWarDeployAndUndeploy; entity="+entity+"; app="+entity.getApplication());
+        
+        URL resource = getClass().getClassLoader().getResource(war);;
+        assertNotNull(resource);
+        
+        Entities.start(entity.getApplication(), ImmutableList.of(loc));
+        
+        // Test deploying
+        entity.deploy(resource.toString(), "myartifactname.war");
+        Asserts.succeedsEventually(MutableMap.of("timeout", 60*1000), new Runnable() {
+            public void run() {
+                // TODO get this URL from a WAR file entity
+                HttpTestUtils.assertHttpStatusCodeEquals(Urls.mergePaths(entity.getAttribute(WebAppService.ROOT_URL), "myartifactname/", urlSubPathToPageToQuery), 200);
+                assertEquals(entity.getAttribute(JavaWebAppSoftwareProcess.DEPLOYED_WARS), ImmutableSet.of("/myartifactname"));
+            }});
+        
+        // And undeploying
+        entity.undeploy("/myartifactname");
+        Asserts.succeedsEventually(MutableMap.of("timeout", 60*1000), new Runnable() {
+            public void run() {
+                // TODO get this URL from a WAR file entity
+                HttpTestUtils.assertHttpStatusCodeEquals(Urls.mergePaths(entity.getAttribute(WebAppService.ROOT_URL), "myartifactname", urlSubPathToPageToQuery), 404);
+                assertEquals(entity.getAttribute(JavaWebAppSoftwareProcess.DEPLOYED_WARS), ImmutableSet.of());
+            }});
+    }
+}


Mime
View raw message