View Javadoc
1   /*
2    * The MIT License (MIT)
3    *
4    * Copyright (c) 2013 - 2023, Tapio Rautonen
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in
14   * all copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22   * THE SOFTWARE.
23   */
24  package org.eluder.coveralls.maven.plugin;
25  
26  import org.apache.maven.plugin.AbstractMojo;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugin.MojoFailureException;
29  import org.apache.maven.plugins.annotations.Mojo;
30  import org.apache.maven.plugins.annotations.Parameter;
31  import org.apache.maven.project.MavenProject;
32  import org.apache.maven.settings.Settings;
33  import org.eluder.coveralls.maven.plugin.domain.CoverallsResponse;
34  import org.eluder.coveralls.maven.plugin.domain.Git;
35  import org.eluder.coveralls.maven.plugin.domain.GitRepository;
36  import org.eluder.coveralls.maven.plugin.domain.Job;
37  import org.eluder.coveralls.maven.plugin.httpclient.CoverallsClient;
38  import org.eluder.coveralls.maven.plugin.httpclient.CoverallsProxyClient;
39  import org.eluder.coveralls.maven.plugin.json.JsonWriter;
40  import org.eluder.coveralls.maven.plugin.logging.CoverageTracingLogger;
41  import org.eluder.coveralls.maven.plugin.logging.DryRunLogger;
42  import org.eluder.coveralls.maven.plugin.logging.JobLogger;
43  import org.eluder.coveralls.maven.plugin.logging.Logger;
44  import org.eluder.coveralls.maven.plugin.logging.Logger.Position;
45  import org.eluder.coveralls.maven.plugin.service.Appveyor;
46  import org.eluder.coveralls.maven.plugin.service.Bamboo;
47  import org.eluder.coveralls.maven.plugin.service.Circle;
48  import org.eluder.coveralls.maven.plugin.service.General;
49  import org.eluder.coveralls.maven.plugin.service.Jenkins;
50  import org.eluder.coveralls.maven.plugin.service.ServiceSetup;
51  import org.eluder.coveralls.maven.plugin.service.Shippable;
52  import org.eluder.coveralls.maven.plugin.service.Travis;
53  import org.eluder.coveralls.maven.plugin.service.Wercker;
54  import org.eluder.coveralls.maven.plugin.source.SourceCallback;
55  import org.eluder.coveralls.maven.plugin.source.SourceLoader;
56  import org.eluder.coveralls.maven.plugin.source.UniqueSourceCallback;
57  import org.eluder.coveralls.maven.plugin.util.CoverageParsersFactory;
58  import org.eluder.coveralls.maven.plugin.util.SourceLoaderFactory;
59  import org.eluder.coveralls.maven.plugin.util.TimestampParser;
60  
61  import java.io.File;
62  import java.io.IOException;
63  import java.util.ArrayList;
64  import java.util.Date;
65  import java.util.List;
66  import java.util.Map;
67  import java.util.Properties;
68  
69  @Mojo(name = "report", threadSafe = false, aggregator = true)
70  public class CoverallsReportMojo extends AbstractMojo {
71  
72      /**
73       * File paths to additional JaCoCo coverage report files.
74       */
75      @Parameter(property = "jacocoReports")
76      protected List<File> jacocoReports;
77  
78      /**
79       * File paths to additional Cobertura coverage report files.
80       */
81      @Parameter(property = "coberturaReports")
82      protected List<File> coberturaReports;
83  
84      /**
85       * File paths to additional Saga coverage report files.
86       */
87      @Parameter(property = "sagaReports")
88      protected List<File> sagaReports;
89  
90      /**
91       * Directories for relative per module specific report files.
92       */
93      @Parameter(property = "relativeReportDirs")
94      protected List<String> relativeReportDirs;
95  
96      /**
97       * File path to write and submit Coveralls data.
98       */
99      @Parameter(property = "coverallsFile", defaultValue = "${project.build.directory}/coveralls.json")
100     protected File coverallsFile;
101 
102     /**
103      * Url for the Coveralls API.
104      */
105     @Parameter(property = "coverallsUrl", defaultValue = "https://coveralls.io/api/v1/jobs")
106     protected String coverallsUrl;
107 
108     /**
109      * Source directories.
110      */
111     @Parameter(property = "sourceDirectories")
112     protected List<File> sourceDirectories;
113 
114     /**
115      * Source file encoding.
116      */
117     @Parameter(property = "sourceEncoding", defaultValue = "${project.build.sourceEncoding}")
118     protected String sourceEncoding;
119 
120     /**
121      * CI service name.
122      */
123     @Parameter(property = "serviceName")
124     protected String serviceName;
125 
126     /**
127      * CI service job id.
128      */
129     @Parameter(property = "serviceJobId")
130     protected String serviceJobId;
131 
132     /**
133      * CI service build number.
134      */
135     @Parameter(property = "serviceBuildNumber")
136     protected String serviceBuildNumber;
137 
138     /**
139      * CI service build url.
140      */
141     @Parameter(property = "serviceBuildUrl")
142     protected String serviceBuildUrl;
143 
144     /**
145      * CI service specific environment properties.
146      */
147     @Parameter(property = "serviceEnvironment")
148     protected Properties serviceEnvironment;
149 
150     /**
151      * Coveralls repository token.
152      */
153     @Parameter(property = "repoToken")
154     protected String repoToken;
155 
156     /**
157      * Git branch name.
158      */
159     @Parameter(property = "branch")
160     protected String branch;
161 
162     /**
163      * GitHub pull request identifier.
164      */
165     @Parameter(property = "pullRequest")
166     protected String pullRequest;
167 
168     /**
169      * Coveralls parallel flag.
170      */
171     @Parameter(property = "parallel")
172     protected boolean parallel;
173 
174     /**
175      * Build timestamp format. Must be in format supported by SimpleDateFormat.
176      */
177     @Parameter(property = "timestampFormat", defaultValue = "${maven.build.timestamp.format}")
178     protected String timestampFormat;
179 
180     /**
181      * Build timestamp. Must be in format defined by 'timestampFormat' if it's available or in
182      * default timestamp format yyyy-MM-dd'T'HH:mm:ss'Z'.
183      */
184     @Parameter(property = "timestamp", defaultValue = "${maven.build.timestamp}")
185     protected String timestamp;
186 
187     /**
188      * Dry run Coveralls report without actually sending it.
189      */
190     @Parameter(property = "dryRun", defaultValue = "false")
191     protected boolean dryRun;
192 
193     /**
194      * Fail build if Coveralls service is not available or submission fails for internal errors.
195      */
196     @Parameter(property = "failOnServiceError", defaultValue = "true")
197     protected boolean failOnServiceError;
198 
199     /**
200      * Scan subdirectories for source files.
201      */
202     @Parameter(property = "scanForSources", defaultValue = "false")
203     protected boolean scanForSources;
204 
205     /**
206      * Base directory of the project.
207      */
208     @Parameter(property = "coveralls.basedir", defaultValue = "${project.basedir}")
209     protected File basedir;
210 
211     /**
212      * Skip the plugin execution.
213      */
214     @Parameter(property = "coveralls.skip", defaultValue = "false")
215     protected boolean skip;
216 
217 
218     /**
219      * Maven settings.
220      */
221     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
222     protected Settings settings;
223 
224     /**
225      * Maven project for runtime value resolution.
226      */
227     @Parameter(defaultValue = "${project}", readonly = true)
228     protected MavenProject project;
229 
230 
231     @Override
232     public final void execute() throws MojoExecutionException, MojoFailureException {
233         if (skip) {
234             getLog().info("Skip property set, skipping plugin execution");
235             return;
236         }
237 
238         try {
239             createEnvironment().setup();
240             Job job = createJob();
241             job.validate().throwOrInform(getLog());
242             SourceLoader sourceLoader = createSourceLoader(job);
243             List<CoverageParser> parsers = createCoverageParsers(sourceLoader);
244             JsonWriter writer = createJsonWriter(job);
245             CoverallsClient client = createCoverallsClient();
246             List<Logger> reporters = new ArrayList<>();
247             reporters.add(new JobLogger(job));
248             SourceCallback sourceCallback = createSourceCallbackChain(writer, reporters);
249             reporters.add(new DryRunLogger(job.isDryRun(), writer.getCoverallsFile()));
250 
251             report(reporters, Position.BEFORE);
252             writeCoveralls(writer, sourceCallback, parsers);
253             report(reporters, Position.AFTER);
254 
255             if (!job.isDryRun()) {
256                 submitData(client, writer.getCoverallsFile());
257             }
258         } catch (ProcessingException ex) {
259             throw new MojoFailureException("Processing of input or output data failed", ex);
260         } catch (IOException ex) {
261             throw new MojoFailureException("I/O operation failed", ex);
262         } catch (Exception ex) {
263             throw new MojoExecutionException("Build error", ex);
264         }
265     }
266 
267     /**
268      * Creates the coverage parsers.
269      *
270      * @param sourceLoader source loader that extracts source files
271      * @return coverage parsers for all maven modules and additional reports
272      * @throws IOException if parsers cannot be created
273      */
274     protected List<CoverageParser> createCoverageParsers(final SourceLoader sourceLoader) throws IOException {
275         return new CoverageParsersFactory(project, sourceLoader)
276                 .withJaCoCoReports(jacocoReports)
277                 .withCoberturaReports(coberturaReports)
278                 .withSagaReports(sagaReports)
279                 .withRelativeReportDirs(relativeReportDirs)
280                 .createParsers();
281     }
282 
283     /**
284      * Creates the source loader.
285      *
286      * @param job the job describing the coveralls report
287      * @return source loader that extracts source files
288      */
289     protected SourceLoader createSourceLoader(final Job job) {
290         return new SourceLoaderFactory(job.getGit().getBaseDir(), project, sourceEncoding)
291                 .withSourceDirectories(sourceDirectories)
292                 .withScanForSources(scanForSources)
293                 .createSourceLoader();
294     }
295 
296     /**
297      * Creates the environment.
298      *
299      * @return environment to setup mojo and service specific properties
300      */
301     protected Environment createEnvironment() {
302         return new Environment(this, getServices());
303     }
304 
305     /**
306      * Gets the services.
307      *
308      * @return list of available continuous integration services
309      */
310     protected List<ServiceSetup> getServices() {
311         Map<String, String> env = System.getenv();
312         List<ServiceSetup> services = new ArrayList<>();
313         services.add(new Shippable(env));
314         services.add(new Travis(env));
315         services.add(new Circle(env));
316         services.add(new Jenkins(env));
317         services.add(new Bamboo(env));
318         services.add(new Appveyor(env));
319         services.add(new Wercker(env));
320         services.add(new General(env));
321         return services;
322     }
323 
324     /**
325      * Creates the job.
326      *
327      * @return job that describes the coveralls report
328      * @throws ProcessingException if processing of timestamp fails
329      * @throws IOException if an I/O error occurs
330      */
331     protected Job createJob() throws ProcessingException, IOException {
332         Git git = new GitRepository(basedir).load();
333         Date time = new TimestampParser(timestampFormat).parse(timestamp);
334         return new Job()
335             .withRepoToken(repoToken)
336             .withServiceName(serviceName)
337             .withServiceJobId(serviceJobId)
338             .withServiceBuildNumber(serviceBuildNumber)
339             .withServiceBuildUrl(serviceBuildUrl)
340             .withParallel(parallel)
341             .withServiceEnvironment(serviceEnvironment)
342             .withDryRun(dryRun)
343             .withBranch(branch)
344             .withPullRequest(pullRequest)
345             .withTimestamp(time)
346             .withGit(git);
347     }
348 
349     /**
350      * Creates the json writer.
351      *
352      * @param job the job describing the coveralls report
353      * @return JSON writer that writes the coveralls data
354      * @throws IOException if an I/O error occurs
355      */
356     protected JsonWriter createJsonWriter(final Job job) throws IOException {
357         return new JsonWriter(job, coverallsFile);
358     }
359 
360     /**
361      * Creates the coveralls client.
362      *
363      * @return http client that submits the coveralls data
364      */
365     protected CoverallsClient createCoverallsClient() {
366         return new CoverallsProxyClient(coverallsUrl, settings.getActiveProxy());
367     }
368 
369     /**
370      * Creates the source callback chain.
371      *
372      * @param writer the JSON writer
373      * @param reporters the logging reporters
374      * @return source callback chain for different source handlers
375      */
376     protected SourceCallback createSourceCallbackChain(final JsonWriter writer, final List<Logger> reporters) {
377         SourceCallback chain = writer;
378         if (getLog().isInfoEnabled()) {
379             CoverageTracingLogger coverageTracingReporter = new CoverageTracingLogger(chain);
380             chain = coverageTracingReporter;
381             reporters.add(coverageTracingReporter);
382         }
383         chain = new UniqueSourceCallback(chain);
384         return chain;
385     }
386 
387     /**
388      * Writes coverage data to JSON file.
389      *
390      * @param writer JSON writer that writes the coveralls data
391      * @param sourceCallback the source callback handler
392      * @param parsers list of coverage parsers
393      * @throws ProcessingException if process to to create JSON file fails
394      * @throws IOException if an I/O error occurs
395      */
396     protected void writeCoveralls(final JsonWriter writer, final SourceCallback sourceCallback, final List<CoverageParser> parsers) throws ProcessingException, IOException {
397         try {
398             getLog().info("Writing Coveralls data to " + writer.getCoverallsFile().getAbsolutePath() + "...");
399             long now = System.currentTimeMillis();
400             sourceCallback.onBegin();
401             for (CoverageParser parser : parsers) {
402                 getLog().info("Processing coverage report from " + parser.getCoverageFile().getAbsolutePath());
403                 parser.parse(sourceCallback);
404             }
405             sourceCallback.onComplete();
406             long duration = System.currentTimeMillis() - now;
407             getLog().info("Successfully wrote Coveralls data in " + duration + "ms");
408         } finally {
409             writer.close();
410         }
411     }
412 
413     private void submitData(final CoverallsClient client, final File coverallsFile) throws ProcessingException, IOException {
414         getLog().info("Submitting Coveralls data to API");
415         long now = System.currentTimeMillis();
416         try {
417             CoverallsResponse response = client.submit(coverallsFile);
418             long duration = System.currentTimeMillis() - now;
419             getLog().info("Successfully submitted Coveralls data in " + duration + "ms for " + response.getMessage());
420             getLog().info(response.getUrl());
421             getLog().info("*** It might take hours for Coveralls to update the actual coverage numbers for a job");
422             getLog().info("    If you see question marks in the report, please be patient");
423         } catch (ProcessingException ex) {
424             long duration = System.currentTimeMillis() - now;
425             String message = "Submission failed in " + duration + "ms while processing data";
426             handleSubmissionError(ex, message, true);
427         } catch (IOException ex) {
428             long duration = System.currentTimeMillis() - now;
429             String message = "Submission failed in " + duration + "ms while handling I/O operations";
430             handleSubmissionError(ex, message, failOnServiceError);
431         }
432     }
433 
434     private <T extends Exception> void handleSubmissionError(final T ex, final String message, final boolean failOnException) throws T {
435         if (failOnException) {
436             getLog().error(message);
437             throw ex;
438         } else {
439             getLog().warn(message);
440         }
441     }
442 
443     private void report(final List<Logger> reporters, final Position position) {
444         for (Logger reporter : reporters) {
445             if (position.equals(reporter.getPosition())) {
446                 reporter.log(getLog());
447             }
448         }
449     }
450 }