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