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