/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.testframework;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDException;
import net.sourceforge.pmd.PropertyDescriptor;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleSetFactory;
import net.sourceforge.pmd.RuleSetNotFoundException;
import net.sourceforge.pmd.RuleSets;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.renderers.TextRenderer;
/**
* Advanced methods for test cases
*/
public abstract class RuleTst {
/**
* Find a rule in a certain ruleset by name
*/
public Rule findRule(String ruleSet, String ruleName) {
try {
Rule rule = new RuleSetFactory().createRuleSets(ruleSet).getRuleByName(ruleName);
if (rule == null) {
fail("Rule " + ruleName + " not found in ruleset " + ruleSet);
} else {
rule.setRuleSetName(ruleSet);
}
return rule;
} catch (RuleSetNotFoundException e) {
e.printStackTrace();
fail("Couldn't find ruleset " + ruleSet);
return null;
}
}
/**
* Run the rule on the given code, and check the expected number of
* violations.
*/
@SuppressWarnings("unchecked")
public void runTest(TestDescriptor test) {
Rule rule = test.getRule();
if (test.getReinitializeRule()) {
rule = findRule(rule.getRuleSetName(), rule.getName());
}
Map<PropertyDescriptor<?>, Object> oldProperties = rule.getPropertiesByPropertyDescriptor();
try {
int res;
Report report;
try {
// Set test specific properties onto the Rule
if (test.getProperties() != null) {
for (Map.Entry<Object, Object> entry : test.getProperties().entrySet()) {
String propertyName = (String) entry.getKey();
PropertyDescriptor propertyDescriptor = rule.getPropertyDescriptor(propertyName);
if (propertyDescriptor == null) {
throw new IllegalArgumentException(
"No such property '" + propertyName + "' on Rule " + rule.getName());
}
Object value = propertyDescriptor.valueFrom((String) entry.getValue());
rule.setProperty(propertyDescriptor, value);
}
}
report = processUsingStringReader(test, rule);
res = report.size();
} catch (Throwable t) {
t.printStackTrace();
throw new RuntimeException('"' + test.getDescription() + "\" failed", t);
}
if (test.getNumberOfProblemsExpected() != res) {
printReport(test, report);
}
assertEquals('"' + test.getDescription() + "\" resulted in wrong number of failures,",
test.getNumberOfProblemsExpected(), res);
assertMessages(report, test);
assertLineNumbers(report, test);
} finally {
// Restore old properties
for (Map.Entry<PropertyDescriptor<?>, Object> entry : oldProperties.entrySet()) {
rule.setProperty((PropertyDescriptor) entry.getKey(), entry.getValue());
}
}
}
private void assertMessages(Report report, TestDescriptor test) {
if (report == null || test.getExpectedMessages().isEmpty()) {
return;
}
List<String> expectedMessages = test.getExpectedMessages();
if (report.size() != expectedMessages.size()) {
throw new RuntimeException("Test setup error: number of expected messages doesn't match "
+ "number of violations for test case '" + test.getDescription() + "'");
}
Iterator<RuleViolation> it = report.iterator();
int index = 0;
while (it.hasNext()) {
RuleViolation violation = it.next();
String actual = violation.getDescription();
if (!expectedMessages.get(index).equals(actual)) {
printReport(test, report);
}
assertEquals(
'"' + test.getDescription() + "\" produced wrong message on violation number " + (index + 1) + ".",
expectedMessages.get(index), actual);
index++;
}
}
private void assertLineNumbers(Report report, TestDescriptor test) {
if (report == null || test.getExpectedLineNumbers().isEmpty()) {
return;
}
List<Integer> expected = test.getExpectedLineNumbers();
if (report.getViolationTree().size() != expected.size()) {
throw new RuntimeException("Test setup error: number of execpted line numbers doesn't match "
+ "number of violations for test case '" + test.getDescription() + "'");
}
Iterator<RuleViolation> it = report.getViolationTree().iterator();
int index = 0;
while (it.hasNext()) {
RuleViolation violation = it.next();
Integer actual = violation.getBeginLine();
if (expected.get(index) != actual.intValue()) {
printReport(test, report);
}
assertEquals('"' + test.getDescription() + "\" violation on wrong line number: violation number "
+ (index + 1) + ".", expected.get(index), actual);
index++;
}
}
private void printReport(TestDescriptor test, Report report) {
System.out.println("--------------------------------------------------------------");
System.out.println("Test Failure: " + test.getDescription());
System.out.println(" -> Expected " + test.getNumberOfProblemsExpected() + " problem(s), " + report.size()
+ " problem(s) found.");
System.out.println(" -> Expected messages: " + test.getExpectedMessages());
System.out.println(" -> Expected line numbers: " + test.getExpectedLineNumbers());
System.out.println();
TextRenderer renderer = new TextRenderer();
renderer.setWriter(new StringWriter());
try {
renderer.start();
renderer.renderFileReport(report);
renderer.end();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println(renderer.getWriter().toString());
System.out.println("--------------------------------------------------------------");
}
private Report processUsingStringReader(TestDescriptor test, Rule rule) throws PMDException {
Report report = new Report();
runTestFromString(test, rule, report);
return report;
}
/**
* Run the rule on the given code and put the violations in the report.
*/
public void runTestFromString(String code, Rule rule, Report report, LanguageVersion languageVersion) {
runTestFromString(code, rule, report, languageVersion, true);
}
public void runTestFromString(String code, Rule rule, Report report, LanguageVersion languageVersion,
boolean isUseAuxClasspath) {
try {
PMD p = new PMD();
p.getConfiguration().setDefaultLanguageVersion(languageVersion);
if (isUseAuxClasspath) {
// configure the "auxclasspath" option for unit testing
p.getConfiguration().prependClasspath(".");
}
RuleContext ctx = new RuleContext();
ctx.setReport(report);
ctx.setSourceCodeFilename("n/a");
ctx.setLanguageVersion(languageVersion);
ctx.setIgnoreExceptions(false);
RuleSet rules = new RuleSetFactory().createSingleRuleRuleSet(rule);
p.getSourceCodeProcessor().processSourceCode(new StringReader(code), new RuleSets(rules), ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void runTestFromString(TestDescriptor test, Rule rule, Report report) {
runTestFromString(test.getCode(), rule, report, test.getLanguageVersion(), test.isUseAuxClasspath());
}
/**
* getResourceAsStream tries to find the XML file in weird locations if the
* ruleName includes the package, so we strip it here.
*/
protected String getCleanRuleName(Rule rule) {
String fullClassName = rule.getClass().getName();
if (fullClassName.equals(rule.getName())) {
// We got the full class name, so we'll use the stripped name
// instead
String packageName = rule.getClass().getPackage().getName();
return fullClassName.substring(packageName.length() + 1);
} else {
return rule.getName(); // Test is using findRule, smart!
}
}
/**
* Extract a set of tests from an XML file. The file should be
* ./xml/RuleName.xml relative to the test class. The format is defined in
* test-data.xsd.
*/
public TestDescriptor[] extractTestsFromXml(Rule rule) {
String testsFileName = getCleanRuleName(rule);
return extractTestsFromXml(rule, testsFileName);
}
public TestDescriptor[] extractTestsFromXml(Rule rule, String testsFileName) {
return extractTestsFromXml(rule, testsFileName, "xml/");
}
/**
* Extract a set of tests from an XML file with the given name. The file
* should be ./xml/[testsFileName].xml relative to the test class. The
* format is defined in test-data.xsd.
*/
public TestDescriptor[] extractTestsFromXml(Rule rule, String testsFileName, String baseDirectory) {
String testXmlFileName = baseDirectory + testsFileName + ".xml";
InputStream inputStream = getClass().getResourceAsStream(testXmlFileName);
if (inputStream == null) {
throw new RuntimeException("Couldn't find " + testXmlFileName);
}
Document doc;
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
doc = builder.parse(inputStream);
} catch (ParserConfigurationException pce) {
pce.printStackTrace();
throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + pce.getMessage());
} catch (FactoryConfigurationError fce) {
fce.printStackTrace();
throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + fce.getMessage());
} catch (IOException ioe) {
ioe.printStackTrace();
throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + ioe.getMessage());
} catch (SAXException se) {
se.printStackTrace();
throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + se.getMessage());
}
return parseTests(rule, doc);
}
private TestDescriptor[] parseTests(Rule rule, Document doc) {
Element root = doc.getDocumentElement();
NodeList testCodes = root.getElementsByTagName("test-code");
TestDescriptor[] tests = new TestDescriptor[testCodes.getLength()];
for (int i = 0; i < testCodes.getLength(); i++) {
Element testCode = (Element) testCodes.item(i);
boolean reinitializeRule = true;
Node reinitializeRuleAttribute = testCode.getAttributes().getNamedItem("reinitializeRule");
if (reinitializeRuleAttribute != null) {
String reinitializeRuleValue = reinitializeRuleAttribute.getNodeValue();
if ("false".equalsIgnoreCase(reinitializeRuleValue) || "0".equalsIgnoreCase(reinitializeRuleValue)) {
reinitializeRule = false;
}
}
boolean isRegressionTest = true;
Node regressionTestAttribute = testCode.getAttributes().getNamedItem("regressionTest");
if (regressionTestAttribute != null) {
String reinitializeRuleValue = regressionTestAttribute.getNodeValue();
if ("false".equalsIgnoreCase(reinitializeRuleValue)) {
isRegressionTest = false;
}
}
boolean isUseAuxClasspath = true;
Node useAuxClasspathAttribute = testCode.getAttributes().getNamedItem("useAuxClasspath");
if (useAuxClasspathAttribute != null) {
String useAuxClasspathValue = useAuxClasspathAttribute.getNodeValue();
if ("false".equalsIgnoreCase(useAuxClasspathValue)) {
isUseAuxClasspath = false;
}
}
NodeList ruleProperties = testCode.getElementsByTagName("rule-property");
Properties properties = new Properties();
for (int j = 0; j < ruleProperties.getLength(); j++) {
Node ruleProperty = ruleProperties.item(j);
String propertyName = ruleProperty.getAttributes().getNamedItem("name").getNodeValue();
properties.setProperty(propertyName, parseTextNode(ruleProperty));
}
NodeList expectedMessagesNodes = testCode.getElementsByTagName("expected-messages");
List<String> messages = new ArrayList<>();
if (expectedMessagesNodes != null && expectedMessagesNodes.getLength() > 0) {
Element item = (Element) expectedMessagesNodes.item(0);
NodeList messagesNodes = item.getElementsByTagName("message");
for (int j = 0; j < messagesNodes.getLength(); j++) {
messages.add(parseTextNode(messagesNodes.item(j)));
}
}
NodeList expectedLineNumbersNodes = testCode.getElementsByTagName("expected-linenumbers");
List<Integer> expectedLineNumbers = new ArrayList<>();
if (expectedLineNumbersNodes != null && expectedLineNumbersNodes.getLength() > 0) {
Element item = (Element) expectedLineNumbersNodes.item(0);
String numbers = item.getTextContent();
for (String n : numbers.split(" *, *")) {
expectedLineNumbers.add(Integer.valueOf(n));
}
}
String code = getNodeValue(testCode, "code", false);
if (code == null) {
// Should have a coderef
NodeList coderefs = testCode.getElementsByTagName("code-ref");
if (coderefs.getLength() == 0) {
throw new RuntimeException(
"Required tag is missing from the test-xml. Supply either a code or a code-ref tag");
}
Node coderef = coderefs.item(0);
String referenceId = coderef.getAttributes().getNamedItem("id").getNodeValue();
NodeList codeFragments = root.getElementsByTagName("code-fragment");
for (int j = 0; j < codeFragments.getLength(); j++) {
String fragmentId = codeFragments.item(j).getAttributes().getNamedItem("id").getNodeValue();
if (referenceId.equals(fragmentId)) {
code = parseTextNode(codeFragments.item(j));
}
}
if (code == null) {
throw new RuntimeException("No matching code fragment found for coderef");
}
}
String description = getNodeValue(testCode, "description", true);
int expectedProblems = Integer.parseInt(getNodeValue(testCode, "expected-problems", true));
String languageVersionString = getNodeValue(testCode, "source-type", false);
if (languageVersionString == null) {
tests[i] = new TestDescriptor(code, description, expectedProblems, rule);
} else {
LanguageVersion languageVersion = LanguageRegistry
.findLanguageVersionByTerseName(languageVersionString);
if (languageVersion != null) {
tests[i] = new TestDescriptor(code, description, expectedProblems, rule, languageVersion);
} else {
throw new RuntimeException("Unknown LanguageVersion for test: " + languageVersionString);
}
}
tests[i].setReinitializeRule(reinitializeRule);
tests[i].setRegressionTest(isRegressionTest);
tests[i].setUseAuxClasspath(isUseAuxClasspath);
tests[i].setExpectedMessages(messages);
tests[i].setExpectedLineNumbers(expectedLineNumbers);
tests[i].setProperties(properties);
tests[i].setNumberInDocument(i);
}
return tests;
}
private String getNodeValue(Element parentElm, String nodeName, boolean required) {
NodeList nodes = parentElm.getElementsByTagName(nodeName);
if (nodes == null || nodes.getLength() == 0) {
if (required) {
throw new RuntimeException("Required tag is missing from the test-xml: " + nodeName);
} else {
return null;
}
}
Node node = nodes.item(0);
return parseTextNode(node);
}
private static String parseTextNode(Node exampleNode) {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < exampleNode.getChildNodes().getLength(); i++) {
Node node = exampleNode.getChildNodes().item(i);
if (node.getNodeType() == Node.CDATA_SECTION_NODE || node.getNodeType() == Node.TEXT_NODE) {
buffer.append(node.getNodeValue());
}
}
return buffer.toString().trim();
}
}