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