View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.testframework;
5   
6   import static org.junit.Assert.assertEquals;
7   import static org.junit.Assert.fail;
8   
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.io.StringReader;
12  import java.io.StringWriter;
13  import java.util.ArrayList;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Properties;
18  
19  import javax.xml.parsers.DocumentBuilder;
20  import javax.xml.parsers.DocumentBuilderFactory;
21  import javax.xml.parsers.FactoryConfigurationError;
22  import javax.xml.parsers.ParserConfigurationException;
23  
24  import net.sourceforge.pmd.PMD;
25  import net.sourceforge.pmd.PMDException;
26  import net.sourceforge.pmd.PropertyDescriptor;
27  import net.sourceforge.pmd.Report;
28  import net.sourceforge.pmd.Rule;
29  import net.sourceforge.pmd.RuleContext;
30  import net.sourceforge.pmd.RuleSet;
31  import net.sourceforge.pmd.RuleSetFactory;
32  import net.sourceforge.pmd.RuleSetNotFoundException;
33  import net.sourceforge.pmd.RuleSets;
34  import net.sourceforge.pmd.RuleViolation;
35  import net.sourceforge.pmd.lang.LanguageRegistry;
36  import net.sourceforge.pmd.lang.LanguageVersion;
37  import net.sourceforge.pmd.renderers.TextRenderer;
38  
39  import org.w3c.dom.Document;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  import org.xml.sax.SAXException;
44  /**
45   * Advanced methods for test cases
46   */
47  public abstract class RuleTst {
48      /**
49       * Find a rule in a certain ruleset by name
50       */
51      public Rule findRule(String ruleSet, String ruleName) {
52          try {
53              Rule rule = new RuleSetFactory().createRuleSets(ruleSet).getRuleByName(ruleName);
54              if (rule == null) {
55                  fail("Rule " + ruleName + " not found in ruleset " + ruleSet);
56              } else {
57                  rule.setRuleSetName(ruleSet);
58              }
59              return rule;
60          } catch (RuleSetNotFoundException e) {
61              e.printStackTrace();
62              fail("Couldn't find ruleset " + ruleSet);
63              return null;
64          }
65      }
66  
67  
68      /**
69       * Run the rule on the given code, and check the expected number of violations.
70       */
71      @SuppressWarnings("unchecked")
72      public void runTest(TestDescriptor test) {
73          Rule rule = test.getRule();
74  
75          if (test.getReinitializeRule()) {
76              rule = findRule(rule.getRuleSetName(), rule.getName());
77          }
78  
79          Map<PropertyDescriptor<?>, Object> oldProperties = rule.getPropertiesByPropertyDescriptor();
80          try {
81              int res;
82              Report report;
83              try {
84          	// Set test specific properties onto the Rule
85                  if (test.getProperties() != null) {
86                      for (Map.Entry<Object, Object> entry : test.getProperties().entrySet()) {
87                  	String propertyName = (String)entry.getKey();
88                  	PropertyDescriptor propertyDescriptor = rule.getPropertyDescriptor(propertyName);
89                  	if (propertyDescriptor == null) {
90                              throw new IllegalArgumentException("No such property '" + propertyName + "' on Rule " + rule.getName());
91                  	}
92  
93                  	Object value = propertyDescriptor.valueFrom((String)entry.getValue());
94                  	rule.setProperty(propertyDescriptor, value);
95                      }
96                  }
97  
98                  report = processUsingStringReader(test, rule);
99                  res = report.size();
100             } catch (Throwable t) {
101                 t.printStackTrace();
102                 throw new RuntimeException('"' + test.getDescription() + "\" failed", t);
103             }
104             if (test.getNumberOfProblemsExpected() != res) {
105                 printReport(test, report);
106             }
107             assertEquals('"' + test.getDescription() + "\" resulted in wrong number of failures,",
108                     test.getNumberOfProblemsExpected(), res);
109             assertMessages(report, test);
110             assertLineNumbers(report, test);
111         } finally {
112             //Restore old properties
113             for (Map.Entry<PropertyDescriptor<?>, Object> entry: oldProperties.entrySet()) {
114         	rule.setProperty((PropertyDescriptor)entry.getKey(), entry.getValue());
115             }
116         }
117     }
118 
119     private void assertMessages(Report report, TestDescriptor test) {
120         if (report == null || test.getExpectedMessages().isEmpty()) {
121             return;
122         }
123 
124         List<String> expectedMessages = test.getExpectedMessages();
125         if (report.size() != expectedMessages.size()) {
126             throw new RuntimeException("Test setup error: number of expected messages doesn't match "
127                     + "number of violations for test case '" + test.getDescription() + "'");
128         }
129 
130         Iterator<RuleViolation> it = report.iterator();
131         int index = 0;
132         while (it.hasNext()) {
133             RuleViolation violation = it.next();
134             String actual = violation.getDescription();
135             if (!expectedMessages.get(index).equals(actual)) {
136                 printReport(test, report);
137             }
138             assertEquals(
139                     '"' + test.getDescription() + "\" produced wrong message on violation number " + (index + 1) + ".",
140                     expectedMessages.get(index), actual);
141             index++;
142         }
143     }
144 
145     private void assertLineNumbers(Report report, TestDescriptor test) {
146         if (report == null || test.getExpectedLineNumbers().isEmpty()) {
147             return;
148         }
149 
150         List<Integer> expected = test.getExpectedLineNumbers();
151         if (report.getViolationTree().size() != expected.size()) {
152             throw new RuntimeException("Test setup error: number of execpted line numbers doesn't match "
153                     + "number of violations for test case '" + test.getDescription() + "'");
154         }
155 
156         Iterator<RuleViolation> it = report.getViolationTree().iterator();
157         int index = 0;
158         while (it.hasNext()) {
159             RuleViolation violation = it.next();
160             Integer actual = violation.getBeginLine();
161             if (expected.get(index) != actual.intValue()) {
162                 printReport(test, report);
163             }
164             assertEquals(
165                     '"' + test.getDescription() + "\" violation on wrong line number: violation number " + (index + 1) + ".",
166                     expected.get(index), actual);
167             index++;
168         }
169     }
170 
171     private void printReport(TestDescriptor test, Report report) {
172         System.out.println("--------------------------------------------------------------");
173         System.out.println("Test Failure: " + test.getDescription());
174         System.out.println(" -> Expected " + test.getNumberOfProblemsExpected() + " problem(s), "
175                 + report.size() + " problem(s) found.");
176         System.out.println(" -> Expected messages: " + test.getExpectedMessages());
177         System.out.println(" -> Expected line numbers: " + test.getExpectedLineNumbers());
178         System.out.println();
179         TextRenderer renderer = new TextRenderer();
180         renderer.setWriter(new StringWriter());
181         try {
182             renderer.start();
183             renderer.renderFileReport(report);
184             renderer.end();
185         } catch (IOException e) {
186             throw new RuntimeException(e);
187         }
188         System.out.println(renderer.getWriter().toString());
189         System.out.println("--------------------------------------------------------------");
190     }
191 
192     private Report processUsingStringReader(TestDescriptor test, Rule rule) throws PMDException {
193         Report report = new Report();
194         runTestFromString(test, rule, report);
195         return report;
196     }
197 
198     /**
199      * Run the rule on the given code and put the violations in the report.
200      */
201     public void runTestFromString(String code, Rule rule, Report report, LanguageVersion languageVersion) {
202         runTestFromString(code, rule, report, languageVersion, true);
203     }
204 
205     public void runTestFromString(String code, Rule rule, Report report, LanguageVersion languageVersion, boolean isUseAuxClasspath) {
206         try {
207             PMD p = new PMD();
208             p.getConfiguration().setDefaultLanguageVersion(languageVersion);
209             if (isUseAuxClasspath) {
210                 p.getConfiguration().prependClasspath("."); // configure the "auxclasspath" option for unit testing
211             }
212             RuleContext ctx = new RuleContext();
213             ctx.setReport(report);
214             ctx.setSourceCodeFilename("n/a");
215             ctx.setLanguageVersion(languageVersion);
216             ctx.setIgnoreExceptions(false);
217             RuleSet rules = new RuleSet();
218             rules.addRule(rule);
219             p.getSourceCodeProcessor().processSourceCode(new StringReader(code), new RuleSets(rules), ctx);
220         } catch (Exception e) {
221             throw new RuntimeException(e);
222         }
223     }
224 
225     public void runTestFromString(TestDescriptor test, Rule rule, Report report) {
226         runTestFromString(test.getCode(), rule, report, test.getLanguageVersion(), test.isUseAuxClasspath());
227     }
228 
229     /**
230      * getResourceAsStream tries to find the XML file in weird locations if the
231      * ruleName includes the package, so we strip it here.
232      */
233     protected String getCleanRuleName(Rule rule) {
234         String fullClassName = rule.getClass().getName();
235         if (fullClassName.equals(rule.getName())) {
236             //We got the full class name, so we'll use the stripped name instead
237             String packageName = rule.getClass().getPackage().getName();
238             return fullClassName.substring(packageName.length()+1);
239         } else {
240             return rule.getName();  //Test is using findRule, smart!
241         }
242     }
243 
244     /**
245      * Extract a set of tests from an XML file. The file should be
246      * ./xml/RuleName.xml relative to the test class. The format is defined in
247      * test-data.xsd.
248      */
249     public TestDescriptor[] extractTestsFromXml(Rule rule) {
250         String testsFileName = getCleanRuleName(rule);
251 
252         return extractTestsFromXml(rule, testsFileName);
253     }
254 
255     public TestDescriptor[] extractTestsFromXml(Rule rule, String testsFileName) {
256         return extractTestsFromXml(rule, testsFileName, "xml/");
257     }
258     /**
259      * Extract a set of tests from an XML file with the given name. The file should be
260      * ./xml/[testsFileName].xml relative to the test class. The format is defined in
261      * test-data.xsd.
262      */
263     public TestDescriptor[] extractTestsFromXml(Rule rule, String testsFileName, String baseDirectory) {
264         String testXmlFileName = baseDirectory + testsFileName + ".xml";
265         InputStream inputStream = getClass().getResourceAsStream(testXmlFileName);
266         if (inputStream == null) {
267             throw new RuntimeException("Couldn't find " + testXmlFileName);
268         }
269 
270         Document doc;
271         try {
272             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
273             doc = builder.parse(inputStream);
274         } catch (ParserConfigurationException pce) {
275             pce.printStackTrace();
276             throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + pce.getMessage());
277         } catch (FactoryConfigurationError fce) {
278             fce.printStackTrace();
279             throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + fce.getMessage());
280         } catch (IOException ioe) {
281             ioe.printStackTrace();
282             throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + ioe.getMessage());
283         } catch (SAXException se) {
284             se.printStackTrace();
285             throw new RuntimeException("Couldn't parse " + testXmlFileName + ", due to: " + se.getMessage());
286         }
287 
288         return parseTests(rule, doc);
289     }
290 
291     private TestDescriptor[] parseTests(Rule rule, Document doc) {
292         Element root = doc.getDocumentElement();
293         NodeList testCodes = root.getElementsByTagName("test-code");
294 
295         TestDescriptor[] tests = new TestDescriptor[testCodes.getLength()];
296         for (int i = 0; i < testCodes.getLength(); i++) {
297             Element testCode = (Element)testCodes.item(i);
298 
299             boolean reinitializeRule = true;
300             Node reinitializeRuleAttribute = testCode.getAttributes().getNamedItem("reinitializeRule");
301             if (reinitializeRuleAttribute != null) {
302                 String reinitializeRuleValue = reinitializeRuleAttribute.getNodeValue();
303                 if ("false".equalsIgnoreCase(reinitializeRuleValue) ||
304                         "0".equalsIgnoreCase(reinitializeRuleValue)) {
305                     reinitializeRule = false;
306                 }
307             }
308 
309             boolean isRegressionTest = true;
310             Node regressionTestAttribute = testCode.getAttributes().getNamedItem("regressionTest");
311             if (regressionTestAttribute != null) {
312                 String reinitializeRuleValue = regressionTestAttribute.getNodeValue();
313                 if ("false".equalsIgnoreCase(reinitializeRuleValue)) {
314                     isRegressionTest = false;
315                 }
316             }
317 
318             boolean isUseAuxClasspath = true;
319             Node useAuxClasspathAttribute = testCode.getAttributes().getNamedItem("useAuxClasspath");
320             if (useAuxClasspathAttribute != null) {
321                 String useAuxClasspathValue = useAuxClasspathAttribute.getNodeValue();
322                 if ("false".equalsIgnoreCase(useAuxClasspathValue)) {
323                     isUseAuxClasspath = false;
324                 }
325             }
326 
327             NodeList ruleProperties = testCode.getElementsByTagName("rule-property");
328             Properties properties = new Properties();
329             for (int j = 0; j < ruleProperties.getLength(); j++) {
330                 Node ruleProperty = ruleProperties.item(j);
331                 String propertyName = ruleProperty.getAttributes().getNamedItem("name").getNodeValue();
332                 properties.setProperty(propertyName, parseTextNode(ruleProperty));
333             }
334 
335             NodeList expectedMessagesNodes = testCode.getElementsByTagName("expected-messages");
336             List<String> messages = new ArrayList<>();
337             if (expectedMessagesNodes != null && expectedMessagesNodes.getLength() > 0) {
338                 Element item = (Element)expectedMessagesNodes.item(0);
339                 NodeList messagesNodes = item.getElementsByTagName("message");
340                 for (int j = 0; j < messagesNodes.getLength(); j++) {
341                     messages.add(parseTextNode(messagesNodes.item(j)));
342                 }
343             }
344 
345             NodeList expectedLineNumbersNodes = testCode.getElementsByTagName("expected-linenumbers");
346             List<Integer> expectedLineNumbers = new ArrayList<>();
347             if (expectedLineNumbersNodes != null && expectedLineNumbersNodes.getLength() > 0) {
348                 Element item = (Element)expectedLineNumbersNodes.item(0);
349                 String numbers = item.getTextContent();
350                 for (String n : numbers.split(" *, *")) {
351                     expectedLineNumbers.add(Integer.valueOf(n));
352                 }
353             }
354 
355             String code = getNodeValue(testCode, "code", false);
356             if (code == null) {
357                 //Should have a coderef
358                 NodeList coderefs = testCode.getElementsByTagName("code-ref");
359                 if (coderefs.getLength()==0) {
360                     throw new RuntimeException("Required tag is missing from the test-xml. Supply either a code or a code-ref tag");
361                 }
362                 Node coderef = coderefs.item(0);
363                 String referenceId = coderef.getAttributes().getNamedItem("id").getNodeValue();
364                 NodeList codeFragments = root.getElementsByTagName("code-fragment");
365                 for (int j = 0; j < codeFragments.getLength(); j++) {
366                     String fragmentId = codeFragments.item(j).getAttributes().getNamedItem("id").getNodeValue();
367                     if (referenceId.equals(fragmentId)) {
368                         code = parseTextNode(codeFragments.item(j));
369                     }
370                 }
371 
372                 if (code==null) {
373                     throw new RuntimeException("No matching code fragment found for coderef");
374                 }
375             }
376 
377             String description = getNodeValue(testCode, "description", true);
378             int expectedProblems = Integer.parseInt(getNodeValue(testCode, "expected-problems", true));
379 
380             String languageVersionString = getNodeValue(testCode, "source-type", false);
381             if (languageVersionString == null) {
382                 tests[i] = new TestDescriptor(code, description, expectedProblems, rule);
383             } else {
384             	LanguageVersion languageVersion = LanguageRegistry.findLanguageVersionByTerseName(languageVersionString);
385                 if (languageVersion != null) {
386                     tests[i] = new TestDescriptor(code, description, expectedProblems, rule, languageVersion);
387                 } else {
388                     throw new RuntimeException("Unknown LanguageVersion for test: " + languageVersionString);
389                 }
390             }
391             tests[i].setReinitializeRule(reinitializeRule);
392             tests[i].setRegressionTest(isRegressionTest);
393             tests[i].setUseAuxClasspath(isUseAuxClasspath);
394             tests[i].setExpectedMessages(messages);
395             tests[i].setExpectedLineNumbers(expectedLineNumbers);
396             tests[i].setProperties(properties);
397             tests[i].setNumberInDocument(i);
398         }
399         return tests;
400     }
401 
402     private String getNodeValue(Element parentElm, String nodeName, boolean required) {
403         NodeList nodes = parentElm.getElementsByTagName(nodeName);
404         if (nodes == null || nodes.getLength() == 0) {
405             if (required) {
406                 throw new RuntimeException("Required tag is missing from the test-xml: " + nodeName);
407             } else {
408                 return null;
409             }
410         }
411         Node node = nodes.item(0);
412         return parseTextNode(node);
413     }
414 
415     private static String parseTextNode(Node exampleNode) {
416         StringBuffer buffer = new StringBuffer();
417         for (int i = 0; i < exampleNode.getChildNodes().getLength(); i++) {
418             Node node = exampleNode.getChildNodes().item(i);
419             if (node.getNodeType() == Node.CDATA_SECTION_NODE
420                     || node.getNodeType() == Node.TEXT_NODE) {
421                 buffer.append(node.getNodeValue());
422             }
423         }
424         return buffer.toString().trim();
425     }
426 }