/* * Copyright 2006-2013 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.batch.core.job; import static org.mockito.Mockito.mock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import org.junit.Before; import org.junit.Test; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobExecutionException; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobInterruptedException; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.UnexpectedJobExecutionException; import org.springframework.batch.core.listener.JobExecutionListenerSupport; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.dao.ExecutionContextDao; import org.springframework.batch.core.repository.dao.JobExecutionDao; import org.springframework.batch.core.repository.dao.JobInstanceDao; import org.springframework.batch.core.repository.dao.MapExecutionContextDao; import org.springframework.batch.core.repository.dao.MapJobExecutionDao; import org.springframework.batch.core.repository.dao.MapJobInstanceDao; import org.springframework.batch.core.repository.dao.MapStepExecutionDao; import org.springframework.batch.core.repository.dao.StepExecutionDao; import org.springframework.batch.core.repository.support.SimpleJobRepository; import org.springframework.batch.core.step.StepSupport; import org.springframework.batch.item.ExecutionContext; /** * Tests for DefaultJobLifecycle. MapJobDao and MapStepExecutionDao are used * instead of a mock repository to test that status is being stored correctly. * * @author Lucas Ward * @author Will Schipp */ public class SimpleJobTests { private JobRepository jobRepository; private JobInstanceDao jobInstanceDao; private JobExecutionDao jobExecutionDao; private StepExecutionDao stepExecutionDao; private ExecutionContextDao ecDao; private List<Serializable> list = new ArrayList<Serializable>(); private JobInstance jobInstance; private JobExecution jobExecution; private StepExecution stepExecution1; private StepExecution stepExecution2; private StubStep step1; private StubStep step2; private JobParameters jobParameters = new JobParameters(); private SimpleJob job; @Before public void setUp() throws Exception { jobInstanceDao = new MapJobInstanceDao(); jobExecutionDao = new MapJobExecutionDao(); stepExecutionDao = new MapStepExecutionDao(); ecDao = new MapExecutionContextDao(); jobRepository = new SimpleJobRepository(jobInstanceDao, jobExecutionDao, stepExecutionDao, ecDao); job = new SimpleJob(); job.setJobRepository(jobRepository); step1 = new StubStep("TestStep1", jobRepository); step1.setCallback(new Runnable() { @Override public void run() { list.add("default"); } }); step2 = new StubStep("TestStep2", jobRepository); step2.setCallback(new Runnable() { @Override public void run() { list.add("default"); } }); List<Step> steps = new ArrayList<Step>(); steps.add(step1); steps.add(step2); job.setName("testJob"); job.setSteps(steps); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); jobInstance = jobExecution.getJobInstance(); stepExecution1 = new StepExecution(step1.getName(), jobExecution); stepExecution2 = new StepExecution(step2.getName(), jobExecution); } /** * Test method for {@link SimpleJob#setSteps(java.util.List)}. */ @Test public void testSetSteps() { job.setSteps(Collections.singletonList((Step) new StepSupport("step"))); job.execute(jobExecution); assertEquals(1, jobExecution.getStepExecutions().size()); } /** * Test method for {@link SimpleJob#setSteps(java.util.List)}. */ @Test public void testGetSteps() { assertEquals(2, job.getStepNames().size()); } /** * Test method for * {@link SimpleJob#addStep(org.springframework.batch.core.Step)}. */ @Test public void testAddStep() { job.setSteps(Collections.<Step> emptyList()); job.addStep(new StepSupport("step")); job.execute(jobExecution); assertEquals(1, jobExecution.getStepExecutions().size()); } // Test to ensure the exit status returned by the last step is returned @Test public void testExitStatusReturned() throws JobExecutionException { final ExitStatus customStatus = new ExitStatus("test"); Step testStep = new Step() { @Override public void execute(StepExecution stepExecution) throws JobInterruptedException { stepExecution.setExitStatus(customStatus); } @Override public String getName() { return "test"; } @Override public int getStartLimit() { return 1; } @Override public boolean isAllowStartIfComplete() { return false; } }; List<Step> steps = new ArrayList<Step>(); steps.add(testStep); job.setSteps(steps); job.execute(jobExecution); assertEquals(customStatus, jobExecution.getExitStatus()); } @Test public void testRunNormally() throws Exception { step1.setStartLimit(5); step2.setStartLimit(5); job.execute(jobExecution); assertEquals(2, list.size()); checkRepository(BatchStatus.COMPLETED); assertNotNull(jobExecution.getEndTime()); assertNotNull(jobExecution.getStartTime()); assertTrue(step1.passedInJobContext.isEmpty()); assertFalse(step2.passedInJobContext.isEmpty()); } @Test public void testRunNormallyWithListener() throws Exception { job.setJobExecutionListeners(new JobExecutionListenerSupport[] { new JobExecutionListenerSupport() { @Override public void beforeJob(JobExecution jobExecution) { list.add("before"); } @Override public void afterJob(JobExecution jobExecution) { list.add("after"); } } }); job.execute(jobExecution); assertEquals(4, list.size()); } @Test public void testRunWithSimpleStepExecutor() throws Exception { job.setJobRepository(jobRepository); // do not set StepExecutorFactory... step1.setStartLimit(5); step2.setStartLimit(5); job.execute(jobExecution); assertEquals(2, list.size()); checkRepository(BatchStatus.COMPLETED, ExitStatus.COMPLETED); } @Test public void testExecutionContextIsSet() throws Exception { testRunNormally(); assertEquals(jobInstance, jobExecution.getJobInstance()); assertEquals(2, jobExecution.getStepExecutions().size()); assertEquals(step1.getName(), stepExecution1.getStepName()); assertEquals(step2.getName(), stepExecution2.getStepName()); } @Test public void testInterrupted() throws Exception { step1.setStartLimit(5); step2.setStartLimit(5); final JobInterruptedException exception = new JobInterruptedException("Interrupt!"); step1.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); assertEquals(exception, jobExecution.getStepExecutions().iterator().next().getFailureExceptions().get(0)); assertEquals(0, list.size()); checkRepository(BatchStatus.STOPPED, ExitStatus.STOPPED); } @Test public void testInterruptedAfterUnknownStatus() throws Exception { step1.setStartLimit(5); step2.setStartLimit(5); final JobInterruptedException exception = new JobInterruptedException("Interrupt!", BatchStatus.UNKNOWN); step1.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); assertEquals(exception, jobExecution.getStepExecutions().iterator().next().getFailureExceptions().get(0)); assertEquals(0, list.size()); checkRepository(BatchStatus.UNKNOWN, ExitStatus.STOPPED); } @Test public void testFailed() throws Exception { step1.setStartLimit(5); step2.setStartLimit(5); final RuntimeException exception = new RuntimeException("Foo!"); step1.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); assertEquals(exception, jobExecution.getAllFailureExceptions().get(0)); assertEquals(0, list.size()); assertEquals(BatchStatus.FAILED, jobExecution.getStatus()); checkRepository(BatchStatus.FAILED, ExitStatus.FAILED); } @Test public void testFailedWithListener() throws Exception { job.setJobExecutionListeners(new JobExecutionListenerSupport[] { new JobExecutionListenerSupport() { @Override public void afterJob(JobExecution jobExecution) { list.add("afterJob"); } } }); final RuntimeException exception = new RuntimeException("Foo!"); step1.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); assertEquals(exception, jobExecution.getAllFailureExceptions().get(0)); assertEquals(1, list.size()); checkRepository(BatchStatus.FAILED, ExitStatus.FAILED); } @Test public void testFailedWithError() throws Exception { step1.setStartLimit(5); step2.setStartLimit(5); final Error exception = new Error("Foo!"); step1.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); assertEquals(exception, jobExecution.getAllFailureExceptions().get(0)); assertEquals(0, list.size()); checkRepository(BatchStatus.FAILED, ExitStatus.FAILED); } @Test public void testStepShouldNotStart() throws Exception { // Start policy will return false, keeping the step from being started. step1.setStartLimit(0); job.execute(jobExecution); assertEquals(1, jobExecution.getFailureExceptions().size()); Throwable ex = jobExecution.getFailureExceptions().get(0); assertTrue("Wrong message in exception: " + ex.getMessage(), ex.getMessage().indexOf("start limit exceeded") >= 0); } @Test public void testStepAlreadyComplete() throws Exception { stepExecution1.setStatus(BatchStatus.COMPLETED); jobRepository.add(stepExecution1); jobExecution.setEndTime(new Date()); jobRepository.update(jobExecution); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); assertEquals(0, jobExecution.getFailureExceptions().size()); assertEquals(1, jobExecution.getStepExecutions().size()); assertEquals(stepExecution2.getStepName(), jobExecution.getStepExecutions().iterator().next().getStepName()); } @Test public void testStepAlreadyCompleteInSameExecution() throws Exception { List<Step> steps = new ArrayList<Step>(); steps.add(step1); steps.add(step2); // Two steps with the same name should both be executed, since // the user might actually want it to happen twice. On a restart // it would be executed twice again, even if it failed on the // second execution. This seems reasonable. steps.add(step2); job.setSteps(steps); job.execute(jobExecution); assertEquals(0, jobExecution.getFailureExceptions().size()); assertEquals(3, jobExecution.getStepExecutions().size()); assertEquals(stepExecution1.getStepName(), jobExecution.getStepExecutions().iterator().next().getStepName()); } @Test public void testNoSteps() throws Exception { job.setSteps(new ArrayList<Step>()); job.execute(jobExecution); ExitStatus exitStatus = jobExecution.getExitStatus(); assertTrue("Wrong message in execution: " + exitStatus, exitStatus.getExitDescription().indexOf( "no steps configured") >= 0); } @Test public void testNotExecutedIfAlreadyStopped() throws Exception { jobExecution.stop(); job.execute(jobExecution); assertEquals(0, list.size()); checkRepository(BatchStatus.STOPPED, ExitStatus.NOOP); ExitStatus exitStatus = jobExecution.getExitStatus(); assertEquals(ExitStatus.NOOP.getExitCode(), exitStatus.getExitCode()); } @Test public void testRestart() throws Exception { step1.setAllowStartIfComplete(true); final RuntimeException exception = new RuntimeException("Foo!"); step2.setProcessException(exception); job.execute(jobExecution); Throwable e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); assertTrue(step1.passedInStepContext.isEmpty()); assertFalse(step2.passedInStepContext.isEmpty()); } @Test public void testRestartWithNullParameter() throws Exception { JobParameters jobParameters = new JobParametersBuilder().addString("foo", null).toJobParameters(); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); jobInstance = jobExecution.getJobInstance(); step1.setAllowStartIfComplete(true); final RuntimeException exception = new RuntimeException("Foo!"); step2.setProcessException(exception); job.execute(jobExecution); Throwable e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); assertTrue(step1.passedInStepContext.isEmpty()); assertFalse(step2.passedInStepContext.isEmpty()); } @Test public void testInterruptWithListener() throws Exception { step1.setProcessException(new JobInterruptedException("job interrupted!")); JobExecutionListener listener = mock(JobExecutionListener.class); listener.beforeJob(jobExecution); listener.afterJob(jobExecution); job.setJobExecutionListeners(new JobExecutionListener[] { listener }); job.execute(jobExecution); assertEquals(BatchStatus.STOPPED, jobExecution.getStatus()); } /** * Execution context should be restored on restart. */ @Test public void testRestartAndExecutionContextRestored() throws Exception { job.setRestartable(true); step1.setAllowStartIfComplete(true); final RuntimeException exception = new RuntimeException("Foo!"); step2.setProcessException(exception); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); Throwable e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); assertTrue(step1.passedInJobContext.isEmpty()); assertFalse(step2.passedInJobContext.isEmpty()); assertFalse(jobExecution.getExecutionContext().isEmpty()); jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); assertEquals(1, jobExecution.getAllFailureExceptions().size()); e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); assertFalse(step1.passedInJobContext.isEmpty()); assertFalse(step2.passedInJobContext.isEmpty()); } @Test public void testInterruptJob() throws Exception { step1 = new StubStep("interruptStep", jobRepository) { @Override public void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException { stepExecution.getJobExecution().stop(); super.execute(stepExecution); } }; job.setSteps(Arrays.asList(new Step[] { step1, step2 })); job.execute(jobExecution); assertEquals(BatchStatus.STOPPED, jobExecution.getStatus()); assertEquals(1, jobExecution.getAllFailureExceptions().size()); Throwable expected = jobExecution.getAllFailureExceptions().get(0); assertTrue("Wrong exception " + expected, expected instanceof JobInterruptedException); assertEquals("JobExecution interrupted.", expected.getMessage()); assertNull("Second step was not supposed to be executed", step2.passedInStepContext); } @Test public void testGetStepExists() { step1 = new StubStep("step1", jobRepository); step2 = new StubStep("step2", jobRepository); job.setSteps(Arrays.asList(new Step[] { step1, step2 })); Step step = job.getStep("step2"); assertNotNull(step); assertEquals("step2", step.getName()); } @Test public void testGetStepNotExists() { step1 = new StubStep("step1", jobRepository); step2 = new StubStep("step2", jobRepository); job.setSteps(Arrays.asList(new Step[] { step1, step2 })); Step step = job.getStep("foo"); assertNull(step); } /* * Check JobRepository to ensure status is being saved. */ private void checkRepository(BatchStatus status, ExitStatus exitStatus) { assertEquals(jobInstance, jobInstanceDao.getJobInstance(job.getName(), jobParameters)); // because map DAO stores in memory, it can be checked directly JobExecution jobExecution = jobExecutionDao.findJobExecutions(jobInstance).get(0); assertEquals(jobInstance.getId(), jobExecution.getJobId()); assertEquals(status, jobExecution.getStatus()); if (exitStatus != null) { assertEquals(exitStatus.getExitCode(), jobExecution.getExitStatus().getExitCode()); } } private void checkRepository(BatchStatus status) { checkRepository(status, null); } private static class StubStep extends StepSupport { private Runnable runnable; private Throwable exception; private JobRepository jobRepository; private ExecutionContext passedInStepContext; private ExecutionContext passedInJobContext; /** * @param string */ public StubStep(String string, JobRepository jobRepository) { super(string); this.jobRepository = jobRepository; } /** * @param exception */ public void setProcessException(Throwable exception) { this.exception = exception; } /** * @param runnable */ public void setCallback(Runnable runnable) { this.runnable = runnable; } /* * (non-Javadoc) * * @seeorg.springframework.batch.core.step.StepSupport#execute(org. * springframework.batch.core.StepExecution) */ @Override public void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException { passedInJobContext = new ExecutionContext(stepExecution.getJobExecution().getExecutionContext()); passedInStepContext = new ExecutionContext(stepExecution.getExecutionContext()); stepExecution.getExecutionContext().putString("stepKey", "stepValue"); stepExecution.getJobExecution().getExecutionContext().putString("jobKey", "jobValue"); jobRepository.update(stepExecution); jobRepository.updateExecutionContext(stepExecution); if (exception instanceof JobInterruptedException) { stepExecution.setExitStatus(ExitStatus.FAILED); stepExecution.setStatus(((JobInterruptedException) exception).getStatus()); stepExecution.addFailureException(exception); throw (JobInterruptedException) exception; } if (exception instanceof RuntimeException) { stepExecution.setExitStatus(ExitStatus.FAILED); stepExecution.setStatus(BatchStatus.FAILED); stepExecution.addFailureException(exception); return; } if (exception instanceof Error) { stepExecution.setExitStatus(ExitStatus.FAILED); stepExecution.setStatus(BatchStatus.FAILED); stepExecution.addFailureException(exception); return; } if (exception instanceof JobInterruptedException) { stepExecution.setExitStatus(ExitStatus.FAILED); stepExecution.setStatus(BatchStatus.FAILED); stepExecution.addFailureException(exception); return; } if (runnable != null) { runnable.run(); } stepExecution.setExitStatus(ExitStatus.COMPLETED); stepExecution.setStatus(BatchStatus.COMPLETED); jobRepository.update(stepExecution); jobRepository.updateExecutionContext(stepExecution); } } }