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();
+         }
+     }
+ 
+ }

Reply via email to