/*
* The MIT License
*
* Copyright (c) 2010-2016 Bruno P. Kinoshita
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.tap4j.plugin;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.tap4j.model.Plan;
import org.tap4j.model.TestResult;
import org.tap4j.model.TestSet;
import org.tap4j.parser.ParserException;
import org.tap4j.parser.Tap13Parser;
import org.tap4j.plugin.model.ParseErrorTestSetMap;
import org.tap4j.plugin.model.TestSetMap;
import org.tap4j.util.DirectiveValues;
import org.tap4j.util.StatusValues;
import hudson.FilePath;
import hudson.model.Run;
/**
* Executes remote TAP Stream retrieval and execution.
*
* @author Bruno P. Kinoshita - http://www.kinoshita.eti.br
* @since 1.1
*/
public class TapParser {
/** Prints the logs to the web server's console / log files */
private static final Logger log = Logger.getLogger(TapParser.class.getName());
private final Boolean outputTapToConsole;
private final Boolean enableSubtests;
private final Boolean todoIsFailure;
/** Build's logger to print logs as part of build's console output */
private final PrintStream logger;
private final Boolean includeCommentDiagnostics;
private final Boolean validateNumberOfTests;
private final Boolean planRequired;
private final Boolean verbose;
private final Boolean stripSingleParents;
private final Boolean flattenTheTap;
private boolean hasFailedTests;
private boolean parserErrors;
public TapParser(Boolean outputTapToConsole, Boolean enableSubtests, Boolean todoIsFailure,
Boolean includeCommentDiagnostics, Boolean validateNumberOfTests, Boolean planRequired, Boolean verbose,
Boolean stripSingleParents, Boolean flattenTheTap,
PrintStream logger) {
this.outputTapToConsole = outputTapToConsole;
this.enableSubtests = enableSubtests;
this.todoIsFailure = todoIsFailure;
this.parserErrors = false;
this.includeCommentDiagnostics = includeCommentDiagnostics;
this.validateNumberOfTests = validateNumberOfTests;
this.planRequired = planRequired;
this.verbose = verbose;
this.stripSingleParents = stripSingleParents;
this.flattenTheTap = flattenTheTap;
this.logger = logger;
}
public Boolean hasParserErrors() {
return this.parserErrors;
}
public Boolean getOutputTapToConsole() {
return outputTapToConsole;
}
public Boolean getTodoIsFailure() {
return todoIsFailure;
}
public boolean getParserErrors() {
return parserErrors;
}
public boolean getStripSingleParents() {
return stripSingleParents;
}
public Boolean getIncludeCommentDiagnostics() {
return includeCommentDiagnostics;
}
public Boolean getValidateNumberOfTests() {
return validateNumberOfTests;
}
public Boolean getPlanRequired() {
return planRequired;
}
public Boolean getEnableSubtests() {
return enableSubtests;
}
public boolean hasFailedTests() {
return this.hasFailedTests;
}
public Boolean getVerbose() {
return verbose;
}
public boolean getFlattenTheTap() {
return flattenTheTap;
}
private boolean containsNotOk(TestSet testSet) {
for (TestResult testResult : testSet.getTestResults()) {
if (testResult.getStatus().equals(StatusValues.NOT_OK) && !(testResult.getDirective() != null
&& DirectiveValues.SKIP == testResult.getDirective().getDirectiveValue())) {
return true;
}
}
return false;
}
public TapResult parse(FilePath[] results, Run build) {
this.parserErrors = Boolean.FALSE;
this.hasFailedTests = Boolean.FALSE;
final List<TestSetMap> testSets = new LinkedList<TestSetMap>();
if (null == results) {
log("File paths not specified. paths var is null. Returning empty test results.");
} else {
for (FilePath path : results) {
File tapFile = new File(path.getRemote());
if (!tapFile.isFile()) {
log("'" + tapFile.getAbsolutePath() + "' points to an invalid test report");
continue; // move to next file
} else {
log("Processing '" + tapFile.getAbsolutePath() + "'");
}
try {
log("Parsing TAP test result [" + tapFile + "].");
final Tap13Parser parser = new Tap13Parser("UTF-8", enableSubtests, planRequired);
final TestSet testSet = flattenTheSetAsRequired(stripSingleParentsAsRequired(parser.parseFile(tapFile)));
if (containsNotOk(testSet) || testSet.containsBailOut()) {
this.hasFailedTests = Boolean.TRUE;
}
final TestSetMap map = new TestSetMap(tapFile.getAbsolutePath(), testSet);
testSets.add(map);
if (this.outputTapToConsole) {
try {
log(FileUtils.readFileToString(tapFile));
} catch (RuntimeException re) {
log(re);
} catch (IOException e) {
log(e);
}
}
} catch (ParserException pe) {
testSets.add(new ParseErrorTestSetMap(tapFile.getAbsolutePath(), pe));
this.parserErrors = Boolean.TRUE;
log(pe);
}
}
}
// final TapResult testResult = new
// TapResult(UUID.randomUUID().toString(), build, testSets);
final TapResult testResult = new TapResult("TAP Test Results", build, testSets, this.todoIsFailure,
this.includeCommentDiagnostics, this.validateNumberOfTests);
return testResult;
}
private TestSet stripSingleParentsAsRequired(TestSet originalSet) {
if (!stripSingleParents) {
return originalSet;
} else {
TestSet result = originalSet;
while (hasSingleParent(result)) {
result = result.getTestResults().get(0).getSubtest();
}
return result;
}
}
private TestSet flattenTheSetAsRequired(TestSet originalSet) {
if (!flattenTheTap) {
return originalSet;
} else {
TestSet result = new TestSet();
final List<TestResult> resultsToProcess = originalSet.getTestResults();
int testIndex = 1;
while (!resultsToProcess.isEmpty()) {
final TestResult actualTestResult = resultsToProcess.remove(0);
TestSet subtests = actualTestResult.getSubtest();
if (subtests == null || subtests.getNumberOfTestResults() == 0) {
actualTestResult.setTestNumber(testIndex++);
result.addTestResult(actualTestResult);
} else {
final List<TestResult> subtestResults = subtests.getTestResults();
for (TestResult subtestResult : subtestResults) {
subtestResult.setDescription(actualTestResult.getDescription() + subtestResult.getDescription());
resultsToProcess.add(subtestResult);
}
final Plan subtestPlan = subtests.getPlan();
final boolean planIsPresent = subtestPlan != null;
final int subtestCountAsPlanned = planIsPresent ?
subtestPlan.getLastTestNumber() - subtestPlan.getInitialTestNumber() + 1
: -1;
final boolean subtestCountDiffersFromPlan = planIsPresent && subtestCountAsPlanned != subtestResults.size();
if (subtestCountDiffersFromPlan) {
final int missingTestCount =
subtestCountAsPlanned - subtestResults.size();
final TestResult timeoutTestResult = new TestResult();
timeoutTestResult.setStatus(StatusValues.NOT_OK);
timeoutTestResult.setDescription(
String.format("%s %s %d %s",
actualTestResult.getDescription(), "failed:", missingTestCount, "subtest(s) missing"));
resultsToProcess.add(timeoutTestResult);
}
}
}
return result;
}
}
private boolean hasSingleParent(TestSet testSet) {
if (testSet == null) {
return false;
}
if (testSet.getNumberOfTestResults() != 1) {
return false; // not a single test result
}
int planSpan = testSet.getPlan() != null ? (testSet.getPlan().getLastTestNumber() - testSet.getPlan().getInitialTestNumber()) : 0;
if (planSpan == 0) { // exactly one test
return testSet.getTestResults().get(0).getSubtest() != null; // which has a child(ern)
} else {
return false;
}
}
private void log(String str) {
if (verbose && logger != null) {
logger.println(str);
} else {
log.fine(str);
}
}
private void log(Exception ex) {
if (logger != null) {
ex.printStackTrace(logger);
} else {
log.severe(ex.toString());
}
}
}