View Javadoc
1   /*
2    * The MIT License (MIT)
3    *
4    * Copyright (c) 2013-2026 The Coveralls Maven Plugin Project Contributors:
5    *     https://github.com/hazendaz/coveralls-maven-plugin/graphs/contributors
6    *
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   *
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   *
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   */
25  package org.eluder.coveralls.maven.plugin;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.nio.charset.Charset;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Properties;
33  
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.settings.Settings;
41  import org.eluder.coveralls.maven.plugin.domain.GitRepository;
42  import org.eluder.coveralls.maven.plugin.domain.Job;
43  import org.eluder.coveralls.maven.plugin.httpclient.CoverallsClient;
44  import org.eluder.coveralls.maven.plugin.httpclient.CoverallsProxyClient;
45  import org.eluder.coveralls.maven.plugin.json.JsonWriter;
46  import org.eluder.coveralls.maven.plugin.logging.CoverageTracingLogger;
47  import org.eluder.coveralls.maven.plugin.logging.DryRunLogger;
48  import org.eluder.coveralls.maven.plugin.logging.JobLogger;
49  import org.eluder.coveralls.maven.plugin.logging.Logger;
50  import org.eluder.coveralls.maven.plugin.logging.Logger.Position;
51  import org.eluder.coveralls.maven.plugin.service.Appveyor;
52  import org.eluder.coveralls.maven.plugin.service.Bamboo;
53  import org.eluder.coveralls.maven.plugin.service.Circle;
54  import org.eluder.coveralls.maven.plugin.service.General;
55  import org.eluder.coveralls.maven.plugin.service.GitHub;
56  import org.eluder.coveralls.maven.plugin.service.Jenkins;
57  import org.eluder.coveralls.maven.plugin.service.ServiceSetup;
58  import org.eluder.coveralls.maven.plugin.service.Shippable;
59  import org.eluder.coveralls.maven.plugin.service.Travis;
60  import org.eluder.coveralls.maven.plugin.service.Wercker;
61  import org.eluder.coveralls.maven.plugin.source.SourceCallback;
62  import org.eluder.coveralls.maven.plugin.source.SourceLoader;
63  import org.eluder.coveralls.maven.plugin.source.UniqueSourceCallback;
64  import org.eluder.coveralls.maven.plugin.util.CoverageParsersFactory;
65  import org.eluder.coveralls.maven.plugin.util.SourceLoaderFactory;
66  import org.eluder.coveralls.maven.plugin.util.TimestampParser;
67  
68  /**
69   * The Class CoverallsReportMojo.
70   */
71  @Mojo(name = "report", threadSafe = false, aggregator = true)
72  public class CoverallsReportMojo extends AbstractMojo {
73  
74      /**
75       * In a Maven Multi-Module project, it's common to configure JaCoCo with a Maven project that's used for aggregating
76       * all JaCoCo reports into a single location. In this circumstance use the "jacocoAggregateReport" property so that
77       * only this location is used for coverage rather than looking for JaCoCo reports in each Maven project.
78       *
79       * @since 5.0.0
80       */
81      @Parameter(property = "jacocoAggregateReport")
82      private File jacocoAggregateReport;
83  
84      /**
85       * File paths to additional JaCoCo coverage report files.
86       * <p>
87       * By default, this plugin will look for a files in the standard JaCoCo locations. If a merged report is found in
88       *
89       * <pre>
90       * ${project.reporting.outputDirectory}/jacoco-merged-report/jacoco.xml
91       * </pre>
92       *
93       * then it will be used. Otherwise, the plugin will fallback to looking for files in both
94       *
95       * <pre>
96       * ${project.reporting.outputDirectory}/jacoco/jacoco.xml
97       * </pre>
98       *
99       * and
100      *
101      * <pre>
102      * ${project.reporting.outputDirectory}/jacoco-it/jacoco.xml
103      * </pre>
104      * <p>
105      * If the "jacocoAggregateReport" property is set then this property will be ignored
106      */
107     @Parameter(property = "jacocoReports")
108     private List<File> jacocoReports;
109 
110     /**
111      * File paths to additional Cobertura coverage report files.
112      * <p>
113      * Coveralls Maven Plugin 4.5.0 is last to work with Cobertura as java 8 required.
114      *
115      * @deprecated Cobertura is deprecated and no longer maintained. Use JaCoCo instead.
116      */
117     @Deprecated(since = "5.0.0", forRemoval = true)
118     @Parameter(property = "coberturaReports")
119     private List<File> coberturaReports;
120 
121     /**
122      * File paths to additional Saga coverage report files.
123      */
124     @Parameter(property = "sagaReports")
125     private List<File> sagaReports;
126 
127     /**
128      * File paths to additional Clover coverage report files.
129      */
130     @Parameter(property = "cloverReports")
131     private List<File> cloverReports;
132 
133     /**
134      * Directories for relative per module specific report files.
135      */
136     @Parameter(property = "relativeReportDirs")
137     private List<String> relativeReportDirs;
138 
139     /**
140      * File path to write and submit Coveralls data.
141      */
142     @Parameter(property = "coverallsFile", defaultValue = "${project.build.directory}/coveralls.json")
143     File coverallsFile;
144 
145     /**
146      * Url for the Coveralls API.
147      */
148     @Parameter(property = "coverallsUrl", defaultValue = "https://coveralls.io/api/v1/jobs")
149     private String coverallsUrl;
150 
151     /**
152      * Source directories.
153      */
154     @Parameter(property = "sourceDirectories")
155     List<File> sourceDirectories;
156 
157     /**
158      * Source file encoding.
159      */
160     @Parameter(property = "sourceEncoding", defaultValue = "${project.build.sourceEncoding}")
161     String sourceEncoding;
162 
163     /**
164      * CI service name.
165      */
166     @Parameter(property = "serviceName")
167     String serviceName;
168 
169     /**
170      * CI service job id.
171      */
172     @Parameter(property = "serviceJobId")
173     String serviceJobId;
174 
175     /**
176      * CI service build number.
177      */
178     @Parameter(property = "serviceBuildNumber")
179     String serviceBuildNumber;
180 
181     /**
182      * CI service build url.
183      */
184     @Parameter(property = "serviceBuildUrl")
185     String serviceBuildUrl;
186 
187     /**
188      * CI service specific environment properties.
189      */
190     @Parameter(property = "serviceEnvironment")
191     Properties serviceEnvironment;
192 
193     /**
194      * Coveralls repository token.
195      */
196     @Parameter(property = "repoToken")
197     String repoToken;
198 
199     /**
200      * Git branch name.
201      */
202     @Parameter(property = "branch")
203     String branch;
204 
205     /**
206      * GitHub pull request identifier.
207      */
208     @Parameter(property = "pullRequest")
209     String pullRequest;
210 
211     /**
212      * Coveralls parallel flag.
213      */
214     @Parameter(property = "parallel")
215     private boolean parallel;
216 
217     /**
218      * Build timestamp format. Must be in format supported by DateTimeFormatter.
219      */
220     @Parameter(property = "timestampFormat", defaultValue = "${maven.build.timestamp.format}")
221     private String timestampFormat;
222 
223     /**
224      * Build timestamp. Must be in format defined by 'timestampFormat' if it's available or in default timestamp format
225      * yyyy-MM-dd'T'HH:mm:ss'Z'.
226      */
227     @Parameter(property = "timestamp", defaultValue = "${maven.build.timestamp}")
228     private String timestamp;
229 
230     /**
231      * Dry run Coveralls report without actually sending it.
232      */
233     @Parameter(property = "dryRun", defaultValue = "false")
234     boolean dryRun;
235 
236     /**
237      * Fail build if Coveralls service is not available or submission fails for internal errors.
238      */
239     @Parameter(property = "failOnServiceError", defaultValue = "true")
240     boolean failOnServiceError;
241 
242     /**
243      * Scan subdirectories for source files.
244      */
245     @Parameter(property = "scanForSources", defaultValue = "false")
246     private boolean scanForSources;
247 
248     /**
249      * Base directory of the project.
250      */
251     @Parameter(property = "coveralls.basedir", defaultValue = "${project.basedir}")
252     File basedir;
253 
254     /**
255      * Skip the plugin execution.
256      */
257     @Parameter(property = "coveralls.skip", defaultValue = "false")
258     boolean skip;
259 
260     /**
261      * Maven settings.
262      */
263     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
264     Settings settings;
265 
266     /**
267      * Maven project for runtime value resolution.
268      */
269     @Parameter(defaultValue = "${project}", readonly = true)
270     MavenProject project;
271 
272     /**
273      * Instantiates a new coveralls report mojo.
274      */
275     public CoverallsReportMojo() {
276         // do nothing
277     }
278 
279     @Override
280     public final void execute() throws MojoExecutionException, MojoFailureException {
281         if (this.skip) {
282             this.getLog().info("Skip property set, skipping plugin execution");
283             return;
284         }
285 
286         try {
287             this.createEnvironment().setup();
288 
289             final var job = this.createJob();
290             job.validate().throwOrInform(this.getLog());
291 
292             final var sourceLoader = this.createSourceLoader(job);
293 
294             final var parsers = this.createCoverageParsers(sourceLoader);
295 
296             final var client = this.createCoverallsClient();
297 
298             final List<Logger> reporters = new ArrayList<>();
299             reporters.add(new JobLogger(job));
300 
301             try (var writer = this.createJsonWriter(job)) {
302                 // For tests (its the same instance as in writer)
303                 this.coverallsFile = writer.getCoverallsFile();
304 
305                 final var sourceCallback = this.createSourceCallbackChain(writer, reporters);
306                 reporters.add(new DryRunLogger(job.isDryRun(), this.coverallsFile));
307 
308                 this.report(reporters, Position.BEFORE);
309                 this.writeCoveralls(writer, sourceCallback, parsers);
310                 this.report(reporters, Position.AFTER);
311             }
312 
313             if (!job.isDryRun()) {
314                 this.submitData(client, this.coverallsFile);
315             }
316         } catch (final ProcessingException e) {
317             throw new MojoFailureException("Processing of input or output data failed", e);
318         } catch (final IOException e) {
319             throw new MojoFailureException("I/O operation failed", e);
320         } catch (final Exception e) {
321             throw new MojoExecutionException("Build error", e);
322         }
323     }
324 
325     /**
326      * Creates the coverage parsers.
327      *
328      * @param sourceLoader
329      *            source loader that extracts source files
330      *
331      * @return coverage parsers for all maven modules and additional reports
332      *
333      * @throws IOException
334      *             if parsers cannot be created
335      */
336     protected List<CoverageParser> createCoverageParsers(final SourceLoader sourceLoader) throws IOException {
337         return new CoverageParsersFactory(this.project, sourceLoader).withJaCoCoReports(this.jacocoReports)
338                 .withJacocoAggregateReport(this.jacocoAggregateReport).withCoberturaReports(this.coberturaReports)
339                 .withSagaReports(this.sagaReports).withCloverReports(this.cloverReports)
340                 .withRelativeReportDirs(this.relativeReportDirs).createParsers();
341     }
342 
343     /**
344      * Creates the source loader.
345      *
346      * @param job
347      *            the job describing the coveralls report
348      *
349      * @return source loader that extracts source files
350      */
351     protected SourceLoader createSourceLoader(final Job job) {
352         return new SourceLoaderFactory(job.getGit().getBaseDir(), this.project, Charset.forName(this.sourceEncoding))
353                 .withSourceDirectories(this.sourceDirectories).withScanForSources(this.scanForSources)
354                 .createSourceLoader();
355     }
356 
357     /**
358      * Creates the environment.
359      *
360      * @return environment to setup mojo and service specific properties
361      */
362     protected Environment createEnvironment() {
363         return new Environment(this, this.getServices());
364     }
365 
366     /**
367      * Gets the services.
368      *
369      * @return list of available continuous integration services
370      */
371     protected List<ServiceSetup> getServices() {
372         final var env = System.getenv();
373         final List<ServiceSetup> services = new ArrayList<>();
374         services.add(new GitHub(env));
375         services.add(new Shippable(env));
376         services.add(new Travis(env));
377         services.add(new Circle(env));
378         services.add(new Jenkins(env));
379         services.add(new Bamboo(env));
380         services.add(new Appveyor(env));
381         services.add(new Wercker(env));
382         services.add(new General(env));
383         return services;
384     }
385 
386     /**
387      * Creates the job.
388      *
389      * @return job that describes the coveralls report
390      *
391      * @throws ProcessingException
392      *             if processing of timestamp fails
393      * @throws IOException
394      *             if an I/O error occurs
395      */
396     protected Job createJob() throws ProcessingException, IOException {
397         final var git = new GitRepository(this.basedir).load();
398         final var time = this.timestamp == null ? null
399                 : new TimestampParser(this.timestampFormat).parse(this.timestamp).toEpochMilli();
400 
401         // Log all non-secret items for debugging and transparency
402         this.getLog().info("Coveralls Job Configuration:");
403         this.getLog().info("  serviceName: " + this.serviceName);
404         this.getLog().info("  serviceJobId: " + this.serviceJobId);
405         this.getLog().info("  serviceBuildNumber: " + this.serviceBuildNumber);
406         this.getLog().info("  serviceBuildUrl: " + this.serviceBuildUrl);
407         this.getLog().info("  parallel: " + this.parallel);
408         this.getLog().info("  branch: " + this.branch);
409         this.getLog().info("  pullRequest: " + this.pullRequest);
410         this.getLog().info("  timestamp: " + time);
411         this.getLog().info("  basedir: " + (this.basedir != null ? this.basedir.getAbsolutePath() : "null"));
412         this.getLog().info("  sourceEncoding: " + this.sourceEncoding);
413         this.getLog().info(
414                 "  coverallsFile: " + (this.coverallsFile != null ? this.coverallsFile.getAbsolutePath() : "null"));
415         this.getLog().info("  coverallsUrl: " + this.coverallsUrl);
416 
417         return new Job().withRepoToken(this.repoToken).withServiceName(this.serviceName)
418                 .withServiceJobId(this.serviceJobId).withServiceBuildNumber(this.serviceBuildNumber)
419                 .withServiceBuildUrl(this.serviceBuildUrl).withParallel(this.parallel)
420                 .withServiceEnvironment(this.serviceEnvironment).withDryRun(this.dryRun).withBranch(this.branch)
421                 .withPullRequest(this.pullRequest).withTimestamp(time).withGit(git);
422     }
423 
424     /**
425      * Creates the json writer.
426      *
427      * @param job
428      *            the job describing the coveralls report
429      *
430      * @return JSON writer that writes the coveralls data
431      *
432      * @throws IOException
433      *             if an I/O error occurs
434      */
435     protected JsonWriter createJsonWriter(final Job job) throws IOException {
436         return new JsonWriter(job, this.coverallsFile);
437     }
438 
439     /**
440      * Creates the coveralls client.
441      *
442      * @return http client that submits the coveralls data
443      */
444     protected CoverallsClient createCoverallsClient() {
445         return new CoverallsProxyClient(this.coverallsUrl, this.settings.getActiveProxy());
446     }
447 
448     /**
449      * Creates the source callback chain.
450      *
451      * @param writer
452      *            the JSON writer
453      * @param reporters
454      *            the logging reporters
455      *
456      * @return source callback chain for different source handlers
457      */
458     protected SourceCallback createSourceCallbackChain(final JsonWriter writer, final List<Logger> reporters) {
459         SourceCallback chain = writer;
460         if (this.getLog().isInfoEnabled()) {
461             final var coverageTracingReporter = new CoverageTracingLogger(chain);
462             chain = coverageTracingReporter;
463             reporters.add(coverageTracingReporter);
464         }
465         return new UniqueSourceCallback(chain);
466     }
467 
468     /**
469      * Writes coverage data to JSON file.
470      *
471      * @param writer
472      *            JSON writer that writes the coveralls data
473      * @param sourceCallback
474      *            the source callback handler
475      * @param parsers
476      *            list of coverage parsers
477      *
478      * @throws ProcessingException
479      *             if process to to create JSON file fails
480      * @throws IOException
481      *             if an I/O error occurs
482      */
483     protected void writeCoveralls(final JsonWriter writer, final SourceCallback sourceCallback,
484             final List<CoverageParser> parsers) throws ProcessingException, IOException {
485         this.getLog().info("Writing Coveralls data to " + this.coverallsFile.getAbsolutePath() + "...");
486         final var now = System.currentTimeMillis();
487         sourceCallback.onBegin();
488         for (final CoverageParser parser : parsers) {
489             this.getLog().info("Processing coverage report from " + parser.getCoverageFile().getAbsolutePath());
490             parser.parse(sourceCallback);
491         }
492         sourceCallback.onComplete();
493         final var duration = System.currentTimeMillis() - now;
494         this.getLog().info("Successfully wrote Coveralls data in " + duration + "ms");
495     }
496 
497     /**
498      * Submit data.
499      *
500      * @param client
501      *            the client
502      * @param coverallsFile
503      *            the coveralls file
504      *
505      * @throws ProcessingException
506      *             the processing exception
507      * @throws IOException
508      *             Signals that an I/O exception has occurred.
509      */
510     private void submitData(final CoverallsClient client, final File coverallsFile)
511             throws ProcessingException, IOException {
512         this.getLog().info("Submitting Coveralls data to API");
513         final var now = System.currentTimeMillis();
514         try {
515             final var response = client.submit(coverallsFile);
516             final var duration = System.currentTimeMillis() - now;
517             this.getLog()
518                     .info("Successfully submitted Coveralls data in " + duration + "ms for " + response.getMessage());
519             this.getLog().info(response.getUrl());
520             this.getLog().info("*** Coverage results are usually available immediately on Coveralls.");
521             this.getLog().info("    If you see question marks or missing data, please allow some time for processing.");
522         } catch (final ProcessingException e) {
523             final var duration = System.currentTimeMillis() - now;
524             final var message = "Submission failed in " + duration + "ms while processing data";
525             this.handleSubmissionError(e, message, true);
526         } catch (final IOException e) {
527             final var duration = System.currentTimeMillis() - now;
528             final var message = "Submission failed in " + duration + "ms while handling I/O operations";
529             this.handleSubmissionError(e, message, this.failOnServiceError);
530         } catch (final InterruptedException e) {
531             final var duration = System.currentTimeMillis() - now;
532             final var message = "Submission failed in " + duration + "ms due to an interuption";
533             this.getLog().error(message, e);
534             Thread.currentThread().interrupt();
535         }
536     }
537 
538     /**
539      * Handle submission error.
540      *
541      * @param <T>
542      *            the generic type
543      * @param ex
544      *            the ex
545      * @param message
546      *            the message
547      * @param failOnException
548      *            the fail on exception
549      *
550      * @throws T
551      *             the t
552      */
553     private <T extends Exception> void handleSubmissionError(final T ex, final String message,
554             final boolean failOnException) throws T {
555         if (failOnException) {
556             this.getLog().error(message);
557             throw ex;
558         }
559         this.getLog().warn(message);
560     }
561 
562     /**
563      * Report.
564      *
565      * @param reporters
566      *            the reporters
567      * @param position
568      *            the position
569      */
570     private void report(final List<Logger> reporters, final Position position) {
571         for (final Logger reporter : reporters) {
572             if (position.equals(reporter.getPosition())) {
573                 reporter.log(this.getLog());
574             }
575         }
576     }
577 }