/* * Copyright 2014 the original author or authors. * * Licensed 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.springframework.xd.distributed.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TestName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.hateoas.PagedResources; import org.springframework.xd.dirt.core.DeploymentUnitStatus; import org.springframework.xd.distributed.util.DefaultDistributedTestSupport; import org.springframework.xd.distributed.util.DistributedTestSupport; import org.springframework.xd.rest.client.impl.SpringXDTemplate; import org.springframework.xd.rest.domain.JobDefinitionResource; import org.springframework.xd.rest.domain.ModuleMetadataResource; import org.springframework.xd.rest.domain.StreamDefinitionResource; import com.oracle.tools.runtime.java.JavaApplication; import com.oracle.tools.runtime.java.SimpleJavaApplication; /** * Base class for distributed tests. Implementations of this class * <em>must</em> have their class names present in the {@link DistributedTestSuite} * {@code @Suite.SuiteClasses} annotation in order to be included in the * test suite executed by the build. * <p/> * Test implementations may assume that the minimum infrastructure * for an XD distributed system has been started up (admin server, ZooKeeper, * HSQL). Containers can be started by invoking {@link #startContainer}. Commands * may be invoked against the admin via the {@link SpringXDTemplate} returned * by {@link #ensureTemplate}. * <p/> * All containers will be shut down and all streams destroyed after each * test execution. The rest of the infrastructure will continue running * until the test suite has completed execution. * * @author Patrick Peralta */ public abstract class AbstractDistributedTests implements DistributedTestSupport { /** * Logger. */ protected final Logger logger = LoggerFactory.getLogger(getClass()); /** * Maximum number of milliseconds a thread will be blocked * while waiting for a module/job/stream state transition. */ private static final long STATE_TRANSITION_TIMEOUT = 60000; /** * Distributed test support infrastructure. */ private static DistributedTestSupport distributedTestSupport; /** * If true, this test is executing in the test suite {@link DistributedTestSuite}. * This also means that the test suite will manage the lifecycle of * {@link #distributedTestSupport}. If false, the instance of this test will * manage the lifecycle. */ private static boolean suiteDetected; /** * Name of currently executing unit test. */ @Rule public TestName testName = new TestName(); /** * Constructor for {@link AbstractDistributedTests}. If this test * is executing as part of test suite {@link DistributedTestSuite}, * the {@link #distributedTestSupport} is obtained from the test suite. * This also means that the lifecycle for the test support object * is managed by the test suite. */ protected AbstractDistributedTests() { if (DistributedTestSuite.distributedTestSupport != null) { distributedTestSupport = DistributedTestSuite.distributedTestSupport; suiteDetected = true; } } /** * Before test execution, ensure that an instance of * {@link #distributedTestSupport} has been created. If the reference * is null, a new instance is created. If a new instance is created, * this test will manage the lifecycle for the test support object. */ @Before public void before() throws Exception { if (distributedTestSupport == null) { distributedTestSupport = new DefaultDistributedTestSupport(); distributedTestSupport.startup(); suiteDetected = false; } } /** * After each test execution, all containers are shut down * and all streams/jobs destroyed. */ @After public void after() throws InterruptedException { distributedTestSupport.ensureTemplate().streamOperations().destroyAll(); distributedTestSupport.ensureTemplate().jobOperations().destroyAll(); distributedTestSupport.shutdownContainers(); } /** * After all tests are executed, {@link DistributedTestSupport#shutdownAll} * is invoked if an instance was not provided by the test suite * {@link DistributedTestSuite}. */ @AfterClass public static void afterClass() throws InterruptedException { if (!suiteDetected) { distributedTestSupport.shutdownAll(); } } /** * {@inheritDoc} */ @Override public void startup() { distributedTestSupport.startup(); } /** * {@inheritDoc} */ @Override public SpringXDTemplate ensureTemplate() { return distributedTestSupport.ensureTemplate(); } /** * {@inheritDoc} */ @Override public JavaApplication<SimpleJavaApplication> startContainer(Properties properties) { return distributedTestSupport.startContainer(properties); } /** * {@inheritDoc} */ @Override public JavaApplication<SimpleJavaApplication> startContainer() { return distributedTestSupport.startContainer(); } /** * {@inheritDoc} */ @Override public Map<Long, String> waitForContainers() throws InterruptedException { return distributedTestSupport.waitForContainers(); } /** * {@inheritDoc} */ @Override public void shutdownContainer(long pid) { distributedTestSupport.shutdownContainer(pid); } /** * {@inheritDoc} */ @Override public void shutdownContainers() throws InterruptedException { distributedTestSupport.shutdownContainers(); } /** * {@inheritDoc} */ @Override public void shutdownAll() throws InterruptedException { distributedTestSupport.shutdownAll(); } /** * Return a mapping of runtime modules to containers for a * given stream name. Note that this method assumes that the * stream has been deployed. To ensure that the stream has been * deployed, invoke {@link #verifyStreamDeployed} prior to invoking * this method. * * @return mapping of modules to the containers they are deployed to * @see #verifyStreamDeployed */ protected ModuleRuntimeContainers retrieveModuleRuntimeContainers(String streamName) { ModuleRuntimeContainers containers = new ModuleRuntimeContainers(); for (ModuleMetadataResource module : distributedTestSupport.ensureTemplate() .runtimeOperations().listDeployedModules()) { String moduleStreamName = module.getUnitName(); String[] fields = module.getName().split("\\."); String moduleLabel = fields[0]; if (moduleStreamName.equals(streamName)) { switch (module.getModuleType()) { case source: containers.addSourceContainer(module.getContainerId()); break; case sink: containers.addSinkContainer(module.getContainerId()); break; case processor: containers.addProcessorContainer(module.getContainerId(), moduleLabel); } } } assertTrue(String.format("A source and/or sink module is missing from %s", containers), containers.isComplete()); return containers; } /** * Assert that the given stream has been created. * * @param streamName name of stream to verify */ protected void verifyStreamCreated(String streamName) throws InterruptedException { long expiry = System.currentTimeMillis() + STATE_TRANSITION_TIMEOUT; while (!streamExists(streamName) && System.currentTimeMillis() < expiry) { Thread.sleep(500); } assertTrue(streamExists(streamName)); } /** * Check if the stream exists. * * @param streamName stream name * @return boolean true if the stream exists. */ private boolean streamExists(String streamName) { PagedResources<StreamDefinitionResource> list = distributedTestSupport.ensureTemplate() .streamOperations().list(); for (StreamDefinitionResource stream : list) { if (stream.getName().equals(streamName)) { return true; } } return false; } /** * Assert that the given job has been created. * * @param jobName name of job to verify */ protected void verifyJobCreated(String jobName) throws InterruptedException { long expiry = System.currentTimeMillis() + STATE_TRANSITION_TIMEOUT; while (!streamExists(jobName) && System.currentTimeMillis() < expiry) { Thread.sleep(500); } assertTrue(jobExists(jobName)); } /** * Check if the job exists. * * @param jobName job name * @return boolean true if the stream exists. */ private boolean jobExists(String jobName) { PagedResources<JobDefinitionResource> list = distributedTestSupport.ensureTemplate() .jobOperations().list(); for (JobDefinitionResource job : list) { if (job.getName().equals(jobName)) { return true; } } return false; } /** * Return a {@link JobDefinitionResource} with a name matching * the requested {@code jobName}. * * @param jobName name of job * * @return a {@code JobDefinitionResource} for the job or {@code null} * if the job does not exist */ private JobDefinitionResource getJob(String jobName) { PagedResources<JobDefinitionResource> list = distributedTestSupport.ensureTemplate() .jobOperations().list(); for (JobDefinitionResource job : list) { if (job.getName().equals(jobName)) { return job; } } return null; } /** * Block the executing thread until either the stream state is * {@link DeploymentUnitStatus.State#deployed} or 30 seconds * have elapsed. * * @param streamName name of stream to verify * @throws InterruptedException */ protected void verifyStreamDeployed(String streamName) throws InterruptedException { verifyStreamState(streamName, DeploymentUnitStatus.State.deployed); } /** * Block the executing thread until either the stream state * matches the indicated state or 30 seconds have elapsed. * * @param streamName name of stream to verify * @param expected the expected state of the stream * @throws InterruptedException * @see #STATE_TRANSITION_TIMEOUT */ protected void verifyStreamState(String streamName, DeploymentUnitStatus.State expected) throws InterruptedException { long expiry = System.currentTimeMillis() + STATE_TRANSITION_TIMEOUT; DeploymentUnitStatus.State state = null; while (state != expected && System.currentTimeMillis() < expiry) { Thread.sleep(500); state = getStreamState(streamName); } logger.debug("Stream '{}' state: {}", streamName, state); assertEquals("Failed assertion for stream " + streamName, expected, state); } /** * Block the executing thread until either the job state * matches the indicated state or 30 seconds have elapsed. * * @param jobName name of job to verify * @param expected the expected state of the job * @throws InterruptedException * @see #STATE_TRANSITION_TIMEOUT */ protected void verifyJobState(String jobName, DeploymentUnitStatus.State expected) throws InterruptedException { long expiry = System.currentTimeMillis() + STATE_TRANSITION_TIMEOUT; DeploymentUnitStatus.State state = null; while (state != expected && System.currentTimeMillis() < expiry) { Thread.sleep(500); state = getJobState(jobName); } logger.debug("Job '{}' state: {}", jobName, state); assertEquals("Failed assertion for job " + jobName, expected, state); } /** * Return the state of the given stream. * * @param streamName name of stream for which to obtain state * @return the state of the stream */ protected DeploymentUnitStatus.State getStreamState(String streamName) { PagedResources<StreamDefinitionResource> list = distributedTestSupport.ensureTemplate() .streamOperations().list(); for (StreamDefinitionResource stream : list) { if (stream.getName().equals(streamName)) { return DeploymentUnitStatus.State.valueOf(stream.getStatus()); } } throw new IllegalStateException(String.format("Stream %s not deployed", streamName)); } /** * Return the state of the given job. * * @param jobName name of job for which to obtain state * @return the state of the job */ protected DeploymentUnitStatus.State getJobState(String jobName) { JobDefinitionResource job = getJob(jobName); if (job == null) { throw new IllegalStateException(String.format("Job %s not deployed", jobName)); } return DeploymentUnitStatus.State.valueOf(job.getStatus()); } /** * Mapping of source and sink modules to the containers they are * deployed to. */ protected static class ModuleRuntimeContainers { /** * IDs of containers that have deployed a source module. */ private final Collection<String> sourceContainers = new HashSet<String>(); /** * IDs of containers that have deployed a sink module. */ private final Collection<String> sinkContainers = new HashSet<String>(); /** * Map of container IDs to a set of processor module labels that * have been deployed by the container. */ private final Map<String, Set<String>> processorContainers = new HashMap<String, Set<String>>(); /** * @see #sourceContainers */ public Collection<String> getSourceContainers() { return sourceContainers; } /** * @see #sourceContainers */ public void addSourceContainer(String sourceContainer) { this.sourceContainers.add(sourceContainer); } /** * @see #processorContainers */ public Map<String, Set<String>> getProcessorContainers() { return processorContainers; } /** * @see #processorContainers */ public void addProcessorContainer(String container, String moduleDescription) { Set<String> labels = processorContainers.get(container); if (labels == null) { labels = new HashSet<String>(); processorContainers.put(container, labels); } labels.add(moduleDescription); } /** * @see #sinkContainers */ public Collection<String> getSinkContainers() { return sinkContainers; } /** * @see #sinkContainers */ public void addSinkContainer(String sinkContainer) { this.sinkContainers.add(sinkContainer); } /** * Return true if a source and sink container have been * populated. * * @return true if source and sink containers have been populated */ public boolean isComplete() { return (!sourceContainers.isEmpty() && !sinkContainers.isEmpty()); } @Override public String toString() { return "ModuleRuntimeContainers{" + "sourceContainers=" + sourceContainers + ", sinkContainers=" + sinkContainers + ", processorContainers=" + processorContainers + '}'; } } }