/*
* Copyright 2013-2015 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.dirt.plugins.job;
import static org.hamcrest.Matchers.anyOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload;
import static org.springframework.xd.module.options.spi.ModulePlaceholders.XD_JOB_NAME_KEY;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.io.Resource;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.validation.BindException;
import org.springframework.xd.batch.hsqldb.server.HsqlDatasourceConfiguration;
import org.springframework.xd.batch.hsqldb.server.HsqlServerApplication;
import org.springframework.xd.dirt.integration.bus.AbstractTestMessageBus;
import org.springframework.xd.dirt.integration.bus.MessageBus;
import org.springframework.xd.dirt.integration.bus.local.LocalMessageBus;
import org.springframework.xd.module.ModuleDeploymentProperties;
import org.springframework.xd.module.ModuleDescriptor;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.module.TestModuleDefinitions;
import org.springframework.xd.module.core.Module;
import org.springframework.xd.module.core.ResourceConfiguredModule;
import org.springframework.xd.module.options.ModuleOptionsMetadata;
import org.springframework.xd.test.RandomConfigurationSupport;
/**
*
* @author Michael Minella
* @author Gunnar Hillert
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author David Turanski
*
*/
public class JobPluginTests extends RandomConfigurationSupport {
private JobPlugin jobPlugin;
private JobPartitionerPlugin jobPartitionerPlugin;
private ConfigurableApplicationContext sharedContext;
protected AbstractTestMessageBus<?> testMessageBus;
private LocalMessageBus messageBus;
@After
public void tearDown() {
if (sharedContext != null) {
sharedContext.close();
}
}
@Configuration
@ImportResource({ "classpath:/META-INF/spring-xd/batch/batch.xml",
"classpath:/META-INF/spring-xd/bus/local-bus.xml" })
@EnableAutoConfiguration
public static class SharedConfiguration {
}
@Before
public void setUp() throws Exception {
sharedContext = new SpringApplicationBuilder(SharedConfiguration.class, HsqlDatasourceConfiguration.class,
HsqlServerApplication.class)
.profiles(HsqlServerApplication.HSQLDBSERVER_PROFILE)
.web(false).run();
messageBus = sharedContext.getBean(LocalMessageBus.class);
jobPlugin = new JobPlugin(messageBus);
jobPartitionerPlugin = new JobPartitionerPlugin(messageBus);
}
@Test
public void streamNameAdded() {
ModuleDescriptor descriptor = new ModuleDescriptor.Builder()
.setModuleDefinition(TestModuleDefinitions.dummy("testJob", ModuleType.job))
.setGroup("foo")
.setIndex(0)
.build();
Module module = new ResourceConfiguredModule(descriptor,
new ModuleDeploymentProperties());
assertEquals(0, module.getProperties().size());
jobPlugin.preProcessModule(module);
Properties moduleProperties = module.getProperties();
assertEquals("foo", moduleProperties.getProperty(XD_JOB_NAME_KEY));
}
@Test
public void jobOptionsDefaults() throws BindException {
ModuleOptionsMetadata metadata = new JobPluginMetadataResolver().resolve(TestModuleDefinitions.dummy("foo",
ModuleType.job));
Map<String, String> emptyMap = Collections.emptyMap();
EnumerablePropertySource<?> ps = metadata.interpolate(emptyMap).asPropertySource();
assertEquals(true, ps.getProperty("makeUnique"));
assertEquals("yyyy-MM-dd", ps.getProperty("dateFormat"));
assertEquals("", ps.getProperty("numberFormat"));
}
@Test
public void partitionedJob() {
String moduleGroupName = "partitionedJob";
int moduleIndex = 0;
Module module = Mockito.mock(Module.class);
when(module.getType()).thenReturn(ModuleType.job);
Properties properties = new Properties();
when(module.getProperties()).thenReturn(properties);
ModuleDeploymentProperties deploymentProperties = new ModuleDeploymentProperties();
deploymentProperties.put("consumer.concurrency", "2");
when(module.getDeploymentProperties()).thenReturn(deploymentProperties);
when(module.getDescriptor()).thenReturn(
new ModuleDescriptor.Builder().setGroup(moduleGroupName).setIndex(moduleIndex).setModuleDefinition(
TestModuleDefinitions.dummy("testjob", ModuleType.job)).build());
MessageChannel stepsOut = new DirectChannel();
when(module.getComponent("stepExecutionRequests.output", MessageChannel.class)).thenReturn(stepsOut);
PollableChannel stepResultsIn = new QueueChannel();
when(module.getComponent("stepExecutionReplies.input", MessageChannel.class)).thenReturn(stepResultsIn);
final PollableChannel stepsIn = new QueueChannel();
DirectChannel boundStepsIn = new DirectChannel();
boundStepsIn.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
stepsIn.send(MessageBuilder.fromMessage(message)
.setHeader("thread", Thread.currentThread().getName())
.build());
}
});
when(module.getComponent("stepExecutionRequests.input", MessageChannel.class)).thenReturn(boundStepsIn);
MessageChannel stepResultsOut = new DirectChannel();
when(module.getComponent("stepExecutionReplies.output", MessageChannel.class)).thenReturn(stepResultsOut);
jobPartitionerPlugin.preProcessModule(module);
jobPartitionerPlugin.postProcessModule(module);
checkBusBound(messageBus);
assertEquals(1, TestUtils.getPropertyValue(messageBus, "reqRepExecutors", Map.class).size());
stepsOut.send(new GenericMessage<String>("foo1"));
stepsOut.send(new GenericMessage<String>("foo2"));
Message<?> stepExecutionRequest1 = stepsIn.receive(10000);
Message<?> stepExecutionRequest2 = stepsIn.receive(10000);
assertThat(stepExecutionRequest1, anyOf(hasPayload("foo1"), hasPayload("foo2")));
assertThat(stepExecutionRequest2, anyOf(hasPayload("foo1"), hasPayload("foo2")));
stepResultsOut.send(MessageBuilder.withPayload("bar1")
.copyHeaders(stepExecutionRequest1.getHeaders()) // replyTo
.build());
stepResultsOut.send(MessageBuilder.withPayload("bar2")
.copyHeaders(stepExecutionRequest1.getHeaders()) // replyTo
.build());
assertThat(stepResultsIn.receive(10000), anyOf(hasPayload("bar1"), hasPayload("bar2")));
assertThat(stepResultsIn.receive(10000), anyOf(hasPayload("bar1"), hasPayload("bar2")));
Set<String> threads = new HashSet<String>();
threads.add((String) stepExecutionRequest1.getHeaders().get("thread"));
threads.add((String) stepExecutionRequest2.getHeaders().get("thread"));
assertEquals(2, threads.size());
jobPartitionerPlugin.removeModule(module);
checkBusUnbound(messageBus);
assertEquals(0, TestUtils.getPropertyValue(messageBus, "reqRepExecutors", Map.class).size());
}
protected MessageBus getMessageBus() {
return messageBus;
}
protected void checkBusBound(MessageBus bus) {
assertEquals(2, TestUtils.getPropertyValue(bus, "requestReplyChannels", Map.class).size());
}
protected void checkBusUnbound(MessageBus bus) {
assertEquals(0, TestUtils.getPropertyValue(bus, "requestReplyChannels", Map.class).size());
}
@Test
public void streamComponentsAdded() {
Module module = Mockito.mock(Module.class);
Mockito.when(module.getType()).thenReturn(ModuleType.job);
Properties properties = new Properties();
Mockito.when(module.getProperties()).thenReturn(properties);
Mockito.when(module.getDescriptor()).thenReturn(
new ModuleDescriptor.Builder().setGroup("job").setIndex(0).setModuleDefinition(
TestModuleDefinitions.dummy("testjob", ModuleType.job)).build());
jobPlugin.preProcessModule(module);
Mockito.verify(module).addSource(Matchers.any(Resource.class));
// TODO: assert that the right resource was added.
// assertTrue(names.contains("registrar"));
// assertTrue(names.contains("jobFactoryBean"));
// assertTrue(names.contains("jobLaunchRequestTransformer"));
// assertTrue(names.contains("jobLaunchingMessageHandler"));
// assertTrue(names.contains("input"));
// assertTrue(names.contains("jobLaunchingChannel"));
// assertTrue(names.contains("notifications"));
}
@Test
public void testThatInputOutputChannelsAreBound() {
ModuleDescriptor moduleDescriptor = new ModuleDescriptor.Builder()
.setModuleDefinition(TestModuleDefinitions.dummy("myjob", ModuleType.job))
.setGroup("myjob")
.setIndex(0)
.build();
final Module module = new ResourceConfiguredModule(moduleDescriptor,
new ModuleDeploymentProperties());
final TestMessageBus messageBus = new TestMessageBus();
final JobPlugin plugin = new JobPlugin(messageBus);
final DirectChannel inputChannel = new DirectChannel();
final Module spiedModule = spy(module);
doReturn(inputChannel).when(spiedModule).getComponent("input", MessageChannel.class);
doReturn(null).when(spiedModule).getComponent("output", MessageChannel.class);
doReturn(null).when(spiedModule).getComponent("stepExecutionRequests.output", MessageChannel.class);
plugin.postProcessModule(spiedModule);
assertEquals(Integer.valueOf(1), Integer.valueOf(messageBus.getConsumerNames().size()));
assertEquals(Integer.valueOf(0), Integer.valueOf(messageBus.getProducerNames().size()));
assertEquals("job:myjob", messageBus.getConsumerNames().get(0));
}
@Test
public void testThatJobEventsChannelsAreBound() {
ModuleDescriptor moduleDescriptor = new ModuleDescriptor.Builder()
.setModuleDefinition(TestModuleDefinitions.dummy("myjob", ModuleType.job))
.setGroup("myjob")
.setIndex(0)
.build();
final Module module = new ResourceConfiguredModule(moduleDescriptor,
new ModuleDeploymentProperties());
final TestMessageBus messageBus = new TestMessageBus();
final JobEventsListenerPlugin eventsListenerPlugin = new JobEventsListenerPlugin(messageBus);
final SubscribableChannel jobExecutionEventsChannel = new PublishSubscribeChannel();
final SubscribableChannel stepExecutionEventsChannel = new PublishSubscribeChannel();
final SubscribableChannel chunkEventsChannel = new PublishSubscribeChannel();
final SubscribableChannel itemEventsChannel = new PublishSubscribeChannel();
final SubscribableChannel skipEventsChannel = new PublishSubscribeChannel();
final SubscribableChannel aggregatedEventsChannel = new PublishSubscribeChannel();
final Module spiedModule = spy(module);
doReturn(messageBus).when(spiedModule).getComponent(MessageBus.class);
doReturn(jobExecutionEventsChannel).when(spiedModule).getComponent("xd.job.jobExecutionEvents",
SubscribableChannel.class);
doReturn(stepExecutionEventsChannel).when(spiedModule).getComponent("xd.job.stepExecutionEvents",
SubscribableChannel.class);
doReturn(chunkEventsChannel).when(spiedModule).getComponent("xd.job.chunkEvents", SubscribableChannel.class);
doReturn(itemEventsChannel).when(spiedModule).getComponent("xd.job.itemEvents", SubscribableChannel.class);
doReturn(skipEventsChannel).when(spiedModule).getComponent("xd.job.skipEvents", SubscribableChannel.class);
doReturn(aggregatedEventsChannel).when(spiedModule).getComponent("xd.job.aggregatedEvents",
SubscribableChannel.class);
eventsListenerPlugin.preProcessModule(spiedModule);
eventsListenerPlugin.postProcessModule(spiedModule);
assertEquals(Integer.valueOf(0), Integer.valueOf(messageBus.getConsumerNames().size()));
assertEquals(Integer.valueOf(6), Integer.valueOf(messageBus.getProducerNames().size()));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob.job"));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob.step"));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob.chunk"));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob.item"));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob.skip"));
assertTrue(messageBus.getProducerNames().contains("tap:job:myjob"));
}
private class TestMessageBus implements MessageBus {
private List<String> consumerNames = new ArrayList<String>();
private List<String> producerNames = new ArrayList<String>();
@Override
public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) {
consumerNames.add(name);
}
@Override
public void bindPubSubConsumer(String name, MessageChannel inputChannel, Properties properties) {
Assert.fail("Should not be called.");
}
@Override
public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) {
producerNames.add(name);
}
@Override
public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) {
producerNames.add(name);
}
@Override
public void unbindConsumers(String name) {
Assert.fail("Should be not be called.");
}
@Override
public void unbindProducers(String name) {
Assert.fail("Should not be called.");
}
@Override
public void unbindConsumer(String name, MessageChannel channel) {
Assert.fail("Should not be called.");
}
@Override
public void unbindProducer(String name, MessageChannel channel) {
Assert.fail("Should not be called.");
}
public List<String> getConsumerNames() {
return consumerNames;
}
public List<String> getProducerNames() {
return producerNames;
}
@Override
public void bindRequestor(String name, MessageChannel requests, MessageChannel replies,
Properties properties) {
Assert.fail("Should not be called.");
}
@Override
public void bindReplier(String name, MessageChannel requests, MessageChannel replies,
Properties properties) {
Assert.fail("Should not be called.");
}
@Override
public MessageChannel bindDynamicProducer(String name, Properties properties) {
Assert.fail("Should not be called.");
return null;
}
@Override
public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) {
Assert.fail("Should not be called.");
return null;
}
@Override
public boolean isCapable(Capability capability) {
return false;
}
}
@After
public void cleanupMessageBus() {
if (testMessageBus != null) {
testMessageBus.cleanup();
}
}
}