/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.component.workflow.execution.headless.internal;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.io.FileUtils;
import de.rcenvironment.core.component.workflow.api.WorkflowConstants;
import de.rcenvironment.core.component.workflow.execution.api.FinalWorkflowState;
import de.rcenvironment.core.utils.common.StringUtils;
/**
* Verifies that the actual behavior of workflows executed with "wf verify" match the the expected behavior given by certain indicator and
* text files.
*
* @author Doreen Seider
* @author Hendrik Abbenhaus
*/
public final class HeadlessWorkflowExecutionVerification
implements HeadlessWorkflowExecutionVerificationRecorder, HeadlessWorkflowExecutionVerificationResult {
/** Name of directory with workflows that are expected to fail. */
public static final String FAILURE_DIR_NAME = "failure";
/** Date-Format used for timestamps in results log. */
public static final String DATE_FORMAT = "yyyy-MM-dd - HH:mm:ss";
/** File name suffix for files that contain expected log messages. */
public static final String FILE_SUFFIX_EXPECTED_LOG = ".log.expected";
/** File name suffix for files that contain prohibited log messages. */
public static final String FILE_SUFFIX_PROHIBITED_LOG = ".log.prohibited";
private static final String EMPTY_COLLECTION_STRING = "-";
private static final int THOUSAND = 1000;
private String wfFileRootDir = null;
private final List<File> wfFilesSubmitted;
private final int parallelRuns;
private final int sequentialRuns;
// measured in seconds as the current approach to determine workflow termination (via notification) doesn't allow more accuracy
private Map<File, List<Long>> wfsWithExecutionDurationSec = new HashMap<>();
private Map<File, List<String>> wfsWithExpectedLogMessages = new HashMap<>();
private Map<File, List<String>> wfsWithProhibitedLogMessages = new HashMap<>();
private int wfsExpectedToFinishCount;
private int wfsExpectedToFailCount;
private List<File> wfsExpectedToFail = new ArrayList<>();
private int resultCount = 0;
private int wfsFinishedAsExpectedCount = 0;
private int wfsFailedAsExpectedCount = 0;
private int wfsCanceledCount = 0;
private int wfsWithErrorCount = 0;
private int wfsWithUnexpectedFinalStateCount = 0;
private int wfsWithUnexpectedMissingLogMessageCount = 0;
private int wfsWithUnexpectedProhibitedLogMessageCount = 0;
private Map<File, FinalWorkflowState> wfsWithUnexpectedFinalState = new HashMap<>();
private Map<File, String> wfsWithUnexpectedMissingLogMessage = new HashMap<>();
private Map<File, String> wfsWithUnexpectedProhibitedLogMessage = new HashMap<>();
private List<File> wfsCanceled = new ArrayList<>();
private Map<File, String> wfsWithError = new HashMap<>();
private List<File> wfRelatedFilesToDelete = new ArrayList<>();
private boolean verified = true;
private Date startTime = null;
private Date endTime = null;
private HeadlessWorkflowExecutionVerification(List<File> wfFiles, int parallelRuns, int sequentialRuns) {
this.wfFilesSubmitted = wfFiles;
this.parallelRuns = parallelRuns;
this.sequentialRuns = sequentialRuns;
if (!wfFiles.isEmpty()) {
File parentFile = wfFiles.get(0).getParentFile();
if (!parentFile.getName().equals(FAILURE_DIR_NAME)) {
wfFileRootDir = parentFile.getAbsolutePath();
} else {
wfFileRootDir = parentFile.getParentFile().getAbsolutePath();
}
}
for (File wfFile : wfFiles) {
wfsWithExecutionDurationSec.put(wfFile, new ArrayList<Long>());
}
wfsExpectedToFinishCount = wfFiles.size() * parallelRuns * sequentialRuns;
}
/**
* Creates an initialized instance of {@link HeadlessWorkflowExecutionVerificationRecorder}.
*
* @param wfFiles the workflow files to consider
* @param parallelRuns number of expected parallel runs
* @param sequentialRuns number of expected sequential runs
* @return initialized instance of {@link HeadlessWorkflowExecutionVerificationRecorder}
* @throws IOException if initializing instance of {@link HeadlessWorkflowExecutionVerificationRecorder} fails (mainly because of
* violated conventions)
*/
public static HeadlessWorkflowExecutionVerificationRecorder createAndInitializeInstance(List<File> wfFiles, int parallelRuns,
int sequentialRuns) throws IOException {
HeadlessWorkflowExecutionVerification verification =
new HeadlessWorkflowExecutionVerification(wfFiles, parallelRuns, sequentialRuns);
verification.initialize();
return verification;
}
/**
* Initializes the expected behavior of workflows to execute by scanning for certain files.
*
* [wf file name without ending].failing -> indicates that the related workflow is expected to fail [wf file name without
* ending].log.expected -> contains log messages (lines) that must be contained in the workflow log [wf file name without
* ending].log.prohibited -> contains log messages (lines) that must not be contained in the workflow log
*
* @throws IOException if accessing the files above failed
*/
private void initialize() throws IOException {
for (File wfFile : wfFilesSubmitted) {
String wfFileNameWithoutEnding = wfFile.getName().replace(WorkflowConstants.WORKFLOW_FILE_ENDING, "");
if (wfFile.getParentFile().getName().equals(FAILURE_DIR_NAME)) {
wfsExpectedToFail.add(wfFile);
}
File wfExpectedLogFile = new File(wfFile.getParentFile(), wfFileNameWithoutEnding + FILE_SUFFIX_EXPECTED_LOG);
if (wfExpectedLogFile.exists()) {
wfsWithExpectedLogMessages.put(wfFile, FileUtils.readLines(wfExpectedLogFile));
}
File wfProhibitedLogFile = new File(wfFile.getParentFile(), wfFileNameWithoutEnding + FILE_SUFFIX_PROHIBITED_LOG);
if (wfProhibitedLogFile.exists()) {
wfsWithProhibitedLogMessages.put(wfFile, FileUtils.readLines(wfProhibitedLogFile));
}
}
wfsExpectedToFailCount = wfsExpectedToFail.size() * parallelRuns * sequentialRuns;
wfsExpectedToFinishCount = wfsExpectedToFinishCount - wfsExpectedToFailCount;
}
@Override
public synchronized void addWorkflowError(File wfFile, String errorMessage) {
wfsWithError.put(wfFile, errorMessage);
wfsWithErrorCount++;
resultCount++;
verifyOverall();
}
@Override
public synchronized boolean addWorkflowExecutionResult(File wfFile, File[] wfLogFiles, FinalWorkflowState finalState,
long executionDuration)
throws IOException {
if (!wfFilesSubmitted.contains(wfFile)) {
throw new IllegalArgumentException("Given workflow file unknown: " + wfFile);
}
boolean behavedAsExpected = false;
switch (finalState) {
case FINISHED:
if (!wfsExpectedToFail.contains(wfFile)) {
wfsFinishedAsExpectedCount++;
behavedAsExpected = verifyLog(wfFile, wfLogFiles, behavedAsExpected);
} else {
wfsWithUnexpectedFinalState.put(wfFile, finalState);
wfsWithUnexpectedFinalStateCount++;
}
break;
case FAILED:
if (wfsExpectedToFail.contains(wfFile)) {
wfsFailedAsExpectedCount++;
behavedAsExpected = verifyLog(wfFile, wfLogFiles, behavedAsExpected);
} else {
wfsWithUnexpectedFinalState.put(wfFile, finalState);
wfsWithUnexpectedFinalStateCount++;
}
break;
case CANCELLED:
wfsCanceled.add(wfFile);
wfsCanceledCount++;
break;
default:
throw new IllegalArgumentException("Given final workflow state unknown: " + finalState);
}
resultCount++;
verifyOverall();
wfsWithExecutionDurationSec.get(wfFile).add(executionDuration / THOUSAND);
if (behavedAsExpected) {
registerWorkflowRelatedFilesForDeletion(wfFile);
}
return behavedAsExpected;
}
private void registerWorkflowRelatedFilesForDeletion(File wfFile) {
wfRelatedFilesToDelete.add(wfFile);
File backupFile = new File(wfFile.getAbsolutePath().replace(WorkflowConstants.WORKFLOW_FILE_ENDING, "")
+ WorkflowConstants.WORKFLOW_FILE_BACKUP_SUFFIX + WorkflowConstants.WORKFLOW_FILE_ENDING);
if (backupFile.exists()) {
wfRelatedFilesToDelete.add(backupFile);
}
if (wfsWithExpectedLogMessages.containsKey(wfFile)) {
wfRelatedFilesToDelete.add(new File(wfFile.getParentFile(),
wfFile.getName().replace(WorkflowConstants.WORKFLOW_FILE_ENDING, "") + FILE_SUFFIX_EXPECTED_LOG));
}
if (wfsWithProhibitedLogMessages.containsKey(wfFile)) {
wfRelatedFilesToDelete.add(new File(wfFile.getParentFile(),
wfFile.getName().replace(WorkflowConstants.WORKFLOW_FILE_ENDING, "") + FILE_SUFFIX_PROHIBITED_LOG));
}
}
@Override
public List<File> getWorkflowRelatedFilesToDelete() {
return wfRelatedFilesToDelete;
}
private boolean verifyLog(File wfFile, File[] wfLogFiles, boolean behavedAsExpected) throws IOException {
for (File wfLogFile : wfLogFiles) {
behavedAsExpected = verifySingleLogFile(wfFile, wfLogFile);
if (!behavedAsExpected) {
break;
}
}
return behavedAsExpected;
}
private boolean verifySingleLogFile(File wfFile, File wfLogFile) throws IOException {
boolean logVerified = true;
String actualLogMessages = FileUtils.readFileToString(wfLogFile);
if (wfsWithExpectedLogMessages.containsKey(wfFile)) {
for (String expectedLogLine : wfsWithExpectedLogMessages.get(wfFile)) {
boolean missed = !actualLogMessages.contains(expectedLogLine);
if (missed) {
wfsWithUnexpectedMissingLogMessage.put(wfFile, expectedLogLine);
wfsWithUnexpectedMissingLogMessageCount++;
}
logVerified &= !missed;
}
}
if (wfsWithProhibitedLogMessages.containsKey(wfFile)) {
for (String prohibitedLogLine : wfsWithProhibitedLogMessages.get(wfFile)) {
boolean contained = actualLogMessages.contains(prohibitedLogLine);
if (contained) {
wfsWithUnexpectedProhibitedLogMessage.put(wfFile, prohibitedLogLine);
wfsWithUnexpectedProhibitedLogMessageCount++;
}
logVerified &= !contained;
}
}
verified &= logVerified;
return logVerified;
}
private boolean verifyOverall() {
if (verified) {
verified &= wfsWithError.isEmpty() && wfsCanceled.isEmpty()
&& wfsWithUnexpectedFinalState.isEmpty() && wfsWithUnexpectedMissingLogMessage.isEmpty()
&& wfsWithUnexpectedProhibitedLogMessage.isEmpty();
}
return isVerified();
}
@Override
public synchronized boolean isVerified() {
return verified && wfsExpectedToFinishCount + wfsExpectedToFailCount == resultCount;
}
@Override
public synchronized String getVerificationReport() {
StringBuilder builder = new StringBuilder();
String result;
if (isVerified()) {
result = "SUCCEEDED";
} else {
result = "FAILED";
}
builder.append("Workflow verification results\n");
builder.append(StringUtils.format(
"- Verification %s [%d workflow files, parallel: %d, sequential: %d]\n",
result, wfFilesSubmitted.size(), parallelRuns, sequentialRuns));
builder.append("Details\n");
builder.append(StringUtils.format("- Finished (as expected): %d/%d,"
+ " Failed (as expected): %d/%d, Canceled: %d/0, Error: %d/0, Unexpected final state: %d/0 [Total: %d/%d]\n",
wfsFinishedAsExpectedCount, wfsExpectedToFinishCount, wfsFailedAsExpectedCount, wfsExpectedToFailCount, wfsCanceledCount,
wfsWithErrorCount, wfsWithUnexpectedFinalStateCount, resultCount, wfsExpectedToFinishCount + wfsExpectedToFailCount));
builder.append(StringUtils.format(
"- Canceled (%d): %s\n- With error (%d): %s\n- With unexpected final state (%d): %s"
+ "\n- With missing log message (%d): %s\n- With prohibited log message (%d): %s\n",
wfsCanceledCount, convertFileListToString(wfsCanceled), wfsWithErrorCount,
convertFileMapToString(wfsWithError), wfsWithUnexpectedFinalStateCount,
convertFileMapToString(wfsWithUnexpectedFinalState), wfsWithUnexpectedMissingLogMessageCount,
convertFileMapToString(wfsWithUnexpectedMissingLogMessage), wfsWithUnexpectedProhibitedLogMessageCount,
convertFileMapToString(wfsWithUnexpectedProhibitedLogMessage)));
builder.append("Durations per workflow [sec]\n");
int sec = 0;
for (Entry<String, Object> durationEntry : reduceFilePathToFileName(wfsWithExecutionDurationSec).entrySet()) {
String duration = convertListToString((List<? extends Object>) durationEntry.getValue());
builder.append(StringUtils.format("- %s: %s\n", durationEntry.getKey(), duration));
int newValue;
try {
newValue = Integer.parseInt(duration);
} catch (NumberFormatException e){
newValue = 0;
}
sec += newValue;
}
builder.append(StringUtils.format("Durations (overall)\n"));
builder.append(StringUtils.format("- Start time: %s \n", new SimpleDateFormat(DATE_FORMAT).format(startTime)));
builder.append(StringUtils.format("- End time: %s \n", new SimpleDateFormat(DATE_FORMAT).format(endTime)));
builder.append(StringUtils.format("- Aggregated: %s sec\n", sec));
int totaltime = (int) ((endTime.getTime() - startTime.getTime()) / THOUSAND);
builder.append(StringUtils.format("- Total: %s sec\n", totaltime));
builder.append(StringUtils.format("(Workflow folder: %s)", wfFileRootDir));
return builder.toString();
}
private SortedMap<String, Object> reduceFilePathToFileName(Map<File, ? extends Object> fileMap) {
SortedMap<String, Object> fileNameMap = new TreeMap<>();
for (File file : fileMap.keySet()) {
fileNameMap.put(file.getName(), fileMap.get(file));
}
return fileNameMap;
}
private String convertFileMapToString(Map<File, ? extends Object> fileMap) {
if (fileMap.isEmpty()) {
return EMPTY_COLLECTION_STRING;
} else {
return reduceFilePathToFileName(fileMap).toString().replaceAll("\\{", "").replaceAll("\\}", "").trim();
}
}
private SortedSet<String> reduceFilePathToFileName(List<File> fileList) {
SortedSet<String> fileNameSet = new TreeSet<>();
for (File file : fileList) {
fileNameSet.add(file.getName());
}
return fileNameSet;
}
private String convertFileListToString(List<File> fileList) {
return convertSetToString(reduceFilePathToFileName(fileList));
}
private String convertListToString(List<? extends Object> list) {
if (list.isEmpty()) {
return EMPTY_COLLECTION_STRING;
} else {
return list.toString().replaceAll("\\[", "").replaceAll("\\]", "").trim();
}
}
private String convertSetToString(Set<? extends Object> set) {
if (set.isEmpty()) {
return EMPTY_COLLECTION_STRING;
} else {
return set.toString().replaceAll("\\[", "").replaceAll("\\]", "").trim();
}
}
@Override
public void setStartAndEndTime(Date start, Date end) {
this.startTime = start;
this.endTime = end;
}
}