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.parser;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Set;
34  
35  import org.eluder.coveralls.maven.plugin.CoverageFixture;
36  import org.eluder.coveralls.maven.plugin.CoverageParser;
37  import org.eluder.coveralls.maven.plugin.ProcessingException;
38  import org.eluder.coveralls.maven.plugin.domain.Branch;
39  import org.eluder.coveralls.maven.plugin.domain.Source;
40  import org.eluder.coveralls.maven.plugin.source.ChainingSourceCallback;
41  import org.eluder.coveralls.maven.plugin.source.SourceCallback;
42  import org.eluder.coveralls.maven.plugin.source.SourceLoader;
43  import org.eluder.coveralls.maven.plugin.source.UniqueSourceCallback;
44  import org.eluder.coveralls.maven.plugin.util.TestIoUtil;
45  import org.junit.jupiter.api.Assertions;
46  import org.junit.jupiter.api.BeforeEach;
47  import org.junit.jupiter.api.Test;
48  import org.junit.jupiter.api.extension.ExtendWith;
49  import org.mockito.ArgumentCaptor;
50  import org.mockito.Mock;
51  import org.mockito.Mockito;
52  import org.mockito.junit.jupiter.MockitoExtension;
53  import org.mockito.stubbing.Answer;
54  
55  /**
56   * The Class AbstractCoverageParserTest.
57   */
58  @ExtendWith(MockitoExtension.class)
59  public abstract class AbstractCoverageParserTest {
60  
61      /** The source loader mock. */
62      @Mock
63      SourceLoader sourceLoaderMock;
64  
65      /** The source callback mock. */
66      @Mock
67      SourceCallback sourceCallbackMock;
68  
69      /**
70       * Instantiates a new abstract coverage parser test.
71       */
72      public AbstractCoverageParserTest() {
73          // Do Nothing
74      }
75  
76      /**
77       * Inits the Abstract Coverage Parser.
78       *
79       * @throws IOException
80       *             Signals that an I/O exception has occurred.
81       */
82      @BeforeEach
83      void init() throws IOException {
84          for (final List<String> coverageFile : this.getCoverageFixture()) {
85              final var name = this.sourceName(coverageFile.get(0));
86              final var content = TestIoUtil.readFileContent(TestIoUtil.getFile(name));
87              Mockito.lenient().when(this.sourceLoaderMock.load(name)).then(this.sourceAnswer(name, content));
88          }
89      }
90  
91      /**
92       * Source name.
93       *
94       * @param coverageFile
95       *            the coverage file
96       *
97       * @return the string
98       */
99      String sourceName(final String coverageFile) {
100         return coverageFile;
101     }
102 
103     /**
104      * Source answer.
105      *
106      * @param name
107      *            the name
108      * @param content
109      *            the content
110      *
111      * @return the answer
112      */
113     Answer<Source> sourceAnswer(final String name, final String content) {
114         return invocation -> new Source(name, content, TestIoUtil.getSha512DigestHex(content));
115     }
116 
117     /**
118      * Parses the coverage.
119      *
120      * @throws ProcessingException
121      *             the processing exception
122      * @throws IOException
123      *             Signals that an I/O exception has occurred.
124      */
125     @Test
126     void parseCoverage() throws ProcessingException, IOException {
127         for (final String coverageResource : this.getCoverageResources()) {
128             final var parser = this.createCoverageParser(TestIoUtil.getFile(coverageResource), this.sourceLoaderMock);
129             parser.parse(this.sourceCallbackMock);
130         }
131 
132         final var fixture = this.getCoverageFixture();
133 
134         final ArgumentCaptor<Source> captor = ArgumentCaptor.forClass(Source.class);
135         Mockito.verify(this.sourceCallbackMock, Mockito.atLeast(CoverageFixture.getTotalFiles(fixture)))
136                 .onSource(captor.capture());
137 
138         final var sourceCollector = new SourceCollector();
139         final var uniqueSourceCallback = new UniqueSourceCallback(sourceCollector);
140         final var classifierRemover = new ClassifierRemover(uniqueSourceCallback);
141         classifierRemover.onBegin();
142         for (final Source source : captor.getAllValues()) {
143             classifierRemover.onSource(source);
144         }
145         classifierRemover.onComplete();
146 
147         for (final List<String> coverageFile : fixture) {
148             AbstractCoverageParserTest.assertCoverage(sourceCollector.sources, coverageFile.get(0),
149                     Integer.parseInt(coverageFile.get(1)), this.toIntegerSet(coverageFile.get(2)),
150                     this.toIntegerSet(coverageFile.get(3)), this.toIntegerSet(coverageFile.get(4)),
151                     this.toIntegerSet(coverageFile.get(5)));
152         }
153     }
154 
155     /**
156      * Creates the coverage parser.
157      *
158      * @param coverageFile
159      *            the coverage file
160      * @param sourceLoader
161      *            the source loader
162      *
163      * @return the coverage parser
164      */
165     protected abstract CoverageParser createCoverageParser(File coverageFile, SourceLoader sourceLoader);
166 
167     /**
168      * Gets the coverage resources.
169      *
170      * @return the coverage resources
171      */
172     protected abstract List<String> getCoverageResources();
173 
174     /**
175      * Gets the coverage fixture.
176      *
177      * @return the coverage fixture
178      */
179     protected abstract List<List<String>> getCoverageFixture();
180 
181     /**
182      * To integer set.
183      *
184      * @param commaSeparated
185      *            the comma separated
186      *
187      * @return the sets the
188      */
189     Set<Integer> toIntegerSet(final String commaSeparated) {
190         if (commaSeparated.isEmpty()) {
191             return Set.of();
192         }
193         final var split = commaSeparated.split(",", -1);
194         final Set<Integer> values = new HashSet<>();
195         for (final String value : split) {
196             values.add(Integer.valueOf(value));
197         }
198         return values;
199     }
200 
201     /**
202      * The Class SourceCollector.
203      */
204     static class SourceCollector implements SourceCallback {
205 
206         /** The sources. */
207         private final List<Source> sources = new ArrayList<>();
208 
209         @Override
210         public void onBegin() {
211             // Does nothing
212         }
213 
214         @Override
215         public void onSource(Source source) {
216             this.sources.add(source);
217         }
218 
219         @Override
220         public void onComplete() {
221             // Does Nothing
222         }
223     }
224 
225     /**
226      * The Class ClassifierRemover.
227      */
228     static class ClassifierRemover extends ChainingSourceCallback {
229 
230         /**
231          * Instantiates a new classifier remover.
232          *
233          * @param chained
234          *            the chained
235          */
236         public ClassifierRemover(SourceCallback chained) {
237             super(chained);
238         }
239 
240         @Override
241         protected void onSourceInternal(Source source) {
242             source.setClassifier(null);
243         }
244     }
245 
246     /**
247      * Assert coverage.
248      *
249      * @param sources
250      *            the sources
251      * @param name
252      *            the name
253      * @param lines
254      *            the lines
255      * @param coveredLines
256      *            the covered lines
257      * @param missedLines
258      *            the missed lines
259      * @param coveredBranches
260      *            the covered branches
261      * @param missedBranches
262      *            the missed branches
263      */
264     static void assertCoverage(final Collection<Source> sources, final String name, final int lines,
265             final Set<Integer> coveredLines, final Set<Integer> missedLines, final Set<Integer> coveredBranches,
266             final Set<Integer> missedBranches) {
267 
268         Source tested = null;
269         for (final Source source : sources) {
270             if (source.getName().endsWith(name)) {
271                 tested = source;
272                 break;
273             }
274         }
275         if (tested == null) {
276             Assertions.fail("Expected source " + name + " not found from coverage report");
277         }
278         if (tested.getCoverage().length != lines) {
279             Assertions.fail("Expected " + lines + " lines for " + name + " was " + tested.getCoverage().length);
280         }
281         for (var i = 0; i < tested.getCoverage().length; i++) {
282             final var lineNumber = i + 1;
283             final var message = name + " line " + lineNumber + " coverage";
284             if (coveredLines.contains(lineNumber)) {
285                 Assertions.assertTrue(tested.getCoverage()[i] > 0, message);
286             } else if (missedLines.contains(lineNumber)) {
287                 Assertions.assertEquals(0, tested.getCoverage()[i], message);
288             } else {
289                 Assertions.assertNull(tested.getCoverage()[i], message);
290             }
291         }
292         for (final Branch b : tested.getBranchesList()) {
293             final var message = name + " branch " + b.getBranchNumber() + " coverage in line " + b.getLineNumber();
294             if (b.getHits() > 0) {
295                 Assertions.assertTrue(coveredBranches.contains(b.getLineNumber()), message);
296             } else {
297                 Assertions.assertTrue(missedBranches.contains(b.getLineNumber()), message);
298             }
299         }
300     }
301 
302 }