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