• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spotbugs / spotbugs-maven-plugin / 2598

19 Aug 2025 03:52PM UTC coverage: 22.863% (-0.03%) from 22.888%
2598

Pull #1139

github

web-flow
Merge 2dc9036c3 into ebe4dbfe3
Pull Request #1139: Cleanup path checks for clarity

93 of 760 branches covered (12.24%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

335 of 1112 relevant lines covered (30.13%)

0.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.18
/src/main/groovy/org/codehaus/mojo/spotbugs/SpotbugsReportGenerator.groovy
1
/*
2
 * Copyright 2005-2025 the original author or authors.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package org.codehaus.mojo.spotbugs
17

18
import groovy.xml.slurpersupport.GPathResult
19
import groovy.xml.slurpersupport.NodeChild
20
import java.nio.file.Files
21
import java.nio.file.Path
22
import java.util.regex.Pattern
23
import java.util.regex.Matcher
24

25
import org.apache.maven.doxia.markup.HtmlMarkup
26
import org.apache.maven.doxia.sink.Sink
27
import org.apache.maven.doxia.sink.SinkEventAttributes
28
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet
29
import org.apache.maven.plugin.logging.Log
30

31
import org.codehaus.plexus.util.PathTool
32

33
/**
34
 * The reporter controls the generation of the SpotBugs report. It contains call back methods which gets called by
35
 * SpotBugs if a bug is found.
36
 */
37
class SpotbugsReportGenerator implements SpotBugsInfo {
38

39
    /** The key to get the value if the line number is not available. */
40
    static final String NOLINE_KEY = 'report.spotbugs.noline'
41

42
    /** The key to get the column title for the line. */
43
    static final String COLUMN_LINE_KEY = 'report.spotbugs.column.line'
44

45
    /** The key to get the column title for the bug. */
46
    static final String COLUMN_BUG_KEY = 'report.spotbugs.column.bug'
47

48
    /** The key to get the column title for the bugs. */
49
    static final String COLUMN_BUGS_KEY = 'report.spotbugs.column.bugs'
50

51
    /** The key to get the column title for the category. */
52
    static final String COLUMN_CATEGORY_KEY = 'report.spotbugs.column.category'
53

54
    /** The key to get the column title for the priority. */
55
    static final String COLUMN_PRIORITY_KEY = 'report.spotbugs.column.priority'
56

57
    /** The key to get the column title for the details. */
58
    static final String COLUMN_DETAILS_KEY = 'report.spotbugs.column.details'
59

60
    /** The key to get the report title of the Plug-In from the bundle. */
61
    static final String REPORT_TITLE_KEY = 'report.spotbugs.reporttitle'
62

63
    /** The key to get the report link title of the Plug-In from the bundle. */
64
    static final String LINKTITLE_KEY = 'report.spotbugs.linktitle'
65

66
    /** The key to get the report link of the Plug-In from the bundle. */
67
    static final String LINK_KEY = 'report.spotbugs.link'
68

69
    /** The key to get the files title of the Plug-In from the bundle. */
70
    static final String FILES_KEY = 'report.spotbugs.files'
71

72
    /** The key to get the threshold of the report from the bundle. */
73
    static final String THRESHOLD_KEY = 'report.spotbugs.threshold'
74

75
    /** The key to get the effort of the report from the bundle. */
76
    static final String EFFORT_KEY = 'report.spotbugs.effort'
77

78
    /** The key to get the link to SpotBugs description page from the bundle. */
79
    static final String DETAILSLINK_KEY = 'report.spotbugs.detailslink'
80

81
    /** The key to get the version title for SpotBugs from the bundle. */
82
    static final String VERSIONTITLE_KEY = 'report.spotbugs.versiontitle'
83

84
    /** The key to get the files title of the Plug-In from the bundle. */
85
    static final String SUMMARY_KEY = 'report.spotbugs.summary'
86

87
    /** The key to column title for the Class. */
88
    static final String COLUMN_CLASS_KEY = 'report.spotbugs.column.class'
89

90
    /** The key to column title for the Classes. */
91
    static final String COLUMN_CLASSES_KEY = 'report.spotbugs.column.classes'
92

93
    /** The key to column title for the errors. */
94
    static final String COLUMN_ERRORS_KEY = 'report.spotbugs.column.errors'
95

96
    /**
97
     * The key to column title for the files.
98
     *
99
     * note: not used but throughout properties
100
     */
101
    static final String COLUMN_FILES_KEY = 'report.spotbugs.column.files'
102

103
    /** The key to column title for the files. */
104
    static final String COLUMN_MISSINGCLASSES_KEY = 'report.spotbugs.column.missingclasses'
105

106
    /** The sink to write the report to. */
107
    Sink sink
108

109
    /** The bundle to get the messages from. */
110
    ResourceBundle bundle
111

112
    /** The logger to write logs to. */
113
    Log log
114

115
    /** The threshold of bugs severity. */
116
    String threshold
117

118
    /** The used effort for searching bugs. */
119
    String effort
120

121
    /** The name of the current class which is analysed by SpotBugs. */
122
    String currentClassName = ''
123

124
    /** Signals if the jxr report plugin is enabled. */
125
    boolean isJXRReportEnabled
126

127
    /** The running total of bugs reported. */
128
    int bugCount
129

130
    /** The running total of missing classes reported. */
131
    int missingClassCount
132

133
    /** The running total of files analyzed. */
134
    int fileCount
135

136
    /** The Set of missing classes names reported. */
137
    Set missingClassSet = new HashSet()
1✔
138

139
    /** The running total of errors reported. */
140
    int errorCount
141

142
    /** Location where generated html will be created. */
143
    File outputDirectory
144

145
    /** Location of the Xrefs to link to. */
146
    File xrefLocation
147

148
    /** Location of the Test Xrefs to link to. */
149
    File xrefTestLocation
150

151
    /** The directories containing the sources to be compiled. */
152
    List<String> compileSourceRoots
153

154
    /** The directories containing the test-sources to be compiled. */
155
    List<String> testSourceRoots
156

157
    /** Run Spotbugs on the tests. */
158
    boolean includeTests
159

160
    /** Spotbugs results. */
161
    GPathResult spotbugsResults
162

163
    /** Bug classes. */
164
    List<String> bugClasses = []
1✔
165

166
    /** Pre-compiled pattern for removing inner class suffixes. */
167
    private static final Pattern INNER_CLASS_PATTERN = Pattern.compile('\$.*')
1✔
168

169
    /**
170
     * Default constructor.
171
     *
172
     * @param sink
173
     *            The sink to generate the report.
174
     * @param bundle
175
     *            The resource bundle to get the messages from.
176
     */
177
    SpotbugsReportGenerator(Sink sink, ResourceBundle bundle) {
1✔
178
        if (!sink || !bundle) {
1!
179
            throw new IllegalArgumentException('All constructor arguments must be provided')
×
180
        }
181

182
        this.sink = sink
1✔
183
        this.bundle = bundle
1✔
184
    }
1✔
185

186
    /**
187
     * @see edu.umd.cs.findbugs.BugReporter#finish()
188
     */
189
    void printBody() {
190
        if (log.isDebugEnabled()) {
1!
191
            log.debug('Finished searching for bugs!...')
×
192
            log.debug('sink is ' + sink)
×
193
        }
194

195
        bugClasses.each() { String bugClass ->
1✔
196
            if (log.isDebugEnabled()) {
1!
197
                log.debug("finish bugClass is ${bugClass}")
×
198
            }
199

200
            printBug(bugClass)
1✔
201
        }
202

203
        // close the report, write it
204
        sink.body_()
1✔
205
    }
1✔
206

207
    /**
208
     * Prints the top header sections of the report.
209
     */
210
    private void doHeading() {
211
        sink.head()
1✔
212
        sink.title()
1✔
213
        sink.text(getReportTitle())
1✔
214
        sink.title_()
1✔
215
        sink.head_()
1✔
216

217
        sink.body()
1✔
218

219
        // the title of the report
220
        sink.section1()
1✔
221
        sink.sectionTitle1()
1✔
222
        sink.text(getReportTitle())
1✔
223
        sink.sectionTitle1_()
1✔
224

225
        // information about SpotBugs
226
        sink.paragraph()
1✔
227
        sink.text(bundle.getString(LINKTITLE_KEY) + SpotBugsInfo.BLANK)
1✔
228
        sink.link(bundle.getString(LINK_KEY))
1✔
229
        sink.text(bundle.getString(SpotBugsInfo.NAME_KEY))
1✔
230
        sink.link_()
1✔
231
        sink.paragraph_()
1✔
232

233
        sink.paragraph()
1✔
234
        sink.text(bundle.getString(VERSIONTITLE_KEY) + SpotBugsInfo.BLANK)
1✔
235
        sink.italic()
1✔
236
        sink.text(edu.umd.cs.findbugs.Version.VERSION_STRING)
1✔
237
        sink.italic_()
1✔
238
        sink.paragraph_()
1✔
239

240
        sink.paragraph()
1✔
241
        sink.text(bundle.getString(THRESHOLD_KEY) + SpotBugsInfo.BLANK)
1✔
242
        sink.italic()
1✔
243
        sink.text(SpotBugsInfo.spotbugsThresholds.get(threshold))
1✔
244
        sink.italic_()
1✔
245
        sink.paragraph_()
1✔
246

247
        sink.paragraph()
1✔
248
        sink.text(bundle.getString(EFFORT_KEY) + SpotBugsInfo.BLANK)
1✔
249
        sink.italic()
1✔
250
        sink.text(SpotBugsInfo.spotbugsEfforts.get(effort))
1✔
251
        sink.italic_()
1✔
252
        sink.paragraph_()
1✔
253
        sink.section1_()
1✔
254
    }
1✔
255

256
    /**
257
     * Print the bug collection to a line in the table
258
     *
259
     * @param bugInstance
260
     *            the bug to print
261
     */
262
    protected void printBug(String bugClass) {
263
        if (log.isDebugEnabled()) {
1!
264
            log.debug("printBug bugClass is ${bugClass}")
×
265
        }
266

267
        openClassReportSection(bugClass)
1✔
268

269
        if (log.isDebugEnabled()) {
1!
270
            log.debug("printBug spotbugsResults is ${spotbugsResults}")
×
271
        }
272

273
        spotbugsResults.BugInstance.each() { GPathResult bugInstance ->
1✔
274

275
            if (log.isDebugEnabled()) {
1!
276
                log.debug("bugInstance --->  ${bugInstance}")
×
277
            }
278

279
            if (bugInstance.Class[0].@classname.text() != bugClass) {
1!
280
                return
×
281
            }
282

283
            String type = bugInstance.@type.text()
1✔
284
            String category = bugInstance.@category.text()
1✔
285
            String message = bugInstance.LongMessage.text()
1✔
286
            String priority = bugInstance.@priority.text()
1✔
287
            GPathResult line = bugInstance.SourceLine[0]
1✔
288
            if (log.isDebugEnabled()) {
1!
289
                log.debug("BugInstance message is ${message}")
×
290
            }
291

292
            sink.tableRow()
1✔
293

294
            // bug
295
            sink.tableCell()
1✔
296
            sink.text(message)
1✔
297
            sink.tableCell_()
1✔
298

299
            // category
300
            sink.tableCell()
1✔
301
            sink.text(category)
1✔
302
            sink.tableCell_()
1✔
303

304
            // description link
305
            sink.tableCell()
1✔
306
            sink.link(bundle.getString(DETAILSLINK_KEY) + "#" + type)
1✔
307
            sink.text(type)
1✔
308
            sink.link_()
1✔
309
            sink.tableCell_()
1✔
310

311
            // line
312
            sink.tableCell()
1✔
313

314
            if (isJXRReportEnabled) {
1!
315
                log.debug('isJXRReportEnabled is enabled')
×
316
                sink.rawText(assembleJxrHyperlink(line))
×
317
            } else {
318
                sink.text(line.@start.text())
1✔
319
            }
320

321
            sink.tableCell_()
1✔
322

323
            // priority
324
            sink.tableCell()
1✔
325
            sink.text(spotbugsPriority[priority as Integer])
1✔
326
            sink.tableCell_()
1✔
327

328
            sink.tableRow_()
1✔
329
        }
330

331
        sink.tableRows_()
1✔
332
        sink.table_()
1✔
333

334
        sink.section2_()
1✔
335
    }
1✔
336

337
    /**
338
     * Assembles the hyperlink to point to the source code.
339
     *
340
     * @param line
341
     *            The line number object with the bug.
342
     * @param lineNumber
343
     *            The line number to show in the hyperlink.
344
     * @return The hyperlink which points to the code.
345
     *
346
     */
347
    protected String assembleJxrHyperlink(GPathResult line) {
348
        if (log.isDebugEnabled()) {
×
349
            log.debug('Inside assembleJxrHyperlink')
×
350
            log.debug('line is ' + line.text())
×
351
            log.debug('outputDirectory is ' + outputDirectory.getAbsolutePath())
×
352
            log.debug('xrefLocation is ' + xrefLocation.getAbsolutePath())
×
353
            log.debug('xrefTestLocation is ' + xrefTestLocation.getAbsolutePath())
×
354
        }
355

356
        String prefix
×
357
        compileSourceRoots.each { String compileSourceRoot ->
×
NEW
358
            Path sourcePath = Path.of(compileSourceRoot + File.separator + line.@sourcepath.text())
×
NEW
359
            if (Files.notExists(sourcePath)) {
×
UNCOV
360
                return
×
361
            }
362
            prefix = PathTool.getRelativePath(outputDirectory.getAbsolutePath(), xrefLocation.getAbsolutePath())
×
363
            prefix = prefix ? prefix + SpotBugsInfo.URL_SEPARATOR + xrefLocation.getName() +
×
364
                SpotBugsInfo.URL_SEPARATOR : SpotBugsInfo.PERIOD
365
        }
366

367
        if (includeTests && !prefix) {
×
368
            testSourceRoots.each { String testSourceRoot ->
×
NEW
369
                Path testSourcePath = Path.of(testSourceRoot + File.separator + line.@sourcepath.text())
×
NEW
370
                if (Files.notExists(testSourcePath)) {
×
UNCOV
371
                    return
×
372
                }
373
                prefix = PathTool.getRelativePath(outputDirectory.getAbsolutePath(), xrefTestLocation.getAbsolutePath())
×
374
                prefix = prefix ? prefix + SpotBugsInfo.URL_SEPARATOR + xrefTestLocation.getName() +
×
375
                    SpotBugsInfo.URL_SEPARATOR : SpotBugsInfo.PERIOD
376
            }
377
        }
378

379
        String className = line.@classname.text().replace('.', '/')
×
380
        Matcher matcher = INNER_CLASS_PATTERN.matcher(className)
×
381
        String cleanClassName = matcher.replaceFirst('')
×
382
        String path = prefix + cleanClassName
×
383
        String lineNumber = valueForLine(line)
×
384

385
        String hyperlink
×
386
        if (lineNumber != bundle.getString(NOLINE_KEY)) {
×
387
            hyperlink = '<a href="' + path + '.html#L' + line.@start.text() + '">' + lineNumber + '</a>'
×
388
        } else {
389
            hyperlink = lineNumber
×
390
        }
391

392
        return hyperlink
×
393
    }
394

395
    /**
396
     * Gets the report title.
397
     *
398
     * @return The report title.
399
     *
400
     */
401
    protected String getReportTitle() {
402
        return bundle.getString(REPORT_TITLE_KEY)
1✔
403
    }
404

405
    /**
406
     * Initialized a bug report section in the report for a particular class.
407
     */
408
    protected void openClassReportSection(String bugClass) {
409
        String columnBugText = bundle.getString(COLUMN_BUG_KEY)
1✔
410
        String columnBugCategory = bundle.getString(COLUMN_CATEGORY_KEY)
1✔
411
        String columnDescriptionLink = bundle.getString(COLUMN_DETAILS_KEY)
1✔
412
        String columnLineText = bundle.getString(COLUMN_LINE_KEY)
1✔
413
        String priorityText = bundle.getString(COLUMN_PRIORITY_KEY)
1✔
414

415
        if (log.isDebugEnabled()) {
1!
416
            log.debug("openClassReportSection bugClass is ${bugClass}")
×
417
            log.debug('Opening Class Report Section')
×
418
        }
419

420
        // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
421
        // https://github.com/spotbugs/spotbugs-maven-plugin/issues/236
422
        sink.unknown(HtmlMarkup.A.toString(), [HtmlMarkup.TAG_TYPE_START] as Object[],
1✔
423
            new SinkEventAttributeSet(SinkEventAttributes.NAME, bugClass))
1✔
424
        sink.unknown(HtmlMarkup.A.toString(), [HtmlMarkup.TAG_TYPE_END] as Object[], null)
1✔
425

426
        sink.section2()
1✔
427
        sink.sectionTitle2()
1✔
428
        sink.text(bugClass)
1✔
429
        sink.sectionTitle2_()
1✔
430
        sink.table()
1✔
431
        sink.tableRows(null, false)
1✔
432
        sink.tableRow()
1✔
433

434
        // bug
435
        sink.tableHeaderCell()
1✔
436
        sink.text(columnBugText)
1✔
437
        sink.tableHeaderCell_()
1✔
438

439
        // category
440
        sink.tableHeaderCell()
1✔
441
        sink.text(columnBugCategory)
1✔
442
        sink.tableHeaderCell_()
1✔
443

444
        // description link
445
        sink.tableHeaderCell()
1✔
446
        sink.text(columnDescriptionLink)
1✔
447
        sink.tableHeaderCell_()
1✔
448

449
        // line
450
        sink.tableHeaderCell()
1✔
451
        sink.text(columnLineText)
1✔
452
        sink.tableHeaderCell_()
1✔
453

454
        // priority
455
        sink.tableHeaderCell()
1✔
456
        sink.text(priorityText)
1✔
457
        sink.tableHeaderCell_()
1✔
458

459
        sink.tableRow_()
1✔
460
    }
1✔
461

462
    /**
463
     * Print the Summary Section.
464
     */
465
    protected void printSummary() {
466
        log.debug('Entering printSummary')
1✔
467

468
        sink.section1()
1✔
469

470
        // the summary section
471
        sink.sectionTitle1()
1✔
472
        sink.text(bundle.getString(SUMMARY_KEY))
1✔
473
        sink.sectionTitle1_()
1✔
474

475
        sink.table()
1✔
476
        sink.tableRows(null, false)
1✔
477
        sink.tableRow()
1✔
478

479
        // classes
480
        sink.tableHeaderCell()
1✔
481
        sink.text(bundle.getString(COLUMN_CLASSES_KEY))
1✔
482
        sink.tableHeaderCell_()
1✔
483

484
        // bugs
485
        sink.tableHeaderCell()
1✔
486
        sink.text(bundle.getString(COLUMN_BUGS_KEY))
1✔
487
        sink.tableHeaderCell_()
1✔
488

489
        // Errors
490
        sink.tableHeaderCell()
1✔
491
        sink.text(bundle.getString(COLUMN_ERRORS_KEY))
1✔
492
        sink.tableHeaderCell_()
1✔
493

494
        // Missing Classes
495
        sink.tableHeaderCell()
1✔
496
        sink.text(bundle.getString(COLUMN_MISSINGCLASSES_KEY))
1✔
497
        sink.tableHeaderCell_()
1✔
498

499
        sink.tableRow_()
1✔
500

501
        sink.tableRow()
1✔
502

503
        // files
504
        sink.tableCell()
1✔
505
        sink.text(spotbugsResults.FindBugsSummary.@total_classes.text())
1✔
506
        sink.tableCell_()
1✔
507

508
        // bug
509
        sink.tableCell()
1✔
510
        sink.text(spotbugsResults.FindBugsSummary.@total_bugs.text())
1✔
511
        sink.tableCell_()
1✔
512

513
        // Errors
514
        sink.tableCell()
1✔
515
        sink.text(spotbugsResults.Errors.@errors.text())
1✔
516
        sink.tableCell_()
1✔
517

518
        // Missing Classes
519
        sink.tableCell()
1✔
520
        sink.text(spotbugsResults.Errors.@missingClasses.text())
1✔
521
        sink.tableCell_()
1✔
522

523
        sink.tableRow_()
1✔
524
        sink.tableRows_()
1✔
525
        sink.table_()
1✔
526

527
        sink.section1_()
1✔
528

529
        log.debug('Exiting printSummary')
1✔
530
    }
1✔
531

532
    /**
533
     * Print the File Summary Section.
534
     */
535
    protected void printFilesSummary() {
536
        log.debug('Entering printFilesSummary')
1✔
537

538
        sink.section1()
1✔
539

540
        // the Files section
541
        sink.sectionTitle1()
1✔
542
        sink.text(bundle.getString(FILES_KEY))
1✔
543
        sink.sectionTitle1_()
1✔
544

545
        /**
546
         * Class Summary
547
         */
548
        sink.table()
1✔
549
        sink.tableRows(null, false)
1✔
550
        sink.tableRow()
1✔
551

552
        // files
553
        sink.tableHeaderCell()
1✔
554
        sink.text(bundle.getString(COLUMN_CLASS_KEY))
1✔
555
        sink.tableHeaderCell_()
1✔
556

557
        // bugs
558
        sink.tableHeaderCell()
1✔
559
        sink.text(bundle.getString(COLUMN_BUGS_KEY))
1✔
560
        sink.tableHeaderCell_()
1✔
561

562
        sink.tableRow_()
1✔
563

564
        spotbugsResults.FindBugsSummary.PackageStats.ClassStats.each() { NodeChild classStats ->
1✔
565

566
            String classStatsValue = classStats.'@class'.text()
1✔
567
            String classStatsBugCount = classStats.'@bugs'.text()
1✔
568

569
            if (Integer.parseInt(classStatsBugCount) == 0) {
1!
570
                return
×
571
            }
572

573
            sink.tableRow()
1✔
574

575
            // class name
576
            sink.tableCell()
1✔
577
            sink.link("#" + classStatsValue)
1✔
578
            sink.text(classStatsValue)
1✔
579
            sink.link_()
1✔
580
            sink.tableCell_()
1✔
581

582
            // class bug total count
583
            sink.tableCell()
1✔
584
            sink.text(classStatsBugCount)
1✔
585
            sink.tableCell_()
1✔
586

587
            sink.tableRow_()
1✔
588

589
            bugClasses << classStatsValue
1✔
590
        }
591

592
        sink.tableRows_()
1✔
593
        sink.table_()
1✔
594

595
        sink.section1_()
1✔
596

597
        log.debug("Exiting printFilesSummary")
1✔
598
    }
1✔
599

600
    public void generateReport() {
601
        if (log.isDebugEnabled()) {
1!
602
            log.debug('Reporter Locale is ' + this.bundle.getLocale().getLanguage())
×
603
        }
604

605
        doHeading()
1✔
606

607
        printSummary()
1✔
608

609
        printFilesSummary()
1✔
610

611
        printBody()
1✔
612

613
        log.debug('Closing up report....................')
1✔
614

615
        sink.flush()
1✔
616
        sink.close()
1✔
617
    }
1✔
618

619
    /**
620
     * Return the value to display. If SpotBugs does not provide a line number, a default message is returned. The line
621
     * number otherwise.
622
     *
623
     * @param line
624
     *            The line to get the value from.
625
     * @return The line number the bug appears or a statement that there is no source line available.
626
     *
627
     */
628
    protected String valueForLine(GPathResult line) {
629
        String value
×
630

631
        if (line) {
×
632
            String startLine = line.@start.text()
×
633
            String endLine = line.@end.text()
×
634

635
            if (startLine == endLine) {
×
636
                if (startLine) {
×
637
                    value = startLine
×
638
                } else {
639
                    value = bundle.getString(NOLINE_KEY)
×
640
                }
641
            } else {
642
                value = startLine + '-' + endLine
×
643
            }
644
        } else {
645
            value = bundle.getString(NOLINE_KEY)
×
646
        }
647

648
        return value
×
649
    }
650
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc