http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java new file mode 100644 index 0000000..b341b6e --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareEffectorTest.java @@ -0,0 +1,141 @@ +/* + * 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.software.base; + +import java.util.Arrays; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.effector.core.Effectors; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.sensor.ssh.SshEffectorTasks; +import org.apache.brooklyn.sensor.ssh.SshEffectorTasks.SshEffectorBody; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; +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 org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation; +import org.apache.brooklyn.location.basic.SshMachineLocation; + +import com.google.common.base.Throwables; + +public class SoftwareEffectorTest { + + private static final Logger log = LoggerFactory.getLogger(SoftwareEffectorTest.class); + + TestApplication app; + ManagementContext mgmt; + + @BeforeMethod(alwaysRun=true) + public void setup() throws Exception { + app = TestApplication.Factory.newManagedInstanceForTests(); + mgmt = app.getManagementContext(); + + LocalhostMachineProvisioningLocation lhc = mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)); + SshMachineLocation lh = lhc.obtain(); + app.start(Arrays.asList(lh)); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (mgmt != null) Entities.destroyAll(mgmt); + mgmt = null; + } + + public static final Effector<String> GET_REMOTE_DATE_1 = Effectors.effector(String.class, "getRemoteDate") + .description("retrieves the date from the remote machine") + .impl(new SshEffectorBody<String>() { + public String call(ConfigBag parameters) { + queue( ssh("date").requiringZeroAndReturningStdout() ); + return last(String.class); + } + }) + .build(); + + public static final Effector<String> GET_REMOTE_DATE_2 = Effectors.effector(GET_REMOTE_DATE_1) + // Just get year to confirm implementation is different + .description("retrieves the year from the remote machine") + .impl(SshEffectorTasks.ssh("date +%Y").requiringZeroAndReturningStdout()) + .build(); + + // TODO revisit next two tests before end 2019 ;) + + @Test(groups="Integration") + public void testSshDateEffector1() { + Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_1); + log.info("ssh date 1 gives: "+call.getUnchecked()); + Assert.assertTrue(call.getUnchecked().indexOf("201") > 0); + } + + @Test(groups="Integration") + public void testSshDateEffector2() { + Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_2); + log.info("ssh date 2 gives: "+call.getUnchecked()); + Assert.assertTrue(call.getUnchecked().indexOf("201") == 0); + } + + public static final String COMMAND_THAT_DOES_NOT_EXIST = "blah_blah_blah_command_DOES_NOT_EXIST"; + + @Test(groups="Integration") + public void testBadExitCodeCaught() { + Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode") + .impl(new SshEffectorBody<Void>() { + public Void call(ConfigBag parameters) { + queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringZeroAndReturningStdout() ); + return null; + } + }).build() ); + try { + Object result = call.getUnchecked(); + Assert.fail("ERROR: should have failed earlier in this test, instead got successful task result "+result+" from "+call); + } catch (Exception e) { + Throwable root = Throwables.getRootCause(e); + if (!(root instanceof IllegalStateException)) Assert.fail("Should have failed with IAE, but got: "+root); + if (root.getMessage()==null || root.getMessage().indexOf("exit code")<=0) + Assert.fail("Should have failed with 'exit code' message, but got: "+root); + // test passed + return; + } + } + + @Test(groups="Integration") + public void testBadExitCodeCaughtAndStdErrAvailable() { + final ProcessTaskWrapper<?>[] sshTasks = new ProcessTaskWrapper[1]; + + Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode") + .impl(new SshEffectorBody<Void>() { + public Void call(ConfigBag parameters) { + sshTasks[0] = queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringExitCodeZero() ); + return null; + } + }).build() ); + call.blockUntilEnded(); + Assert.assertTrue(call.isError()); + log.info("stderr gives: "+new String(sshTasks[0].getStderr())); + Assert.assertTrue(new String(sshTasks[0].getStderr()).indexOf(COMMAND_THAT_DOES_NOT_EXIST) >= 0); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java new file mode 100644 index 0000000..e743b24 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityLatchTest.java @@ -0,0 +1,161 @@ +/* + * 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.software.base; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.List; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.entity.core.Attributes; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.MyService; +import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.SimulatedDriver; +import org.apache.brooklyn.entity.stock.BasicEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.location.basic.FixedListMachineProvisioningLocation; +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.apache.brooklyn.sensor.core.DependentConfiguration; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.core.task.TaskInternal; +import org.apache.brooklyn.util.time.Duration; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + + +public class SoftwareProcessEntityLatchTest extends BrooklynAppUnitTestSupport { + + // NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves. + + @SuppressWarnings("unused") + private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessEntityLatchTest.class); + + private SshMachineLocation machine; + private FixedListMachineProvisioningLocation<SshMachineLocation> loc; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + loc = getLocation(); + } + + @SuppressWarnings("unchecked") + private FixedListMachineProvisioningLocation<SshMachineLocation> getLocation() { + FixedListMachineProvisioningLocation<SshMachineLocation> loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)); + machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("address", "localhost")); + loc.addMachine(machine); + return loc; + } + + @Test + public void testStartLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.START_LATCH, ImmutableList.<String>of()); + } + + @Test + public void testSetupLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.SETUP_LATCH, ImmutableList.<String>of()); + } + + @Test + public void testIntallResourcesLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.INSTALL_RESOURCES_LATCH, ImmutableList.of("setup")); + } + + @Test + public void testInstallLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.INSTALL_LATCH, ImmutableList.of("setup", "copyInstallResources")); + } + + @Test + public void testCustomizeLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.CUSTOMIZE_LATCH, ImmutableList.of("setup", "copyInstallResources", "install")); + } + + @Test + public void testRuntimeResourcesLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.RUNTIME_RESOURCES_LATCH, ImmutableList.of("setup", "copyInstallResources", "install", "customize")); + } + + @Test + public void testLaunchLatchBlocks() throws Exception { + runTestLatchBlocks(SoftwareProcess.LAUNCH_LATCH, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources")); + } + + protected void runTestLatchBlocks(final ConfigKey<Boolean> latch, List<String> preLatchEvents) throws Exception { + final BasicEntity triggerEntity = app.createAndManageChild(EntitySpec.create(BasicEntity.class)); + final MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure(latch, DependentConfiguration.attributeWhenReady(triggerEntity, Attributes.SERVICE_UP))); + + final Task<Void> task = Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc))); + + assertEffectorBlockingDetailsEventually(entity, "Waiting for config "+latch.getName()); + assertDriverEventsEquals(entity, preLatchEvents); + + assertFalse(task.isDone()); + ((EntityLocal)triggerEntity).setAttribute(Attributes.SERVICE_UP, true); + task.get(Duration.THIRTY_SECONDS); + assertDriverEventsEquals(entity, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources", "launch")); + } + + private void assertDriverEventsEquals(MyService entity, List<String> expectedEvents) { + List<String> events = ((SimulatedDriver)entity.getDriver()).events; + assertEquals(events, expectedEvents, "events="+events); + } + + private void assertEffectorBlockingDetailsEventually(final Entity entity, final String blockingDetailsSnippet) { + Asserts.succeedsEventually(new Runnable() { + @Override public void run() { + Task<?> entityTask = Iterables.getOnlyElement(mgmt.getExecutionManager().getTasksWithAllTags(ImmutableList.of(BrooklynTaskTags.EFFECTOR_TAG, BrooklynTaskTags.tagForContextEntity(entity)))); + String blockingDetails = getBlockingDetails(entityTask); + assertTrue(blockingDetails.contains(blockingDetailsSnippet)); + }}); + } + + private String getBlockingDetails(Task<?> task) { + List<TaskInternal<?>> taskChain = Lists.newArrayList(); + TaskInternal<?> taskI = (TaskInternal<?>) task; + while (taskI != null) { + taskChain.add(taskI); + if (taskI.getBlockingDetails() != null) { + return taskI.getBlockingDetails(); + } + taskI = (TaskInternal<?>) taskI.getBlockingTask(); + } + throw new IllegalStateException("No blocking details for "+task+" (walked task chain "+taskChain+")"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java new file mode 100644 index 0000000..475cb09 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityRebindTest.java @@ -0,0 +1,179 @@ +/* + * 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.software.base; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.location.MachineProvisioningLocation; +import org.apache.brooklyn.api.location.NoMachinesAvailableException; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.entity.core.Attributes; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.entity.lifecycle.Lifecycle; +import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic; +import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic.ServiceProblemsLogic; +import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest.MyService; +import org.apache.brooklyn.test.EntityTestUtils; +import org.apache.brooklyn.util.core.flags.SetFromFlag; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.location.basic.AbstractLocation; +import org.apache.brooklyn.location.basic.SshMachineLocation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.io.Files; + +public class SoftwareProcessEntityRebindTest extends BrooklynAppUnitTestSupport { + + private ClassLoader classLoader = getClass().getClassLoader(); + private TestApplication newApp; + private ManagementContext newManagementContext; + private MyService origE; + private File mementoDir; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + mementoDir = Files.createTempDir(); + mgmt = RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader); + super.setUp(); + } + + @AfterMethod(alwaysRun=true) + @Override + public void tearDown() throws Exception { + super.tearDown(); + if (newApp != null) Entities.destroyAll(newApp.getManagementContext()); + if (newManagementContext != null) Entities.destroyAll(newManagementContext); + if (mementoDir != null) RebindTestUtils.deleteMementoDir(mementoDir); + } + + @Test + public void testReleasesLocationOnStopAfterRebinding() throws Exception { + origE = app.createAndManageChild(EntitySpec.create(MyService.class)); + + MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class) + .displayName("mylocname")); + app.start(ImmutableList.of(origLoc)); + assertEquals(origLoc.inUseCount.get(), 1); + + newApp = (TestApplication) rebind(); + MyProvisioningLocation newLoc = (MyProvisioningLocation) Iterables.getOnlyElement(newApp.getLocations()); + assertEquals(newLoc.inUseCount.get(), 1); + + newApp.stop(); + assertEquals(newLoc.inUseCount.get(), 0); + } + + @Test + public void testCreatesDriverAfterRebind() throws Exception { + origE = app.createAndManageChild(EntitySpec.create(MyService.class)); + //the entity skips enricher initialization, do it explicitly + origE.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp()); + + MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class) + .displayName("mylocname")); + app.start(ImmutableList.of(origLoc)); + assertEquals(origE.getAttribute(Attributes.SERVICE_STATE_EXPECTED).getState(), Lifecycle.RUNNING); + EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING); + + ServiceProblemsLogic.updateProblemsIndicator((EntityLocal)origE, "test", "fire"); + EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE); + + newApp = (TestApplication) rebind(); + MyService newE = (MyService) Iterables.getOnlyElement(newApp.getChildren()); + assertTrue(newE.getDriver() != null, "driver should be initialized"); + } + + @Test + public void testDoesNotCreateDriverAfterRebind() throws Exception { + origE = app.createAndManageChild(EntitySpec.create(MyService.class)); + //the entity skips enricher initialization, do it explicitly + origE.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp()); + + MyProvisioningLocation origLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class) + .displayName("mylocname")); + app.start(ImmutableList.of(origLoc)); + assertEquals(origE.getAttribute(Attributes.SERVICE_STATE_EXPECTED).getState(), Lifecycle.RUNNING); + EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING); + + ServiceStateLogic.setExpectedState(origE, Lifecycle.ON_FIRE); + EntityTestUtils.assertAttributeEqualsEventually(origE, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE); + + newApp = (TestApplication) rebind(); + MyService newE = (MyService) Iterables.getOnlyElement(newApp.getChildren()); + assertNull(newE.getDriver(), "driver should not be initialized because entity is in a permanent failure"); + } + + private TestApplication rebind() throws Exception { + RebindTestUtils.waitForPersisted(app); + TestApplication result = (TestApplication) RebindTestUtils.rebind(mementoDir, getClass().getClassLoader()); + newManagementContext = result.getManagementContext(); + return result; + } + + public static class MyProvisioningLocation extends AbstractLocation implements MachineProvisioningLocation<SshMachineLocation> { + private static final long serialVersionUID = 1L; + + @SetFromFlag(defaultVal="0") + AtomicInteger inUseCount; + + public MyProvisioningLocation() { + } + + @Override + public MachineProvisioningLocation<SshMachineLocation> newSubLocation(Map<?, ?> newFlags) { + throw new UnsupportedOperationException(); + } + + @Override + public SshMachineLocation obtain(Map flags) throws NoMachinesAvailableException { + inUseCount.incrementAndGet(); + return getManagementContext().getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .parent(this) + .configure("address","localhost")); + } + + @Override + public void release(SshMachineLocation machine) { + inUseCount.decrementAndGet(); + } + + @Override + public Map getProvisioningFlags(Collection tags) { + return Collections.emptyMap(); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java new file mode 100644 index 0000000..0a6bd26 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessEntityTest.java @@ -0,0 +1,798 @@ +/* + * 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.software.base; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.location.MachineLocation; +import org.apache.brooklyn.api.mgmt.EntityManager; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.api.mgmt.TaskAdaptable; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.effector.core.Effectors; +import org.apache.brooklyn.entity.core.Attributes; +import org.apache.brooklyn.entity.core.BrooklynConfigKeys; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.entity.core.EntityInternal; +import org.apache.brooklyn.entity.drivers.BasicEntityDriverManager; +import org.apache.brooklyn.entity.drivers.ReflectiveEntityDriverFactory; +import org.apache.brooklyn.entity.lifecycle.Lifecycle; +import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic; +import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessDriver; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver; +import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl; +import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters; +import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters; +import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters.RestartMachineMode; +import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode; +import org.apache.brooklyn.entity.trait.Startable; +import org.apache.brooklyn.sensor.core.PortAttributeSensorAndConfigKey; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.EntityTestUtils; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException; +import org.apache.brooklyn.util.net.UserAndHostAndPort; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.time.Duration; +import org.jclouds.util.Throwables2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import org.apache.brooklyn.location.basic.FixedListMachineProvisioningLocation; +import org.apache.brooklyn.location.basic.Locations; +import org.apache.brooklyn.location.basic.SimulatedLocation; +import org.apache.brooklyn.location.basic.SshMachineLocation; + + +public class SoftwareProcessEntityTest extends BrooklynAppUnitTestSupport { + + // NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves. + + private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessEntityTest.class); + + private SshMachineLocation machine; + private FixedListMachineProvisioningLocation<SshMachineLocation> loc; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + loc = getLocation(); + } + + @SuppressWarnings("unchecked") + private FixedListMachineProvisioningLocation<SshMachineLocation> getLocation() { + FixedListMachineProvisioningLocation<SshMachineLocation> loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)); + machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("address", "localhost")); + loc.addMachine(machine); + return loc; + } + + @Test + public void testSetsMachineAttributes() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + + assertEquals(entity.getAttribute(SoftwareProcess.HOSTNAME), machine.getAddress().getHostName()); + assertEquals(entity.getAttribute(SoftwareProcess.ADDRESS), machine.getAddress().getHostAddress()); + assertEquals(entity.getAttribute(Attributes.SSH_ADDRESS), UserAndHostAndPort.fromParts(machine.getUser(), machine.getAddress().getHostName(), machine.getPort())); + assertEquals(entity.getAttribute(SoftwareProcess.PROVISIONING_LOCATION), loc); + } + + @Test + public void testProcessTemplateWithExtraSubstitutions() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver driver = (SimulatedDriver) entity.getDriver(); + Map<String,String> substitutions = MutableMap.of("myname","peter"); + String result = driver.processTemplate("/org/apache/brooklyn/entity/software/base/template_with_extra_substitutions.txt",substitutions); + Assert.assertTrue(result.contains("peter")); + } + + @Test + public void testInstallDirAndRunDir() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo")); + + entity.start(ImmutableList.of(loc)); + + Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/MyService"); + Assert.assertEquals(entity.getAttribute(SoftwareProcess.RUN_DIR), "/tmp/brooklyn-foo/apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId()); + } + + @Test + public void testInstallDirAndRunDirUsingTilde() throws Exception { + String dataDirName = ".brooklyn-foo"+Strings.makeRandomId(4); + String dataDir = "~/"+dataDirName; + String resolvedDataDir = Os.mergePaths(Os.home(), dataDirName); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, dataDir)); + + entity.start(ImmutableList.of(loc)); + + Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.INSTALL_DIR)), + Os.nativePath(Os.mergePaths(resolvedDataDir, "installs/MyService"))); + Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.RUN_DIR)), + Os.nativePath(Os.mergePaths(resolvedDataDir, "apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId()))); + } + + protected <T extends MyService> void doStartAndCheckVersion(Class<T> type, String expectedLabel, ConfigBag config) { + MyService entity = app.createAndManageChild(EntitySpec.create(type) + .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo") + .configure(config.getAllConfigAsConfigKeyMap())); + entity.start(ImmutableList.of(loc)); + Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/" + + expectedLabel); + } + + @Test + public void testCustomInstallDir0() throws Exception { + doStartAndCheckVersion(MyService.class, "MyService", ConfigBag.newInstance()); + } + @Test + public void testCustomInstallDir1() throws Exception { + doStartAndCheckVersion(MyService.class, "MyService_9.9.8", ConfigBag.newInstance() + .configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.8")); + } + @Test + public void testCustomInstallDir2() throws Exception { + doStartAndCheckVersion(MyService.class, "MySvc_998", ConfigBag.newInstance() + .configure(SoftwareProcess.INSTALL_UNIQUE_LABEL, "MySvc_998")); + } + @Test + public void testCustomInstallDir3() throws Exception { + doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9", ConfigBag.newInstance()); + } + @Test + public void testCustomInstallDir4() throws Exception { + doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.7", ConfigBag.newInstance() + .configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.7")); + } + @Test + public void testCustomInstallDir5() throws Exception { + doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9_NaCl", ConfigBag.newInstance() + .configure(ConfigKeys.newStringConfigKey("salt"), "NaCl")); + } + + @Test + public void testBasicSoftwareProcessEntityLifecycle() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Assert.assertTrue(d.isRunning()); + entity.stop(); + Assert.assertEquals(d.events, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources", "launch", "stop")); + assertFalse(d.isRunning()); + } + + @Test + public void testBasicSoftwareProcessRestarts() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Assert.assertTrue(d.isRunning()); + + // this will cause restart to fail if it attempts to replace the machine + loc.removeMachine(Locations.findUniqueSshMachineLocation(entity.getLocations()).get()); + + // with defaults, it won't reboot machine + d.events.clear(); + entity.restart(); + assertEquals(d.events, ImmutableList.of("stop", "launch")); + + // but here, it will try to reboot, and fail because there is no machine available + TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART, + ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE))); + t1.asTask().blockUntilEnded(Duration.TEN_SECONDS); + if (!t1.asTask().isError()) { + Assert.fail("Should have thrown error during "+t1+" because no more machines available at "+loc); + } + + // now it has a machine, so reboot should succeed + SshMachineLocation machine2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("address", "localhost")); + loc.addMachine(machine2); + TaskAdaptable<Void> t2 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART, + ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE))); + t2.asTask().get(); + + assertFalse(d.isRunning()); + } + + @Test + public void testBasicSoftwareProcessStopsEverything() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Location machine = Iterables.getOnlyElement(entity.getLocations()); + + d.events.clear(); + entity.stop(); + assertEquals(d.events, ImmutableList.of("stop")); + assertEquals(entity.getLocations().size(), 0); + assertTrue(loc.getAvailable().contains(machine)); + } + + @Test + public void testBasicSoftwareProcessStopEverythingExplicitly() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Location machine = Iterables.getOnlyElement(entity.getLocations()); + d.events.clear(); + + TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP, + ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.IF_NOT_STOPPED))); + t1.asTask().get(); + + assertEquals(d.events, ImmutableList.of("stop")); + assertEquals(entity.getLocations().size(), 0); + assertTrue(loc.getAvailable().contains(machine)); + } + + @Test + public void testBasicSoftwareProcessStopsProcess() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Location machine = Iterables.getOnlyElement(entity.getLocations()); + d.events.clear(); + + TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP, + ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.NEVER))); + t1.asTask().get(10, TimeUnit.SECONDS); + + assertEquals(d.events, ImmutableList.of("stop")); + assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine)); + assertFalse(loc.getAvailable().contains(machine)); + } + + @Test(groups = "Integration") + public void testBasicSoftwareProcessStopAllModes() throws Exception { + for (boolean isEntityStopped : new boolean[] {true, false}) { + for (StopMode stopProcessMode : StopMode.values()) { + for (StopMode stopMachineMode : StopMode.values()) { + try { + testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped); + } catch (Exception e) { + String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped; + throw new PropagatedRuntimeException(msg, e); + } + } + } + } + } + + @Test + public void testBasicSoftwareProcessStopSomeModes() throws Exception { + for (boolean isEntityStopped : new boolean[] {true, false}) { + StopMode stopProcessMode = StopMode.IF_NOT_STOPPED; + StopMode stopMachineMode = StopMode.IF_NOT_STOPPED; + try { + testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped); + } catch (Exception e) { + String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped; + throw new PropagatedRuntimeException(msg, e); + } + } + } + + private void testBasicSoftwareProcessStopModes(StopMode stopProcessMode, StopMode stopMachineMode, boolean isEntityStopped) throws Exception { + FixedListMachineProvisioningLocation<SshMachineLocation> l = getLocation(); + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(l)); + SimulatedDriver d = (SimulatedDriver) entity.getDriver(); + Location machine = Iterables.getOnlyElement(entity.getLocations()); + d.events.clear(); + + if (isEntityStopped) { + ((EntityInternal)entity).setAttribute(ServiceStateLogic.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED); + } + + TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP, + ConfigBag.newInstance() + .configure(StopSoftwareParameters.STOP_PROCESS_MODE, stopProcessMode) + .configure(StopSoftwareParameters.STOP_MACHINE_MODE, stopMachineMode))); + t1.asTask().get(10, TimeUnit.SECONDS); + + if (MachineLifecycleEffectorTasksTest.canStop(stopProcessMode, isEntityStopped)) { + assertEquals(d.events, ImmutableList.of("stop")); + } else { + assertTrue(d.events.isEmpty()); + } + if (MachineLifecycleEffectorTasksTest.canStop(stopMachineMode, machine == null)) { + assertTrue(entity.getLocations().isEmpty()); + assertTrue(l.getAvailable().contains(machine)); + } else { + assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine)); + assertFalse(l.getAvailable().contains(machine)); + } + } + + @Test + public void testShutdownIsIdempotent() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + entity.start(ImmutableList.of(loc)); + entity.stop(); + + entity.stop(); + } + + @Test + public void testReleaseEvenIfErrorDuringStart() throws Exception { + MyServiceImpl entity = new MyServiceImpl(app) { + @Override public Class<?> getDriverInterface() { + return SimulatedFailOnStartDriver.class; + } + }; + Entities.manage(entity); + + try { + entity.start(ImmutableList.of(loc)); + Assert.fail(); + } catch (Exception e) { + IllegalStateException cause = Throwables2.getFirstThrowableOfType(e, IllegalStateException.class); + if (cause == null || !cause.toString().contains("Simulating start error")) throw e; + } + + try { + entity.stop(); + } catch (Exception e) { + // Keep going + LOG.info("Error during stop, after simulating error during start", e); + } + Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine)); + Entities.unmanage(entity); + } + + @SuppressWarnings("rawtypes") + public void doTestReleaseEvenIfErrorDuringStop(final Class driver) throws Exception { + MyServiceImpl entity = new MyServiceImpl(app) { + @Override public Class<?> getDriverInterface() { + return driver; + } + }; + Entities.manage(entity); + + entity.start(ImmutableList.of(loc)); + Task<Void> t = entity.invoke(Startable.STOP); + t.blockUntilEnded(); + + assertFalse(t.isError(), "Expected parent to succeed, not fail with " + Tasks.getError(t)); + Iterator<Task<?>> failures; + failures = Tasks.failed(Tasks.descendants(t, true)).iterator(); + Assert.assertTrue(failures.hasNext(), "Expected error in descendants"); + failures = Tasks.failed(Tasks.children(t)).iterator(); + Assert.assertTrue(failures.hasNext(), "Expected error in child"); + Throwable e = Tasks.getError(failures.next()); + if (e == null || !e.toString().contains("Simulating stop error")) + Assert.fail("Wrong error", e); + + Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine), "Expected location to be available again"); + + Entities.unmanage(entity); + } + + @Test + public void testReleaseEvenIfErrorDuringStop() throws Exception { + doTestReleaseEvenIfErrorDuringStop(SimulatedFailOnStopDriver.class); + } + + @Test + public void testReleaseEvenIfChildErrorDuringStop() throws Exception { + doTestReleaseEvenIfErrorDuringStop(SimulatedFailInChildOnStopDriver.class); + } + + @Test + public void testDoubleStopEntity() { + ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory(); + f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName()); + + // Second stop on SoftwareProcess will return early, while the first stop is still in progress + // This causes the app to shutdown prematurely, leaking machines. + EntityManager emgr = mgmt.getEntityManager(); + EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class); + TestApplication app = emgr.createEntity(appSpec); + emgr.manage(app); + EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class); + Entity entity = app.createAndManageChild(latchEntitySpec); + + final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class)); + try { + app.start(ImmutableSet.of(loc)); + EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING); + + final Task<Void> firstStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of()); + // Wait until first task tries to release the location, at this point the entity's reference + // to the location is already cleared. + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(loc.isBlocked()); + } + }); + + // Subsequent stops will end quickly - no location to release, + // while the first one is still releasing the machine. + final Task<Void> secondStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of());; + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(secondStop.isDone()); + } + }); + + // Entity state is STOPPED even though first location is still releasing + EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED); + Asserts.succeedsContinually(new Runnable() { + @Override + public void run() { + assertFalse(firstStop.isDone()); + } + }); + + loc.unblock(); + + // After the location is released, first task ends as well. + EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED); + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(firstStop.isDone()); + } + }); + + } finally { + loc.unblock(); + } + + } + + @Test + public void testDoubleStopApp() { + ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory(); + f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName()); + + // Second stop on SoftwareProcess will return early, while the first stop is still in progress + // This causes the app to shutdown prematurely, leaking machines. + EntityManager emgr = mgmt.getEntityManager(); + EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class); + final TestApplication app = emgr.createEntity(appSpec); + emgr.manage(app); + EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class); + final Entity entity = app.createAndManageChild(latchEntitySpec); + + final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class)); + try { + app.start(ImmutableSet.of(loc)); + EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING); + + final Task<Void> firstStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of()); + // Wait until first task tries to release the location, at this point the entity's reference + // to the location is already cleared. + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(loc.isBlocked()); + } + }); + + // Subsequent stops will end quickly - no location to release, + // while the first one is still releasing the machine. + final Task<Void> secondStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());; + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(secondStop.isDone()); + } + }); + + // Since second stop succeeded the app will get unmanaged. + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(!Entities.isManaged(entity)); + assertTrue(!Entities.isManaged(app)); + } + }); + + // Unmanage will cancel the first task + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertTrue(firstStop.isDone()); + } + }); + } finally { + // We still haven't unblocked the location release, but entity is already unmanaged. + // Double STOP on an application could leak locations!!! + loc.unblock(); + } + } + + @Test + public void testOpenPortsWithPortRangeConfig() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure("http.port", "9999+")); + Assert.assertTrue(entity.getRequiredOpenPorts().contains(9999)); + } + + @ImplementedBy(MyServiceImpl.class) + public interface MyService extends SoftwareProcess { + PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT; + public SoftwareProcessDriver getDriver(); + public Collection<Integer> getRequiredOpenPorts(); + } + + public static class MyServiceImpl extends SoftwareProcessImpl implements MyService { + public MyServiceImpl() {} + public MyServiceImpl(Entity parent) { super(parent); } + + @Override + protected void initEnrichers() { + // Don't add enrichers messing with the SERVICE_UP state - we are setting it manually + } + + @Override + public Class<?> getDriverInterface() { + return SimulatedDriver.class; + } + + @Override + public Collection<Integer> getRequiredOpenPorts() { + return super.getRequiredOpenPorts(); + } + } + + @ImplementedBy(MyServiceWithVersionImpl.class) + public interface MyServiceWithVersion extends MyService { + public static ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "9.9.9"); + } + + public static class MyServiceWithVersionImpl extends MyServiceImpl implements MyServiceWithVersion { + public MyServiceWithVersionImpl() {} + public MyServiceWithVersionImpl(Entity parent) { super(parent); } + } + + public static class SimulatedFailOnStartDriver extends SimulatedDriver { + public SimulatedFailOnStartDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public void install() { + throw new IllegalStateException("Simulating start error"); + } + } + + public static class SimulatedFailOnStopDriver extends SimulatedDriver { + public SimulatedFailOnStopDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public void stop() { + throw new IllegalStateException("Simulating stop error"); + } + } + + public static class SimulatedFailInChildOnStopDriver extends SimulatedDriver { + public SimulatedFailInChildOnStopDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public void stop() { + DynamicTasks.queue(Tasks.fail("Simulating stop error in child", null)); + } + } + + public static class SimulatedDriver extends AbstractSoftwareProcessSshDriver { + public List<String> events = new ArrayList<String>(); + private volatile boolean launched = false; + + public SimulatedDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public boolean isRunning() { + return launched; + } + + @Override + public void stop() { + events.add("stop"); + launched = false; + entity.setAttribute(Startable.SERVICE_UP, false); + entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED); + } + + @Override + public void kill() { + events.add("kill"); + launched = false; + entity.setAttribute(Startable.SERVICE_UP, false); + } + + @Override + public void install() { + events.add("install"); + entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STARTING); + } + + @Override + public void customize() { + events.add("customize"); + } + + @Override + public void launch() { + events.add("launch"); + launched = true; + entity.setAttribute(Startable.SERVICE_UP, true); + entity.setAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING); + } + + @Override + public void setup() { + events.add("setup"); + } + + @Override + public void copyInstallResources() { + events.add("copyInstallResources"); + } + + @Override + public void copyRuntimeResources() { + events.add("copyRuntimeResources"); + } + + @Override + public void runPreInstallCommand(String command) { } + + @Override + public void runPostInstallCommand(String command) { } + + @Override + public void runPreLaunchCommand(String command) { } + + @Override + public void runPostLaunchCommand(String command) { } + + @Override + protected String getInstallLabelExtraSalt() { + return (String)getEntity().getConfigRaw(ConfigKeys.newStringConfigKey("salt"), true).or((String)null); + } + } + + public static class ReleaseLatchLocation extends SimulatedLocation { + private static final long serialVersionUID = 1L; + + private CountDownLatch lock = new CountDownLatch(1); + private volatile boolean isBlocked; + + public void unblock() { + lock.countDown(); + } + @Override + public void release(MachineLocation machine) { + super.release(machine); + try { + isBlocked = true; + lock.await(); + isBlocked = false; + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } + } + + public boolean isBlocked() { + return isBlocked; + } + + } + + public static class MinimalEmptySoftwareProcessTestDriver implements EmptySoftwareProcessDriver { + + private EmptySoftwareProcessImpl entity; + private Location location; + + public MinimalEmptySoftwareProcessTestDriver(EmptySoftwareProcessImpl entity, Location location) { + this.entity = entity; + this.location = location; + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public EntityLocal getEntity() { + return entity; + } + + @Override + public boolean isRunning() { + return true; + } + + @Override + public void rebind() { + } + + @Override + public void start() { + } + + @Override + public void restart() { + } + + @Override + public void stop() { + } + + @Override + public void kill() { + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java new file mode 100644 index 0000000..1b270b9 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSshDriverIntegrationTest.java @@ -0,0 +1,389 @@ +/* + * 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.software.base; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.entity.core.BrooklynConfigKeys; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.entity.factory.ApplicationBuilder; +import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl; +import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess; +import org.apache.brooklyn.entity.trait.Startable; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.BrooklynNetworkUtils; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.stream.KnownSizeInputStream; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.brooklyn.util.yaml.Yamls; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation; +import org.apache.brooklyn.location.basic.SshMachineLocation; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.io.ByteSource; +import com.google.common.io.Files; + + +public class SoftwareProcessSshDriverIntegrationTest { + + private LocalManagementContext managementContext; + private LocalhostMachineProvisioningLocation localhost; + private SshMachineLocation machine127; + private TestApplication app; + private File tempDataDir; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + tempDataDir = Files.createTempDir(); + managementContext = new LocalManagementContext(); + + localhost = managementContext.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)); + machine127 = managementContext.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure("address", "localhost")); + app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (app != null) Entities.destroyAll(app.getManagementContext()); + if (tempDataDir != null) Os.deleteRecursively(tempDataDir); + } + + // Integration test because requires ssh'ing (and takes about 5 seconds) + // See also SoftwareProcessEntityTest.testCustomInstallDirX for a lot more mocked variants + @Test(groups="Integration") + public void testCanInstallMultipleVersionsOnSameMachine() throws Exception { + managementContext.getBrooklynProperties().put(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure(SoftwareProcess.SUGGESTED_VERSION, "0.1.0")); + MyService entity2 = app.createAndManageChild(EntitySpec.create(MyService.class) + .configure(SoftwareProcess.SUGGESTED_VERSION, "0.2.0")); + app.start(ImmutableList.of(machine127)); + + String installDir1 = entity.getAttribute(SoftwareProcess.INSTALL_DIR); + String installDir2 = entity2.getAttribute(SoftwareProcess.INSTALL_DIR); + + assertNotEquals(installDir1, installDir2); + assertTrue(installDir1.contains("0.1.0"), "installDir1="+installDir1); + assertTrue(installDir2.contains("0.2.0"), "installDir2="+installDir2); + assertTrue(new File(new File(installDir1), "myfile").isFile()); + assertTrue(new File(new File(installDir2), "myfile").isFile()); + } + + @Test(groups="Integration") + public void testLocalhostInTmp() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(localhost)); + + String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR); + assertTrue(installDir.startsWith("/tmp/brooklyn-"+Os.user()+"/installs/"), "installed in "+installDir); + } + + @Test(groups="Integration") + public void testMachine127InHome() throws Exception { + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(machine127)); + + String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR); + assertTrue(installDir.startsWith(Os.home()+"/brooklyn-managed-processes/installs/"), "installed in "+installDir); + } + + @Test(groups="Integration") + public void testLocalhostInCustom() throws Exception { + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(localhost)); + + String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR); + assertTrue(installDir.startsWith(tempDataDir.getAbsolutePath()+"/installs/"), "installed in "+installDir); + } + + @Test(groups="Integration") + @Deprecated + public void testMachineInCustomFromDataDir() throws Exception { + managementContext.getBrooklynProperties().put(BrooklynConfigKeys.BROOKLYN_DATA_DIR, tempDataDir.getAbsolutePath()); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(machine127)); + + String installDir = entity.getAttribute(SoftwareProcess.INSTALL_DIR); + assertTrue(installDir.startsWith(tempDataDir.getAbsolutePath()+"/installs/"), "installed in "+installDir); + } + + @Test(groups="Integration") + public void testCopyResource() throws Exception { + File tempDest = new File(tempDataDir, "tempDest.txt"); + String tempLocalContent = "abc"; + File tempLocal = new File(tempDataDir, "tempLocal.txt"); + Files.write(tempLocalContent, tempLocal, Charsets.UTF_8); + + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(localhost)); + + // Copy local file + entity.getDriver().copyResource(tempLocal, tempDest.getAbsolutePath()); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + tempDest.delete(); + + // Copy local file using url + entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath()); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + tempDest.delete(); + + // Copy reader + entity.getDriver().copyResource(new StringReader(tempLocalContent), tempDest.getAbsolutePath()); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + tempDest.delete(); + + // Copy stream + entity.getDriver().copyResource(ByteSource.wrap(tempLocalContent.getBytes()).openStream(), tempDest.getAbsolutePath()); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + tempDest.delete(); + + // Copy known-size stream + entity.getDriver().copyResource(new KnownSizeInputStream(Streams.newInputStreamWithContents(tempLocalContent), tempLocalContent.length()), tempDest.getAbsolutePath()); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + tempDest.delete(); + } + + @Test(groups="Integration") + public void testCopyResourceCreatingParentDir() throws Exception { + /* + * TODO copyResource will now always create the parent dir, irrespective of the createParentDir value! + * In SshMachineLocation on 2014-05-29, Alex added: mkdir -p `dirname '$DEST'` + * + * Changing this test to assert that parent dir always created; should we delete boolean createParentDir + * from the copyResource method? + * + * TODO Have also deleted test that if relative path is given it will write that relative to $RUN_DIR. + * That is not the case: it is relative to $HOME, which seems fine. For example, if copyResource + * is used during install phase then $RUN_DIR would be the wrong default. + * Is there any code that relies on this behaviour? + */ + File tempDataDirSub = new File(tempDataDir, "subdir"); + File tempDest = new File(tempDataDirSub, "tempDest.txt"); + String tempLocalContent = "abc"; + File tempLocal = new File(tempDataDir, "tempLocal.txt"); + Files.write(tempLocalContent, tempLocal, Charsets.UTF_8); + + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + + MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)); + app.start(ImmutableList.of(localhost)); + + // First confirm that even if createParentDir==false that it still gets created! + try { + entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath(), false); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + } finally { + Os.deleteRecursively(tempDataDirSub); + } + + // Copy to absolute path + try { + entity.getDriver().copyResource(tempLocal.toURI().toString(), tempDest.getAbsolutePath(), true); + assertEquals(Files.readLines(tempDest, Charsets.UTF_8), ImmutableList.of(tempLocalContent)); + } finally { + Os.deleteRecursively(tempDataDirSub); + } + } + + @Test(groups="Integration") + public void testPreAndPostLaunchCommands() throws IOException { + File tempFile = new File(tempDataDir, "tempFile.txt"); + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class) + .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "") + .configure(SoftwareProcess.PRE_LAUNCH_COMMAND, String.format("echo inPreLaunch >> %s", tempFile.getAbsoluteFile())) + .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, String.format("echo inLaunch >> %s", tempFile.getAbsoluteFile())) + .configure(SoftwareProcess.POST_LAUNCH_COMMAND, String.format("echo inPostLaunch >> %s", tempFile.getAbsoluteFile()))); + app.start(ImmutableList.of(localhost)); + + List<String> output = Files.readLines(tempFile, Charsets.UTF_8); + assertEquals(output.size(), 3); + assertEquals(output.get(0), "inPreLaunch"); + assertEquals(output.get(1), "inLaunch"); + assertEquals(output.get(2), "inPostLaunch"); + tempFile.delete(); + } + + @Test(groups="Integration") + public void testInstallResourcesCopy() throws IOException { + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + File template = new File(Os.tmp(), "template.yaml"); + VanillaSoftwareProcess entity = app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class) + .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "") + .configure(SoftwareProcess.INSTALL_FILES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/frogs.txt", "frogs.txt")) + .configure(SoftwareProcess.INSTALL_TEMPLATES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/template.yaml", template.getAbsolutePath())) + .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "date")); + app.start(ImmutableList.of(localhost)); + + File frogs = new File(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "frogs.txt"); + try { + Assert.assertTrue(frogs.canRead(), "File not readable: " + frogs); + String output = Files.toString(frogs, Charsets.UTF_8); + Assert.assertTrue(output.contains("Brekekekex"), "File content not found: " + output); + } finally { + frogs.delete(); + } + + try { + String expectedHostname = BrooklynNetworkUtils.getLocalhostInetAddress().getHostName(); + String expectedIp = BrooklynNetworkUtils.getLocalhostInetAddress().getHostAddress(); + + Map<?,?> data = (Map) Iterables.getOnlyElement(Yamls.parseAll(Files.toString(template, Charsets.UTF_8))); + Assert.assertEquals(data.size(), 3); + Assert.assertEquals(data.get("entity.hostname"), expectedHostname); + Assert.assertEquals(data.get("entity.address"), expectedIp); + Assert.assertEquals(data.get("frogs"), Integer.valueOf(12)); + } finally { + template.delete(); + } + } + + @Test(groups="Integration") + public void testRuntimeResourcesCopy() throws IOException { + localhost.setConfig(BrooklynConfigKeys.ONBOX_BASE_DIR, tempDataDir.getAbsolutePath()); + File template = new File(Os.tmp(), "template.yaml"); + VanillaSoftwareProcess entity = app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class) + .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "") + .configure(SoftwareProcess.RUNTIME_FILES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/frogs.txt", "frogs.txt")) + .configure(SoftwareProcess.RUNTIME_TEMPLATES, MutableMap.of("classpath://org/apache/brooklyn/entity/software/base/template.yaml", template.getAbsolutePath())) + .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "date")); + app.start(ImmutableList.of(localhost)); + + File frogs = new File(entity.getAttribute(SoftwareProcess.RUN_DIR), "frogs.txt"); + try { + Assert.assertTrue(frogs.canRead(), "File not readable: " + frogs); + String output = Files.toString(frogs, Charsets.UTF_8); + Assert.assertTrue(output.contains("Brekekekex"), "File content not found: " + output); + } finally { + frogs.delete(); + } + + try { + String expectedHostname = BrooklynNetworkUtils.getLocalhostInetAddress().getHostName(); + String expectedIp = BrooklynNetworkUtils.getLocalhostInetAddress().getHostAddress(); + + Map<?,?> data = (Map) Iterables.getOnlyElement(Yamls.parseAll(Files.toString(template, Charsets.UTF_8))); + Assert.assertEquals(data.size(), 3); + Assert.assertEquals(data.get("entity.hostname"), expectedHostname); + Assert.assertEquals(data.get("entity.address"), expectedIp); + Assert.assertEquals(data.get("frogs"), Integer.valueOf(12)); + } finally { + template.delete(); + } + } + + @ImplementedBy(MyServiceImpl.class) + public interface MyService extends SoftwareProcess { + public SimulatedDriver getDriver(); + } + + public static class MyServiceImpl extends SoftwareProcessImpl implements MyService { + public MyServiceImpl() { + } + + @Override + public Class<?> getDriverInterface() { + return SimulatedDriver.class; + } + + @Override + public SimulatedDriver getDriver() { + return (SimulatedDriver) super.getDriver(); + } + } + + public static class SimulatedDriver extends AbstractSoftwareProcessSshDriver { + public List<String> events = new ArrayList<String>(); + private volatile boolean launched = false; + + public SimulatedDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public void install() { + events.add("install"); + newScript(INSTALLING) + .failOnNonZeroResultCode() + .body.append("touch myfile") + .execute(); + } + + @Override + public void customize() { + events.add("customize"); + } + + @Override + public void launch() { + events.add("launch"); + launched = true; + entity.setAttribute(Startable.SERVICE_UP, true); + } + + @Override + public boolean isRunning() { + return launched; + } + + @Override + public void stop() { + events.add("stop"); + launched = false; + entity.setAttribute(Startable.SERVICE_UP, false); + } + + @Override + public void kill() { + events.add("kill"); + launched = false; + entity.setAttribute(Startable.SERVICE_UP, false); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java ---------------------------------------------------------------------- diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java new file mode 100644 index 0000000..338b1f3 --- /dev/null +++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessSubclassTest.java @@ -0,0 +1,169 @@ +/* + * 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.software.base; + +import static org.testng.Assert.assertEquals; + +import java.util.Collections; +import java.util.List; + +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.effector.core.EffectorAndBody; +import org.apache.brooklyn.effector.core.MethodEffector; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + + +public class SoftwareProcessSubclassTest extends BrooklynAppUnitTestSupport { + +// NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves. + + @SuppressWarnings("unused") + private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessSubclassTest.class); + + @ImplementedBy(SubSoftwareProcessImpl.class) + public static interface SubSoftwareProcess extends EmptySoftwareProcess { + public List<String> getCallHistory(); + public void triggerStopOutsideOfEffector(); + public void customRestart(); + } + + public static class SubSoftwareProcessImpl extends EmptySoftwareProcessImpl implements SubSoftwareProcess { + + protected List<String> callHistory = Collections.synchronizedList(Lists.<String>newArrayList()); + + @Override + public void init() { + super.init(); + getMutableEntityType().addEffector(new EffectorAndBody<Void>(SoftwareProcess.RESTART, new MethodEffector<Void>(SubSoftwareProcess.class, "customRestart").getBody())); + } + + @Override + public List<String> getCallHistory() { + return callHistory; + } + + @Override + public void preStart() { + callHistory.add("doStart"); + super.preStart(); + } + + @Override + public void preStop() { + callHistory.add("doStop"); + super.preStop(); + } + + @Override + public void customRestart() { + callHistory.add("doRestart"); + } + + @Override + public void triggerStopOutsideOfEffector() { + stop(); + } + + } + + private Location loc; + private List<Location> locs; + private SubSoftwareProcess entity; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + loc = mgmt.getLocationRegistry().resolve("localhost"); + locs = ImmutableList.of(loc); + entity = app.createAndManageChild(EntitySpec.create(SubSoftwareProcess.class)); + } + + @Test + public void testStartCalledViaMethod() throws Exception { + entity.start(locs); + + assertCallHistory(ImmutableList.of("doStart")); + } + + @Test + public void testStopCalledViaMethod() throws Exception { + app.start(locs); + entity.stop(); + + assertCallHistory(ImmutableList.of("doStart", "doStop")); + } + + @Test + public void testRestartCalledViaMethod() throws Exception { + app.start(locs); + entity.restart(); + + assertCallHistory(ImmutableList.of("doStart", "doRestart")); + } + + @Test + public void testStopCalledWithoutEffector() throws Exception { + app.start(locs); + entity.triggerStopOutsideOfEffector(); + + assertCallHistory(ImmutableList.of("doStart", "doStop")); + } + + @Test + public void testStartCalledViaInvokeEffector() throws Exception { + entity.invoke(SubSoftwareProcess.START, ImmutableMap.<String,Object>of("locations", locs)).get(); + + assertCallHistory(ImmutableList.of("doStart")); + } + + @Test + public void testStopCalledViaInvokeEffector() throws Exception { + app.start(locs); + entity.invoke(SubSoftwareProcess.STOP, ImmutableMap.<String,Object>of()).get(); + + assertCallHistory(ImmutableList.of("doStart", "doStop")); + } + + @Test + public void testRestartCalledViaInvokeEffector() throws Exception { + app.start(locs); + entity.invoke(SubSoftwareProcess.RESTART, ImmutableMap.<String,Object>of()).get(); + + assertCallHistory(ImmutableList.of("doStart", "doRestart")); + } + + private void assertCallHistory(Iterable<String> expected) { + List<String> actual = entity.getCallHistory(); + assertEquals(actual, ImmutableList.copyOf(expected), "actual="+actual); + } +}
