/*
* 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.springframework.test.util.MatcherAssertionErrors.assertThat;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Test;
import org.springframework.xd.dirt.core.DeploymentUnitStatus;
import org.springframework.xd.rest.client.impl.SpringXDTemplate;
import org.springframework.xd.test.fixtures.EventuallyMatcher;
/**
* Series of tests to validate job deployments and job state transitions.
* This test uses the {@code timestampfile} job.
*
* @author Patrick Peralta
*/
public class JobStateTests extends AbstractDistributedTests {
/**
* Output directory for {@code timestampfile} job.
*/
private final static String DIRECTORY = System.getProperty("java.io.tmpdir");
/**
* Extension for {@code timestampfile} job output file.
*/
private static final String FILE_EXTENSION = "txt";
/**
* Date format for {@code timestampfile} job output file.
*/
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* Assert the correct deployment and state transition for job deployments,
* in particular with regard to container availability.
*
* @throws Exception
*/
@Test
public void testJobStateTransition() throws Exception {
SpringXDTemplate template = ensureTemplate();
String jobName = testName.getMethodName() + "-ticktock";
String definition = String.format("timestampfile --directory=%s --format='%s' --fileExtension=%s",
DIRECTORY, DATE_FORMAT, FILE_EXTENSION);
String fileName = DIRECTORY + File.separatorChar + jobName + '.' + FILE_EXTENSION;
try {
template.jobOperations().createJob(jobName, definition, false);
verifyJobCreated(jobName);
verifyJobState(jobName, DeploymentUnitStatus.State.undeployed);
template.jobOperations().deploy(jobName, null);
verifyJobState(jobName, DeploymentUnitStatus.State.failed);
startContainer();
Map<Long, String> mapPidUuid = waitForContainers();
assertEquals(1, mapPidUuid.size());
verifyJobState(jobName, DeploymentUnitStatus.State.deployed);
template.jobOperations().launchJob(jobName, null);
assertThat(fileName, fileUpdated(System.currentTimeMillis()));
shutdownContainer(mapPidUuid.keySet().iterator().next());
waitForContainers();
verifyJobState(jobName, DeploymentUnitStatus.State.failed);
startContainer();
waitForContainers();
verifyJobState(jobName, DeploymentUnitStatus.State.deployed);
}
finally {
File file = new File(fileName);
if (file.exists()) {
if (!file.delete()) {
logger.warn("Could not delete job output file {}", fileName);
}
}
}
}
/**
* Wrap a {@link FileContentMatcher} in an {@link EventuallyMatcher}.
*
* @param startTime the approximate start time of the test
* @return matcher for the expected file
*/
private EventuallyMatcher<String> fileUpdated(long startTime) {
return new EventuallyMatcher<String>(new FileContentMatcher(startTime));
}
/**
* Custom matcher for the {@code timestampfile} job output file.
* This matcher performs the following checks:
* <ul>
* <li>The expected file was created</li>
* <li>The file contains a timestamp as its last line</li>
* <li>The timestamp indicates a time not less than 60
* seconds after the test was launched; this handles
* the scenario where a prior test run correctly
* executed the job but the current test run did not</li>
* </ul>
*/
private class FileContentMatcher extends BaseMatcher<String> {
/**
* Approximate start time for the running test.
*/
private final long startTime;
/**
* Description of mismatch; i.e. a detailed description
* of failed assertions.
*/
private final StringBuilder mismatch = new StringBuilder();
/**
* Formatter for dates in the job output file.
*/
private final DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
/**
* Construct a {@code FileContentMatcher} that expects a timestamp
* in the job output file.
*
* @param startTime the approximate start time of the test
*/
private FileContentMatcher(long startTime) {
this.startTime = startTime;
}
/**
* Return {@code true} if:
* <ul>
* <li>The expected file was created</li>
* <li>The file contains a timestamp as its last line</li>
* <li>The timestamp indicates a time not less than 60
* seconds after the test was launched; this handles
* the scenario where a prior test run correctly
* executed the job but the current test run did not</li>
* </ul>
*
* @param item name of the timestamp file that should be created by the job
* @return true if the file contains an acceptable timestamp
*/
@Override
public boolean matches(Object item) {
String fileName = (String) item;
File file = new File(fileName);
if (!file.exists()) {
mismatch.append("file ").append(fileName).append(" not found");
return false;
}
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
String lastLine = null;
String line;
do {
line = reader.readLine();
if (line != null) {
lastLine = line;
}
}
while (line != null);
if (lastLine == null) {
mismatch.append("file ").append(fileName).append(" is empty");
return false;
}
long fileTime = dateFormat.parse(lastLine).getTime();
if (fileTime - startTime > 60000) {
mismatch.append("file timestamp was written more than one minute after start of test: ")
.append(" file timestamp: ")
.append(lastLine)
.append(", start of test: ")
.append(dateFormat.format(new Date(startTime)));
return false;
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
if (reader != null) {
try {
reader.close();
}
catch (IOException e) {
// ignore errors on close
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void describeMismatch(Object item, Description description) {
description.appendText(mismatch.toString());
}
/**
* {@inheritDoc}
*/
@Override
public void describeTo(Description description) {
description.appendText("expected timestamp file");
}
}
}