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/09 04:55:34 UTC

[27/28] incubator-brooklyn git commit: Fix conflicts from #806. This closes #806.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2aac052f/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
----------------------------------------------------------------------
diff --cc usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
index 0000000,f33820d..a323496
mode 000000,100644..100644
--- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
+++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/SensorResourceTest.java
@@@ -1,0 -1,272 +1,273 @@@
+ /*
+  * 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.rest.resources;
+ 
+ import static org.testng.Assert.assertEquals;
+ 
+ import java.util.Map;
+ 
+ import javax.ws.rs.core.MediaType;
+ import javax.ws.rs.core.Response;
+ 
++import org.apache.brooklyn.test.HttpTestUtils;
+ import org.testng.annotations.AfterClass;
+ import org.testng.annotations.BeforeClass;
+ import org.testng.annotations.Test;
+ 
+ import brooklyn.config.render.RendererHints;
+ import brooklyn.config.render.TestRendererHints;
+ import brooklyn.entity.basic.EntityInternal;
+ import brooklyn.entity.basic.EntityPredicates;
+ import brooklyn.event.AttributeSensor;
+ import brooklyn.event.basic.Sensors;
+ import org.apache.brooklyn.rest.api.SensorApi;
+ import org.apache.brooklyn.rest.domain.ApplicationSpec;
+ import org.apache.brooklyn.rest.domain.EntitySpec;
+ import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
+ import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
 -import brooklyn.test.HttpTestUtils;
++import org.apache.brooklyn.test.HttpTestUtils;
+ import brooklyn.util.collections.MutableMap;
+ import brooklyn.util.stream.Streams;
+ import brooklyn.util.text.StringFunctions;
+ 
+ import com.google.common.base.Functions;
+ import com.google.common.collect.ImmutableSet;
+ import com.google.common.collect.Iterables;
+ import com.sun.jersey.api.client.ClientResponse;
+ import com.sun.jersey.api.client.GenericType;
+ import com.sun.jersey.api.client.WebResource;
+ import com.sun.jersey.api.client.WebResource.Builder;
+ 
+ /**
+  * Test the {@link SensorApi} implementation.
+  * <p>
+  * Check that {@link SensorResource} correctly renders {@link AttributeSensor}
+  * values, including {@link RendererHints.DisplayValue} hints.
+  */
+ @Test(singleThreaded = true)
+ public class SensorResourceTest extends BrooklynRestResourceTest {
+ 
+     final static ApplicationSpec SIMPLE_SPEC = ApplicationSpec.builder()
+             .name("simple-app")
+             .entities(ImmutableSet.of(new EntitySpec("simple-ent", RestMockSimpleEntity.class.getName())))
+             .locations(ImmutableSet.of("localhost"))
+             .build();
+ 
+     static final String SENSORS_ENDPOINT = "/v1/applications/simple-app/entities/simple-ent/sensors";
+     static final String SENSOR_NAME = "amphibian.count";
+     static final AttributeSensor<Integer> SENSOR = Sensors.newIntegerSensor(SENSOR_NAME);
+ 
+     EntityInternal entity;
+ 
+     /**
+      * Sets up the application and entity.
+      * <p>
+      * Adds a sensor and sets its value to {@code 12345}. Configures a display value
+      * hint that appends {@code frogs} to the value of the sensor.
+      */
+     @BeforeClass(alwaysRun = true)
+     @Override
+     public void setUp() throws Exception {
+         super.setUp();
+ 
+         // Deploy application
+         ClientResponse deploy = clientDeploy(SIMPLE_SPEC);
+         waitForApplicationToBeRunning(deploy.getLocation());
+ 
+         entity = (EntityInternal) Iterables.find(getManagementContext().getEntityManager().getEntities(), EntityPredicates.displayNameEqualTo("simple-ent"));
+         addAmphibianSensor(entity);
+     }
+ 
+     static void addAmphibianSensor(EntityInternal entity) {
+         // Add new sensor
+         entity.getMutableEntityType().addSensor(SENSOR);
+         entity.setAttribute(SENSOR, 12345);
+ 
+         // Register display value hint
+         RendererHints.register(SENSOR, RendererHints.displayValue(Functions.compose(StringFunctions.append(" frogs"), Functions.toStringFunction())));
+     }
+ 
+     @AfterClass(alwaysRun = true)
+     @Override
+     public void tearDown() throws Exception {
+         TestRendererHints.clearRegistry();
+         super.tearDown();
+     }
+ 
+     /** Check default is to use display value hint. */
+     @Test
+     public void testBatchSensorRead() throws Exception {
+         ClientResponse response = client().resource(SENSORS_ENDPOINT + "/current-state")
+                 .accept(MediaType.APPLICATION_JSON)
+                 .get(ClientResponse.class);
+         Map<String, ?> currentState = response.getEntity(new GenericType<Map<String,?>>(Map.class) {});
+ 
+         for (String sensor : currentState.keySet()) {
+             if (sensor.equals(SENSOR_NAME)) {
+                 assertEquals(currentState.get(sensor), "12345 frogs");
+             }
+         }
+     }
+ 
+     /** Check setting {@code raw} to {@code true} ignores display value hint. */
+     @Test(dependsOnMethods = "testBatchSensorRead")
+     public void testBatchSensorReadRaw() throws Exception {
+         ClientResponse response = client().resource(SENSORS_ENDPOINT + "/current-state")
+                 .queryParam("raw", "true")
+                 .accept(MediaType.APPLICATION_JSON)
+                 .get(ClientResponse.class);
+         Map<String, ?> currentState = response.getEntity(new GenericType<Map<String,?>>(Map.class) {});
+ 
+         for (String sensor : currentState.keySet()) {
+             if (sensor.equals(SENSOR_NAME)) {
+                 assertEquals(currentState.get(sensor), Integer.valueOf(12345));
+             }
+         }
+     }
+ 
+     protected ClientResponse doSensorTest(Boolean raw, MediaType acceptsType, Object expectedValue) {
+         return doSensorTestUntyped(
+             raw==null ? null : (""+raw).toLowerCase(), 
+             acceptsType==null ? null : new String[] { acceptsType.getType() }, 
+             expectedValue);
+     }
+     protected ClientResponse doSensorTestUntyped(String raw, String[] acceptsTypes, Object expectedValue) {
+         WebResource req = client().resource(SENSORS_ENDPOINT + "/" + SENSOR_NAME);
+         if (raw!=null) req = req.queryParam("raw", raw);
+         ClientResponse response;
+         if (acceptsTypes!=null) {
+             Builder rb = req.accept(acceptsTypes);
+             response = rb.get(ClientResponse.class);
+         } else {
+             response = req.get(ClientResponse.class);
+         }
+         if (expectedValue!=null) {
+             HttpTestUtils.assertHealthyStatusCode(response.getStatus());
+             Object value = response.getEntity(expectedValue.getClass());
+             assertEquals(value, expectedValue);
+         }
+         return response;
+     }
+     
+     /**
+      * Check we can get a sensor, explicitly requesting json; gives a string picking up the rendering hint.
+      * 
+      * If no "Accepts" header is given, then we don't control whether json or plain text comes back.
+      * It is dependent on the method order, which is compiler-specific.
+      */
+     @Test
+     public void testGetJson() throws Exception {
+         doSensorTest(null, MediaType.APPLICATION_JSON_TYPE, "\"12345 frogs\"");
+     }
+     
+     @Test
+     public void testGetJsonBytes() throws Exception {
+         ClientResponse response = doSensorTest(null, MediaType.APPLICATION_JSON_TYPE, null);
+         byte[] bytes = Streams.readFully(response.getEntityInputStream());
+         // assert we have one set of surrounding quotes
+         assertEquals(bytes.length, 13);
+     }
+ 
+     /** Check that plain returns a string without quotes, with the rendering hint */
+     @Test
+     public void testGetPlain() throws Exception {
+         doSensorTest(null, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
+     }
+ 
+     /** 
+      * Check that when we set {@code raw = true}, the result ignores the display value hint.
+      *
+      * If no "Accepts" header is given, then we don't control whether json or plain text comes back.
+      * It is dependent on the method order, which is compiler-specific.
+      */
+     @Test
+     public void testGetRawJson() throws Exception {
+         doSensorTest(true, MediaType.APPLICATION_JSON_TYPE, 12345);
+     }
+     
+     /** As {@link #testGetRaw()} but with plain set, returns the number */
+     @Test
+     public void testGetPlainRaw() throws Exception {
+         // have to pass a string because that's how PLAIN is deserialized
+         doSensorTest(true, MediaType.TEXT_PLAIN_TYPE, "12345");
+     }
+ 
+     /** Check explicitly setting {@code raw} to {@code false} is as before */
+     @Test
+     public void testGetPlainRawFalse() throws Exception {
+         doSensorTest(false, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
+     }
+ 
+     /** Check empty vaue for {@code raw} will revert to using default. */
+     @Test
+     public void testGetPlainRawEmpty() throws Exception {
+         doSensorTestUntyped("", new String[] { MediaType.TEXT_PLAIN }, "12345 frogs");
+     }
+ 
+     /** Check unparseable vaue for {@code raw} will revert to using default. */
+     @Test
+     public void testGetPlainRawError() throws Exception {
+         doSensorTestUntyped("biscuits", new String[] { MediaType.TEXT_PLAIN }, "12345 frogs");
+     }
+     
+     /** Check we can set a value */
+     @Test
+     public void testSet() throws Exception {
+         try {
+             ClientResponse response = client().resource(SENSORS_ENDPOINT + "/" + SENSOR_NAME)
+                 .type(MediaType.APPLICATION_JSON_TYPE)
+                 .post(ClientResponse.class, 67890);
+             assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
+ 
+             assertEquals(entity.getAttribute(SENSOR), (Integer)67890);
+             
+             String value = client().resource(SENSORS_ENDPOINT + "/" + SENSOR_NAME).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
+             assertEquals(value, "67890 frogs");
+ 
+         } finally { addAmphibianSensor(entity); }
+     }
+ 
+     @Test
+     public void testSetFromMap() throws Exception {
+         try {
+             ClientResponse response = client().resource(SENSORS_ENDPOINT)
+                 .type(MediaType.APPLICATION_JSON_TYPE)
+                 .post(ClientResponse.class, MutableMap.of(SENSOR_NAME, 67890));
+             assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
+             
+             assertEquals(entity.getAttribute(SENSOR), (Integer)67890);
+ 
+         } finally { addAmphibianSensor(entity); }
+     }
+     
+     /** Check we can delete a value */
+     @Test
+     public void testDelete() throws Exception {
+         try {
+             ClientResponse response = client().resource(SENSORS_ENDPOINT + "/" + SENSOR_NAME)
+                 .delete(ClientResponse.class);
+             assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
+ 
+             String value = client().resource(SENSORS_ENDPOINT + "/" + SENSOR_NAME).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
+             assertEquals(value, "");
+ 
+         } finally { addAmphibianSensor(entity); }
+     }
+ 
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2aac052f/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerResourceIntegrationTest.java
----------------------------------------------------------------------
diff --cc usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerResourceIntegrationTest.java
index 0000000,1b29fad..7f70971
mode 000000,100644..100644
--- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerResourceIntegrationTest.java
+++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerResourceIntegrationTest.java
@@@ -1,0 -1,126 +1,127 @@@
+ /*
+  * 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.rest.resources;
+ 
+ import static brooklyn.util.http.HttpTool.httpClientBuilder;
+ import static org.testng.Assert.assertEquals;
+ 
+ import java.net.URI;
+ import java.util.Collections;
+ import java.util.Map;
+ 
++import org.apache.brooklyn.test.HttpTestUtils;
+ import org.apache.http.HttpStatus;
+ import org.apache.http.auth.UsernamePasswordCredentials;
+ import org.apache.http.client.HttpClient;
+ import org.eclipse.jetty.server.Server;
+ import org.testng.annotations.Test;
+ 
+ import brooklyn.config.BrooklynProperties;
+ import brooklyn.management.ManagementContext;
+ import brooklyn.management.internal.LocalManagementContext;
+ import brooklyn.management.internal.ManagementContextInternal;
+ import org.apache.brooklyn.rest.BrooklynRestApiLauncher;
+ import org.apache.brooklyn.rest.BrooklynRestApiLauncherTestFixture;
+ import org.apache.brooklyn.rest.security.provider.TestSecurityProvider;
 -import brooklyn.test.HttpTestUtils;
++import org.apache.brooklyn.test.HttpTestUtils;
+ import brooklyn.util.http.HttpTool;
+ import brooklyn.util.http.HttpToolResponse;
+ 
+ import com.google.common.collect.ImmutableMap;
+ 
+ public class ServerResourceIntegrationTest extends BrooklynRestApiLauncherTestFixture {
+ 
+     /**
+      * [sam] Other tests rely on brooklyn.properties not containing security properties so ..
+      * I think the best way to test this is to set a security provider, then reload properties
+      * and check no authentication is required.
+      * 
+      * [aled] Changing this test so doesn't rely on brooklyn.properties having no security
+      * provider (that can lead to failures locally when running just this test). Asserts 
+      */
+     @Test(groups = "Integration")
+     public void testSecurityProviderUpdatesWhenPropertiesReloaded() {
+         BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty();
+         brooklynProperties.put("brooklyn.webconsole.security.users", "admin");
+         brooklynProperties.put("brooklyn.webconsole.security.user.admin.password", "mypassword");
+         UsernamePasswordCredentials defaultCredential = new UsernamePasswordCredentials("admin", "mypassword");
+ 
+         ManagementContext mgmt = new LocalManagementContext(brooklynProperties);
+         
+         try {
+             Server server = useServerForTest(BrooklynRestApiLauncher.launcher()
+                     .managementContext(mgmt)
+                     .withoutJsgui()
+                     .securityProvider(TestSecurityProvider.class)
+                     .start());
+             String baseUri = getBaseUri(server);
+     
+             HttpToolResponse response;
+             final URI uri = URI.create(getBaseUri() + "/v1/server/properties/reload");
+             final Map<String, String> args = Collections.emptyMap();
+     
+             // Unauthorised when no credentials, and when default credentials.
+             response = HttpTool.httpPost(httpClientBuilder().uri(baseUri).build(), uri, args, args);
+             assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
+     
+             response = HttpTool.httpPost(httpClientBuilder().uri(baseUri).credentials(defaultCredential).build(), 
+                     uri, args, args);
+             assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
+ 
+             // Accepts TestSecurityProvider credentials, and we reload.
+             response = HttpTool.httpPost(httpClientBuilder().uri(baseUri).credentials(TestSecurityProvider.CREDENTIAL).build(),
+                     uri, args, args);
+             HttpTestUtils.assertHealthyStatusCode(response.getResponseCode());
+     
+             // Has no gone back to credentials from brooklynProperties; TestSecurityProvider credentials no longer work
+             response = HttpTool.httpPost(httpClientBuilder().uri(baseUri).credentials(defaultCredential).build(), 
+                     uri, args, args);
+             HttpTestUtils.assertHealthyStatusCode(response.getResponseCode());
+             
+             response = HttpTool.httpPost(httpClientBuilder().uri(baseUri).credentials(TestSecurityProvider.CREDENTIAL).build(), 
+                     uri, args, args);
+             assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
+     
+         } finally {
+             ((ManagementContextInternal)mgmt).terminate();
+         }
+     }
+ 
+     @Test(groups = "Integration")
+     public void testGetUser() throws Exception {
+         Server server = useServerForTest(BrooklynRestApiLauncher.launcher()
+                 .securityProvider(TestSecurityProvider.class)
+                 .withoutJsgui()
+                 .start());
+         assertEquals(getServerUser(server), TestSecurityProvider.USER);
+     }
+ 
+     private String getServerUser(Server server) throws Exception {
+         HttpClient client = httpClientBuilder()
+                 .uri(getBaseUri(server))
+                 .credentials(TestSecurityProvider.CREDENTIAL)
+                 .build();
+         
+         HttpToolResponse response = HttpTool.httpGet(client, URI.create(getBaseUri(server) + "/v1/server/user"),
+                 ImmutableMap.<String, String>of());
+         HttpTestUtils.assertHealthyStatusCode(response.getResponseCode());
+         return response.getContentAsString();
+     }
+ 
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2aac052f/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerShutdownTest.java
----------------------------------------------------------------------
diff --cc usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerShutdownTest.java
index 0000000,d38b380..a74f22a
mode 000000,100644..100644
--- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerShutdownTest.java
+++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ServerShutdownTest.java
@@@ -1,0 -1,187 +1,187 @@@
+ /*
+  * 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.rest.resources;
+ 
+ import static org.testng.Assert.assertEquals;
+ import static org.testng.Assert.assertFalse;
+ import static org.testng.Assert.assertNull;
+ import static org.testng.Assert.assertTrue;
+ 
+ import java.util.concurrent.atomic.AtomicReference;
+ 
+ import javax.ws.rs.core.MultivaluedMap;
+ 
++import org.apache.brooklyn.test.EntityTestUtils;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ 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 com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.ImmutableSet;
+ import com.sun.jersey.core.util.MultivaluedMapImpl;
+ 
+ import brooklyn.entity.basic.Attributes;
+ import brooklyn.entity.basic.Entities;
+ import brooklyn.entity.basic.Lifecycle;
+ import brooklyn.entity.drivers.BasicEntityDriverManager;
+ import brooklyn.entity.drivers.ReflectiveEntityDriverFactory;
+ import brooklyn.entity.proxying.EntitySpec;
+ import brooklyn.entity.trait.Startable;
+ import brooklyn.management.EntityManager;
+ import brooklyn.management.Task;
+ import org.apache.brooklyn.rest.resources.ServerResourceTest.StopLatchEntity;
+ import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
+ import brooklyn.test.Asserts;
 -import brooklyn.test.EntityTestUtils;
+ import brooklyn.test.entity.TestApplication;
+ import brooklyn.util.exceptions.Exceptions;
+ 
+ public class ServerShutdownTest extends BrooklynRestResourceTest {
+     private static final Logger log = LoggerFactory.getLogger(ServerResourceTest.class);
+ 
+     // Need to initialise the ManagementContext before each test as it is destroyed.
+     @Override
+     @BeforeClass(alwaysRun = true)
+     public void setUp() throws Exception {
+     }
+ 
+     @Override
+     @AfterClass(alwaysRun = true)
+     public void tearDown() throws Exception {
+     }
+ 
+     @Override
+     @BeforeMethod(alwaysRun = true)
+     public void setUpMethod() {
+         setUpJersey();
+         super.setUpMethod();
+     }
+ 
+     @AfterMethod(alwaysRun = true)
+     public void tearDownMethod() {
+         tearDownJersey();
+         destroyManagementContext();
+     }
+ 
+     @Test
+     public void testShutdown() throws Exception {
+         assertTrue(getManagementContext().isRunning());
+         assertFalse(shutdownListener.isRequested());
+ 
+         MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
+         formData.add("requestTimeout", "0");
+         formData.add("delayForHttpReturn", "0");
+         client().resource("/v1/server/shutdown").entity(formData).post();
+ 
+         Asserts.succeedsEventually(new Runnable() {
+             @Override
+             public void run() {
+                 assertTrue(shutdownListener.isRequested());
+             }
+         });
+         Asserts.succeedsEventually(new Runnable() {
+             @Override public void run() {
+                 assertFalse(getManagementContext().isRunning());
+             }});
+     }
+ 
+     @Test
+     public void testStopAppThenShutdownAndStopAppsWaitsForFirstStop() throws InterruptedException {
+         ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)getManagementContext().getEntityDriverManager()).getReflectiveDriverFactory();
+         f.addClassFullNameMapping("brooklyn.entity.basic.EmptySoftwareProcessDriver", "org.apache.brooklyn.rest.resources.ServerResourceTest$EmptySoftwareProcessTestDriver");
+ 
+         // Second stop on SoftwareProcess could return early, while the first stop is still in progress
+         // This causes the app to shutdown prematurely, leaking machines.
+         EntityManager emgr = getManagementContext().getEntityManager();
+         EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class);
+         TestApplication app = emgr.createEntity(appSpec);
+         emgr.manage(app);
+         EntitySpec<StopLatchEntity> latchEntitySpec = EntitySpec.create(StopLatchEntity.class);
+         final StopLatchEntity entity = app.createAndManageChild(latchEntitySpec);
+         app.start(ImmutableSet.of(app.newLocalhostProvisioningLocation()));
+         EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+ 
+         try {
+             final Task<Void> firstStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());
+             Asserts.succeedsEventually(new Runnable() {
+                 @Override
+                 public void run() {
+                     assertTrue(entity.isBlocked());
+                 }
+             });
+ 
+             final AtomicReference<Exception> shutdownError = new AtomicReference<>();
+             // Can't use ExecutionContext as it will be stopped on shutdown
+             Thread shutdownThread = new Thread() {
+                 @Override
+                 public void run() {
+                     try {
+                         MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
+                         formData.add("stopAppsFirst", "true");
+                         formData.add("shutdownTimeout", "0");
+                         formData.add("requestTimeout", "0");
+                         formData.add("delayForHttpReturn", "0");
+                         client().resource("/v1/server/shutdown").entity(formData).post();
+                     } catch (Exception e) {
+                         log.error("Shutdown request error", e);
+                         shutdownError.set(e);
+                         throw Exceptions.propagate(e);
+                     }
+                 }
+             };
+             shutdownThread.start();
+ 
+             //shutdown must wait until the first stop completes (or time out)
+             Asserts.succeedsContinually(new Runnable() {
+                 @Override
+                 public void run() {
+                     assertFalse(firstStop.isDone());
+                     assertEquals(getManagementContext().getApplications().size(), 1);
+                     assertFalse(shutdownListener.isRequested());
+                 }
+             });
+ 
+             // NOTE test is not fully deterministic. Depending on thread scheduling this will
+             // execute before or after ServerResource.shutdown does the app stop loop. This
+             // means that the shutdown code might not see the app at all. In any case though
+             // the test must succeed.
+             entity.unblock();
+ 
+             Asserts.succeedsEventually(new Runnable() {
+                 @Override
+                 public void run() {
+                     assertTrue(firstStop.isDone());
+                     assertTrue(shutdownListener.isRequested());
+                     assertFalse(getManagementContext().isRunning());
+                 }
+             });
+ 
+             shutdownThread.join();
+             assertNull(shutdownError.get(), "Shutdown request error, logged above");
+         } finally {
+             // Be sure we always unblock entity stop even in the case of an exception.
+             // In the success path the entity is already unblocked above.
+             entity.unblock();
+         }
+     }
+ 
+ }