1 /******************************************************************************* 2 * Copyright (c) 2009, 2015 Mountainminds GmbH & Co. KG and Contributors 3 * All rights reserved. This program and the accompanying materials 4 * are made available under the terms of the Eclipse Public License v1.0 5 * which accompanies this distribution, and is available at 6 * http://www.eclipse.org/legal/epl-v10.html 7 * 8 * Contributors: 9 * Marc R. Hoffmann - initial API and implementation 10 * 11 *******************************************************************************/ 12 package org.jacoco.ant; 13 14 import static java.lang.String.format; 15 16 import java.io.File; 17 import java.io.FileOutputStream; 18 import java.io.IOException; 19 import java.io.InputStream; 20 import java.util.ArrayList; 21 import java.util.Collection; 22 import java.util.Iterator; 23 import java.util.List; 24 import java.util.Locale; 25 import java.util.StringTokenizer; 26 27 import org.apache.tools.ant.BuildException; 28 import org.apache.tools.ant.Project; 29 import org.apache.tools.ant.Task; 30 import org.apache.tools.ant.types.Resource; 31 import org.apache.tools.ant.types.resources.FileResource; 32 import org.apache.tools.ant.types.resources.Union; 33 import org.apache.tools.ant.util.FileUtils; 34 import org.jacoco.core.analysis.Analyzer; 35 import org.jacoco.core.analysis.CoverageBuilder; 36 import org.jacoco.core.analysis.IBundleCoverage; 37 import org.jacoco.core.analysis.IClassCoverage; 38 import org.jacoco.core.analysis.ICoverageNode; 39 import org.jacoco.core.data.ExecutionDataStore; 40 import org.jacoco.core.data.SessionInfoStore; 41 import org.jacoco.core.tools.ExecFileLoader; 42 import org.jacoco.report.FileMultiReportOutput; 43 import org.jacoco.report.IMultiReportOutput; 44 import org.jacoco.report.IReportGroupVisitor; 45 import org.jacoco.report.IReportVisitor; 46 import org.jacoco.report.MultiReportVisitor; 47 import org.jacoco.report.ZipMultiReportOutput; 48 import org.jacoco.report.check.IViolationsOutput; 49 import org.jacoco.report.check.Limit; 50 import org.jacoco.report.check.Rule; 51 import org.jacoco.report.check.RulesChecker; 52 import org.jacoco.report.csv.CSVFormatter; 53 import org.jacoco.report.html.HTMLFormatter; 54 import org.jacoco.report.xml.XMLFormatter; 55 56 /** 57 * Task for coverage report generation. 58 */ 59 public class ReportTask extends Task { 60 61 /** 62 * The source files are specified in a resource collection with additional 63 * attributes. 64 */ 65 public static class SourceFilesElement extends Union { 66 67 String encoding = null; 68 69 int tabWidth = 4; 70 71 /** 72 * Defines the optional source file encoding. If not set the platform 73 * default is used. 74 * 75 * @param encoding 76 * source file encoding 77 */ setEncoding(final String encoding)78 public void setEncoding(final String encoding) { 79 this.encoding = encoding; 80 } 81 82 /** 83 * Sets the tab stop width for the source pages. Default value is 4. 84 * 85 * @param tabWidth 86 * number of characters per tab stop 87 */ setTabwidth(final int tabWidth)88 public void setTabwidth(final int tabWidth) { 89 if (tabWidth <= 0) { 90 throw new BuildException("Tab width must be greater than 0"); 91 } 92 this.tabWidth = tabWidth; 93 } 94 95 } 96 97 /** 98 * Container element for class file groups. 99 */ 100 public static class GroupElement { 101 102 private final List<GroupElement> children = new ArrayList<GroupElement>(); 103 104 private final Union classfiles = new Union(); 105 106 private final SourceFilesElement sourcefiles = new SourceFilesElement(); 107 108 private String name; 109 110 /** 111 * Sets the name of the group. 112 * 113 * @param name 114 * name of the group 115 */ setName(final String name)116 public void setName(final String name) { 117 this.name = name; 118 } 119 120 /** 121 * Creates a new child group. 122 * 123 * @return new child group 124 */ createGroup()125 public GroupElement createGroup() { 126 final GroupElement group = new GroupElement(); 127 children.add(group); 128 return group; 129 } 130 131 /** 132 * Returns the nested resource collection for class files. 133 * 134 * @return resource collection for class files 135 */ createClassfiles()136 public Union createClassfiles() { 137 return classfiles; 138 } 139 140 /** 141 * Returns the nested resource collection for source files. 142 * 143 * @return resource collection for source files 144 */ createSourcefiles()145 public SourceFilesElement createSourcefiles() { 146 return sourcefiles; 147 } 148 149 } 150 151 /** 152 * Interface for child elements that define formatters. 153 */ 154 private abstract class FormatterElement { 155 createVisitor()156 abstract IReportVisitor createVisitor() throws IOException; 157 finish()158 void finish() { 159 } 160 } 161 162 /** 163 * Formatter element for HTML reports. 164 */ 165 public class HTMLFormatterElement extends FormatterElement { 166 167 private File destdir; 168 169 private File destfile; 170 171 private String footer = ""; 172 173 private String encoding = "UTF-8"; 174 175 private Locale locale = Locale.getDefault(); 176 177 /** 178 * Sets the output directory for the report. 179 * 180 * @param destdir 181 * output directory 182 */ setDestdir(final File destdir)183 public void setDestdir(final File destdir) { 184 this.destdir = destdir; 185 } 186 187 /** 188 * Sets the Zip output file for the report. 189 * 190 * @param destfile 191 * Zip output file 192 */ setDestfile(final File destfile)193 public void setDestfile(final File destfile) { 194 this.destfile = destfile; 195 } 196 197 /** 198 * Sets an optional footer text that will be displayed on every report 199 * page. 200 * 201 * @param text 202 * footer text 203 */ setFooter(final String text)204 public void setFooter(final String text) { 205 this.footer = text; 206 } 207 208 /** 209 * Sets the output encoding for generated HTML files. Default is UTF-8. 210 * 211 * @param encoding 212 * output encoding 213 */ setEncoding(final String encoding)214 public void setEncoding(final String encoding) { 215 this.encoding = encoding; 216 } 217 218 /** 219 * Sets the locale for generated text output. By default the platform 220 * locale is used. 221 * 222 * @param locale 223 * text locale 224 */ setLocale(final String locale)225 public void setLocale(final String locale) { 226 this.locale = parseLocale(locale); 227 } 228 229 @Override createVisitor()230 public IReportVisitor createVisitor() throws IOException { 231 final IMultiReportOutput output; 232 if (destfile != null) { 233 if (destdir != null) { 234 throw new BuildException( 235 "Either destination directory or file must be supplied, not both", 236 getLocation()); 237 } 238 final FileOutputStream stream = new FileOutputStream(destfile); 239 output = new ZipMultiReportOutput(stream); 240 241 } else { 242 if (destdir == null) { 243 throw new BuildException( 244 "Destination directory or file must be supplied for html report", 245 getLocation()); 246 } 247 output = new FileMultiReportOutput(destdir); 248 } 249 final HTMLFormatter formatter = new HTMLFormatter(); 250 formatter.setFooterText(footer); 251 formatter.setOutputEncoding(encoding); 252 formatter.setLocale(locale); 253 return formatter.createVisitor(output); 254 } 255 256 } 257 258 /** 259 * Formatter element for CSV reports. 260 */ 261 public class CSVFormatterElement extends FormatterElement { 262 263 private File destfile; 264 265 private String encoding = "UTF-8"; 266 267 /** 268 * Sets the output file for the report. 269 * 270 * @param destfile 271 * output file 272 */ setDestfile(final File destfile)273 public void setDestfile(final File destfile) { 274 this.destfile = destfile; 275 } 276 277 @Override createVisitor()278 public IReportVisitor createVisitor() throws IOException { 279 if (destfile == null) { 280 throw new BuildException( 281 "Destination file must be supplied for csv report", 282 getLocation()); 283 } 284 final CSVFormatter formatter = new CSVFormatter(); 285 formatter.setOutputEncoding(encoding); 286 return formatter.createVisitor(new FileOutputStream(destfile)); 287 } 288 289 /** 290 * Sets the output encoding for generated XML file. Default is UTF-8. 291 * 292 * @param encoding 293 * output encoding 294 */ setEncoding(final String encoding)295 public void setEncoding(final String encoding) { 296 this.encoding = encoding; 297 } 298 299 } 300 301 /** 302 * Formatter element for XML reports. 303 */ 304 public class XMLFormatterElement extends FormatterElement { 305 306 private File destfile; 307 308 private String encoding = "UTF-8"; 309 310 /** 311 * Sets the output file for the report. 312 * 313 * @param destfile 314 * output file 315 */ setDestfile(final File destfile)316 public void setDestfile(final File destfile) { 317 this.destfile = destfile; 318 } 319 320 /** 321 * Sets the output encoding for generated XML file. Default is UTF-8. 322 * 323 * @param encoding 324 * output encoding 325 */ setEncoding(final String encoding)326 public void setEncoding(final String encoding) { 327 this.encoding = encoding; 328 } 329 330 @Override createVisitor()331 public IReportVisitor createVisitor() throws IOException { 332 if (destfile == null) { 333 throw new BuildException( 334 "Destination file must be supplied for xml report", 335 getLocation()); 336 } 337 final XMLFormatter formatter = new XMLFormatter(); 338 formatter.setOutputEncoding(encoding); 339 return formatter.createVisitor(new FileOutputStream(destfile)); 340 } 341 342 } 343 344 /** 345 * Formatter element for coverage checks. 346 */ 347 public class CheckFormatterElement extends FormatterElement implements 348 IViolationsOutput { 349 350 private final List<Rule> rules = new ArrayList<Rule>(); 351 private boolean violations = false; 352 private boolean failOnViolation = true; 353 private String violationsPropery = null; 354 355 /** 356 * Creates and adds a new rule. 357 * 358 * @return new rule 359 */ createRule()360 public Rule createRule() { 361 final Rule rule = new Rule(); 362 rules.add(rule); 363 return rule; 364 } 365 366 /** 367 * Sets whether the build should fail in case of a violation. Default is 368 * <code>true</code>. 369 * 370 * @param flag 371 * if <code>true</code> the build fails on violation 372 */ setFailOnViolation(final boolean flag)373 public void setFailOnViolation(final boolean flag) { 374 this.failOnViolation = flag; 375 } 376 377 /** 378 * Sets the name of a property to append the violation messages to. 379 * 380 * @param property 381 * name of a property 382 */ setViolationsProperty(final String property)383 public void setViolationsProperty(final String property) { 384 this.violationsPropery = property; 385 } 386 387 @Override createVisitor()388 public IReportVisitor createVisitor() throws IOException { 389 final RulesChecker formatter = new RulesChecker(); 390 formatter.setRules(rules); 391 return formatter.createVisitor(this); 392 } 393 onViolation(final ICoverageNode node, final Rule rule, final Limit limit, final String message)394 public void onViolation(final ICoverageNode node, final Rule rule, 395 final Limit limit, final String message) { 396 log(message, Project.MSG_ERR); 397 violations = true; 398 if (violationsPropery != null) { 399 final String old = getProject().getProperty(violationsPropery); 400 final String value = old == null ? message : String.format( 401 "%s\n%s", old, message); 402 getProject().setProperty(violationsPropery, value); 403 } 404 } 405 406 @Override finish()407 void finish() { 408 if (violations && failOnViolation) { 409 throw new BuildException( 410 "Coverage check failed due to violated rules.", 411 getLocation()); 412 } 413 } 414 } 415 416 private final Union executiondataElement = new Union(); 417 418 private SessionInfoStore sessionInfoStore; 419 420 private ExecutionDataStore executionDataStore; 421 422 private final GroupElement structure = new GroupElement(); 423 424 private final List<FormatterElement> formatters = new ArrayList<FormatterElement>(); 425 426 /** 427 * Returns the nested resource collection for execution data files. 428 * 429 * @return resource collection for execution files 430 */ createExecutiondata()431 public Union createExecutiondata() { 432 return executiondataElement; 433 } 434 435 /** 436 * Returns the root group element that defines the report structure. 437 * 438 * @return root group element 439 */ createStructure()440 public GroupElement createStructure() { 441 return structure; 442 } 443 444 /** 445 * Creates a new HTML report element. 446 * 447 * @return HTML report element 448 */ createHtml()449 public HTMLFormatterElement createHtml() { 450 final HTMLFormatterElement element = new HTMLFormatterElement(); 451 formatters.add(element); 452 return element; 453 } 454 455 /** 456 * Creates a new CSV report element. 457 * 458 * @return CSV report element 459 */ createCsv()460 public CSVFormatterElement createCsv() { 461 final CSVFormatterElement element = new CSVFormatterElement(); 462 formatters.add(element); 463 return element; 464 } 465 466 /** 467 * Creates a new coverage check element. 468 * 469 * @return coverage check element 470 */ createCheck()471 public CheckFormatterElement createCheck() { 472 final CheckFormatterElement element = new CheckFormatterElement(); 473 formatters.add(element); 474 return element; 475 } 476 477 /** 478 * Creates a new XML report element. 479 * 480 * @return CSV report element 481 */ createXml()482 public XMLFormatterElement createXml() { 483 final XMLFormatterElement element = new XMLFormatterElement(); 484 formatters.add(element); 485 return element; 486 } 487 488 @Override execute()489 public void execute() throws BuildException { 490 loadExecutionData(); 491 try { 492 final IReportVisitor visitor = createVisitor(); 493 visitor.visitInfo(sessionInfoStore.getInfos(), 494 executionDataStore.getContents()); 495 createReport(visitor, structure); 496 visitor.visitEnd(); 497 for (final FormatterElement f : formatters) { 498 f.finish(); 499 } 500 } catch (final IOException e) { 501 throw new BuildException("Error while creating report", e, 502 getLocation()); 503 } 504 } 505 loadExecutionData()506 private void loadExecutionData() { 507 final ExecFileLoader loader = new ExecFileLoader(); 508 for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) { 509 final Resource resource = (Resource) i.next(); 510 log(format("Loading execution data file %s", resource)); 511 InputStream in = null; 512 try { 513 in = resource.getInputStream(); 514 loader.load(in); 515 } catch (final IOException e) { 516 throw new BuildException(format( 517 "Unable to read execution data file %s", resource), e, 518 getLocation()); 519 } finally { 520 FileUtils.close(in); 521 } 522 } 523 sessionInfoStore = loader.getSessionInfoStore(); 524 executionDataStore = loader.getExecutionDataStore(); 525 } 526 createVisitor()527 private IReportVisitor createVisitor() throws IOException { 528 final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>(); 529 for (final FormatterElement f : formatters) { 530 visitors.add(f.createVisitor()); 531 } 532 return new MultiReportVisitor(visitors); 533 } 534 createReport(final IReportGroupVisitor visitor, final GroupElement group)535 private void createReport(final IReportGroupVisitor visitor, 536 final GroupElement group) throws IOException { 537 if (group.name == null) { 538 throw new BuildException("Group name must be supplied", 539 getLocation()); 540 } 541 if (group.children.isEmpty()) { 542 final IBundleCoverage bundle = createBundle(group); 543 final SourceFilesElement sourcefiles = group.sourcefiles; 544 final AntResourcesLocator locator = new AntResourcesLocator( 545 sourcefiles.encoding, sourcefiles.tabWidth); 546 locator.addAll(sourcefiles.iterator()); 547 if (!locator.isEmpty()) { 548 checkForMissingDebugInformation(bundle); 549 } 550 visitor.visitBundle(bundle, locator); 551 } else { 552 final IReportGroupVisitor groupVisitor = visitor 553 .visitGroup(group.name); 554 for (final GroupElement child : group.children) { 555 createReport(groupVisitor, child); 556 } 557 } 558 } 559 createBundle(final GroupElement group)560 private IBundleCoverage createBundle(final GroupElement group) 561 throws IOException { 562 final CoverageBuilder builder = new CoverageBuilder(); 563 final Analyzer analyzer = new Analyzer(executionDataStore, builder); 564 for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) { 565 final Resource resource = (Resource) i.next(); 566 if (resource.isDirectory() && resource instanceof FileResource) { 567 analyzer.analyzeAll(((FileResource) resource).getFile()); 568 } else { 569 final InputStream in = resource.getInputStream(); 570 analyzer.analyzeAll(in, resource.getName()); 571 in.close(); 572 } 573 } 574 final IBundleCoverage bundle = builder.getBundle(group.name); 575 logBundleInfo(bundle, builder.getNoMatchClasses()); 576 return bundle; 577 } 578 logBundleInfo(final IBundleCoverage bundle, final Collection<IClassCoverage> nomatch)579 private void logBundleInfo(final IBundleCoverage bundle, 580 final Collection<IClassCoverage> nomatch) { 581 log(format("Writing bundle '%s' with %s classes", bundle.getName(), 582 Integer.valueOf(bundle.getClassCounter().getTotalCount()))); 583 if (!nomatch.isEmpty()) { 584 log(format( 585 "Classes in bundle '%s' do no match with execution data. " 586 + "For report generation the same class files must be used as at runtime.", 587 bundle.getName()), Project.MSG_WARN); 588 for (final IClassCoverage c : nomatch) { 589 log(format("Execution data for class %s does not match.", 590 c.getName()), Project.MSG_WARN); 591 } 592 } 593 } 594 checkForMissingDebugInformation(final ICoverageNode node)595 private void checkForMissingDebugInformation(final ICoverageNode node) { 596 if (node.getClassCounter().getTotalCount() > 0 597 && node.getLineCounter().getTotalCount() == 0) { 598 log(format( 599 "To enable source code annotation class files for bundle '%s' have to be compiled with debug information.", 600 node.getName()), Project.MSG_WARN); 601 } 602 } 603 604 /** 605 * Splits a given underscore "_" separated string and creates a Locale. This 606 * method is implemented as the method Locale.forLanguageTag() was not 607 * available in Java 5. 608 * 609 * @param locale 610 * String representation of a Locate 611 * @return Locale instance 612 */ parseLocale(final String locale)613 static Locale parseLocale(final String locale) { 614 final StringTokenizer st = new StringTokenizer(locale, "_"); 615 final String language = st.hasMoreTokens() ? st.nextToken() : ""; 616 final String country = st.hasMoreTokens() ? st.nextToken() : ""; 617 final String variant = st.hasMoreTokens() ? st.nextToken() : ""; 618 return new Locale(language, country, variant); 619 } 620 621 } 622