Skip to content

Commit c9aedca

Browse files
authored
QUA-948: Support .NET DotCover coverage reporting (#508)
1 parent da0995a commit c9aedca

File tree

8 files changed

+254
-2
lines changed

8 files changed

+254
-2
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ test-cobertura:
143143
test-excoveralls:
144144
docker build -f integration-tests/excoveralls/Dockerfile .
145145

146+
test-dotcover:
147+
docker build -f integration-tests/dotcover/Dockerfile .
148+
146149
publish-head:
147150
$(call upload_artifacts,head)
148151

cmd/format-coverage.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/codeclimate/test-reporter/formatters/clover"
1212
"github.com/codeclimate/test-reporter/formatters/cobertura"
1313
"github.com/codeclimate/test-reporter/formatters/coveragepy"
14+
"github.com/codeclimate/test-reporter/formatters/dotcover"
1415
"github.com/codeclimate/test-reporter/formatters/excoveralls"
1516
"github.com/codeclimate/test-reporter/formatters/gcov"
1617
"github.com/codeclimate/test-reporter/formatters/gocov"
@@ -37,7 +38,7 @@ type CoverageFormatter struct {
3738
var formatOptions = CoverageFormatter{}
3839

3940
// a prioritized list of the formatters to use
40-
var formatterList = []string{"clover", "cobertura", "coverage.py", "excoveralls", "gcov", "gocov", "jacoco", "lcov", "lcov-json", "simplecov", "xccov"}
41+
var formatterList = []string{"clover", "cobertura", "coverage.py", "excoveralls", "gcov", "gocov", "jacoco", "lcov", "lcov-json", "simplecov", "xccov", "dotcover"}
4142

4243
// a map of the formatters to use
4344
var formatterMap = map[string]formatters.Formatter{
@@ -52,6 +53,7 @@ var formatterMap = map[string]formatters.Formatter{
5253
"lcov-json": &lcovjson.Formatter{},
5354
"simplecov": &simplecov.Formatter{},
5455
"xccov": &xccov.Formatter{},
56+
"dotcover": &dotcover.Formatter{},
5557
}
5658

5759
// formatCoverageCmd represents the format command

formatters/dotcover/dotcover.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package dotcover
2+
3+
import (
4+
"encoding/xml"
5+
"os"
6+
"strings"
7+
8+
"github.com/Sirupsen/logrus"
9+
"github.com/codeclimate/test-reporter/env"
10+
"github.com/codeclimate/test-reporter/formatters"
11+
"github.com/pkg/errors"
12+
)
13+
14+
var searchPaths = []string{"dotcover.xml"}
15+
16+
// Formatter is the exported struct to be used on format-coverage.go
17+
type Formatter struct {
18+
Path string
19+
}
20+
21+
// Search looks for the dotcover test report file in default paths or provided ones.
22+
func (f *Formatter) Search(paths ...string) (string, error) {
23+
paths = append(paths, searchPaths...)
24+
for _, p := range paths {
25+
logrus.Debugf("checking search path %s for dotcover formatter", p)
26+
if _, err := os.Stat(p); err == nil {
27+
f.Path = p
28+
return p, nil
29+
}
30+
}
31+
32+
return "", errors.WithStack(errors.Errorf("could not find any files in search paths for dotcover. search paths were: %s", strings.Join(paths, ", ")))
33+
}
34+
35+
// Format transforms the provided test report into a CC readable report format.
36+
func (f Formatter) Format() (formatters.Report, error) {
37+
rep, err := formatters.NewReport()
38+
if err != nil {
39+
return rep, err
40+
}
41+
42+
c, err := f.readDotCoverXML()
43+
if err != nil {
44+
return rep, err
45+
}
46+
47+
gitHead, _ := env.GetHead()
48+
49+
for _, file := range c.Files {
50+
sf, err := formatters.NewSourceFile(file.Path, gitHead)
51+
if err != nil {
52+
err = errors.WithStack(err)
53+
break
54+
}
55+
56+
for _, statement := range c.Statements {
57+
if file.Index == statement.FileIndex {
58+
if statement.Covered {
59+
sf.Coverage = append(sf.Coverage, formatters.NewNullInt(1))
60+
} else {
61+
sf.Coverage = append(sf.Coverage, formatters.NewNullInt(0))
62+
}
63+
}
64+
}
65+
66+
err = rep.AddSourceFile(sf)
67+
68+
if err != nil {
69+
err = errors.WithStack(err)
70+
break
71+
}
72+
}
73+
74+
return rep, err
75+
}
76+
77+
// readDotCoverXML reads the dotCover XML file and returns its contents.
78+
func (f Formatter) readDotCoverXML() (*xmlDotCover, error) {
79+
fx, err := os.Open(f.Path)
80+
if err != nil {
81+
return nil, errors.WithStack(err)
82+
}
83+
84+
c := &xmlDotCover{}
85+
if err = xml.NewDecoder(fx).Decode(c); err != nil {
86+
return nil, errors.WithStack(err)
87+
}
88+
89+
return c, nil
90+
}

formatters/dotcover/dotcover_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package dotcover
2+
3+
import (
4+
"testing"
5+
6+
"gopkg.in/src-d/go-git.v4/plumbing/object"
7+
8+
"github.com/codeclimate/test-reporter/env"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_Parse(t *testing.T) {
13+
ogb := env.GitBlob
14+
defer func() {
15+
env.GitBlob = ogb
16+
}()
17+
env.GitBlob = func(s string, c *object.Commit) (string, error) {
18+
return s, nil
19+
}
20+
21+
assert := require.New(t)
22+
23+
formatter := Formatter{
24+
Path: "./example.xml",
25+
}
26+
rep, err := formatter.Format()
27+
assert.NoError(err)
28+
29+
assert.Len(rep.SourceFiles, 3)
30+
assert.InDelta(71, rep.CoveredPercent, 1)
31+
assert.Equal(24, rep.LineCounts.Total)
32+
assert.Equal(17, rep.LineCounts.Covered)
33+
34+
sf_one := rep.SourceFiles[`C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService\PrimeService.cs`]
35+
assert.InDelta(83, sf_one.CoveredPercent, 1)
36+
37+
sf_two := rep.SourceFiles[`C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService\SecondService.cs`]
38+
assert.Equal(0.0, sf_two.CoveredPercent)
39+
40+
sf_three := rep.SourceFiles[`C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService.Tests\PrimeService_IsPrimeShould.cs`]
41+
assert.Equal(100.0, sf_three.CoveredPercent)
42+
}

formatters/dotcover/example.xml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Root CoveredStatements="17" TotalStatements="24" CoveragePercent="71" ReportType="DetailedXml" DotCoverVersion="2022.3.2">
3+
<FileIndices>
4+
<File Index="1" Name="C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService.Tests\PrimeService_IsPrimeShould.cs" ChecksumAlgorithm="SHA256" Checksum="23612935A1229C4721145EACB2122A220B0F761AD0DB20CC980DBDB0D5003C2A" />
5+
<File Index="2" Name="C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService\PrimeService.cs" ChecksumAlgorithm="SHA256" Checksum="CA22D548019CFF7919E45E7289DC438D453674E827BBFCE249587AD9DDFAD47D" />
6+
<File Index="3" Name="C:\Users\fulano\Desktop\unit-testing-using-mstest\PrimeService\SecondService.cs" ChecksumAlgorithm="SHA256" Checksum="073781814111C3F1C12EB8C41863004150417FC1A9D1E08D7F48C95214356D6E" />
7+
</FileIndices>
8+
<Assembly Name="PrimeService" CoveredStatements="5" TotalStatements="12" CoveragePercent="42">
9+
<Namespace Name="Prime.Services" CoveredStatements="5" TotalStatements="12" CoveragePercent="42">
10+
<Type Name="PrimeService" CoveredStatements="5" TotalStatements="6" CoveragePercent="83">
11+
<Method Name="IsPrime(System.Int32):System.Boolean" CoveredStatements="5" TotalStatements="6" CoveragePercent="83">
12+
<Statement FileIndex="2" Line="8" Column="9" EndLine="8" EndColumn="10" Covered="True" />
13+
<Statement FileIndex="2" Line="9" Column="13" EndLine="9" EndColumn="31" Covered="True" />
14+
<Statement FileIndex="2" Line="10" Column="13" EndLine="10" EndColumn="14" Covered="True" />
15+
<Statement FileIndex="2" Line="11" Column="17" EndLine="11" EndColumn="30" Covered="True" />
16+
<Statement FileIndex="2" Line="13" Column="13" EndLine="13" EndColumn="77" Covered="False" />
17+
<Statement FileIndex="2" Line="14" Column="9" EndLine="14" EndColumn="10" Covered="True" />
18+
</Method>
19+
</Type>
20+
<Type Name="SecondService" CoveredStatements="0" TotalStatements="6" CoveragePercent="0">
21+
<Method Name="IsPrime(System.Int32):System.Boolean" CoveredStatements="0" TotalStatements="6" CoveragePercent="0">
22+
<Statement FileIndex="3" Line="8" Column="9" EndLine="8" EndColumn="10" Covered="False" />
23+
<Statement FileIndex="3" Line="9" Column="13" EndLine="9" EndColumn="31" Covered="False" />
24+
<Statement FileIndex="3" Line="10" Column="13" EndLine="10" EndColumn="14" Covered="False" />
25+
<Statement FileIndex="3" Line="11" Column="17" EndLine="11" EndColumn="30" Covered="False" />
26+
<Statement FileIndex="3" Line="13" Column="13" EndLine="13" EndColumn="77" Covered="False" />
27+
<Statement FileIndex="3" Line="14" Column="9" EndLine="14" EndColumn="10" Covered="False" />
28+
</Method>
29+
</Type>
30+
</Namespace>
31+
</Assembly>
32+
<Assembly Name="PrimeService.Tests" CoveredStatements="12" TotalStatements="12" CoveragePercent="100">
33+
<Namespace Name="Prime.UnitTests.Services" CoveredStatements="12" TotalStatements="12" CoveragePercent="100">
34+
<Type Name="PrimeService_IsPrimeShould" CoveredStatements="12" TotalStatements="12" CoveragePercent="100">
35+
<Method Name=".ctor():System.Void" CoveredStatements="4" TotalStatements="4" CoveragePercent="100">
36+
<Statement FileIndex="1" Line="11" Column="9" EndLine="11" EndColumn="44" Covered="True" />
37+
<Statement FileIndex="1" Line="12" Column="9" EndLine="12" EndColumn="10" Covered="True" />
38+
<Statement FileIndex="1" Line="13" Column="13" EndLine="13" EndColumn="48" Covered="True" />
39+
<Statement FileIndex="1" Line="14" Column="9" EndLine="14" EndColumn="10" Covered="True" />
40+
</Method>
41+
<Method Name="IsPrime_InputIs1_ReturnFalse():System.Void" CoveredStatements="4" TotalStatements="4" CoveragePercent="100">
42+
<Statement FileIndex="1" Line="18" Column="9" EndLine="18" EndColumn="10" Covered="True" />
43+
<Statement FileIndex="1" Line="19" Column="13" EndLine="19" EndColumn="51" Covered="True" />
44+
<Statement FileIndex="1" Line="21" Column="13" EndLine="21" EndColumn="62" Covered="True" />
45+
<Statement FileIndex="1" Line="22" Column="9" EndLine="22" EndColumn="10" Covered="True" />
46+
</Method>
47+
<Method Name="IsPrime_ValuesLessThan2_ReturnFalse(System.Int32):System.Void" CoveredStatements="4" TotalStatements="4" CoveragePercent="100">
48+
<Statement FileIndex="1" Line="30" Column="9" EndLine="30" EndColumn="10" Covered="True" />
49+
<Statement FileIndex="1" Line="31" Column="13" EndLine="31" EndColumn="55" Covered="True" />
50+
<Statement FileIndex="1" Line="33" Column="13" EndLine="33" EndColumn="68" Covered="True" />
51+
<Statement FileIndex="1" Line="34" Column="9" EndLine="34" EndColumn="10" Covered="True" />
52+
</Method>
53+
</Type>
54+
</Namespace>
55+
</Assembly>
56+
</Root>

formatters/dotcover/xml.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dotcover
2+
3+
import "encoding/xml"
4+
5+
type xmlDotCover struct {
6+
XMLName xml.Name `xml:"Root"`
7+
Files []struct {
8+
Path string `xml:"Name,attr"`
9+
Index int `xml:"Index,attr"`
10+
} `xml:"FileIndices>File"`
11+
Statements []struct {
12+
FileIndex int `xml:"FileIndex,attr"`
13+
Covered bool `xml:"Covered,attr"`
14+
} `xml:"Assembly>Namespace>Type>Method>Statement"`
15+
}

integration-tests/dotcover/Dockerfile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:7.0
2+
3+
# Install GoLang
4+
RUN curl -O https://dl.google.com/go/go1.15.linux-amd64.tar.gz
5+
RUN tar -xzf go1.15.linux-amd64.tar.gz
6+
RUN mv go /usr/local
7+
8+
ENV PATH $PATH:/usr/local/go/bin
9+
ENV GOBIN="/usr/local/go/bin"
10+
RUN go version
11+
12+
ENV GOPATH /go
13+
RUN mkdir $GOPATH
14+
ENV PATH $PATH:/go/bin
15+
16+
ENV CCTR=$GOPATH/src/github.com/codeclimate/test-reporter
17+
RUN mkdir -p $CCTR
18+
WORKDIR $CCTR
19+
COPY . .
20+
RUN go install -v
21+
22+
ENV PATH $PATH:/root/.dotnet/tools
23+
RUN dotnet tool install JetBrains.dotCover.GlobalTool -g --version "2022.3.2"
24+
25+
# Clone .NET example repo and run test
26+
RUN git clone https://github.com/codeclimate/dot-net-coverage-test.git
27+
WORKDIR dot-net-coverage-test
28+
RUN dotnet build
29+
RUN dotnet dotcover test --dcReportType=DetailedXML --dcOutput="dotcover.xml" --no-build
30+
31+
RUN echo "testing" > ignore.me && \
32+
git config --global user.email "you@example.com" && \
33+
git config --global user.name "Your Name" && \
34+
git add ignore.me && \
35+
git commit -m "testing"
36+
37+
ENV CC_TEST_REPORTER_ID=49a59a1849364524250d544e098b5d987335376cdb739ea7649c9f8bce968e3b
38+
RUN test-reporter format-coverage -d -t dotcover
39+
RUN cat coverage/codeclimate.json
40+
RUN test-reporter upload-coverage -d -s 2

man/cc-test-reporter-format-coverage.1.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Locate, parse, and re-format supported coverage sources.
1717

1818
# OPTIONS
1919

20-
## -t, --input-type *simplecov*|*lcov*|*coverage.py*|*gcov*|*clover*
20+
## -t, --input-type *simplecov*|*lcov*|*coverage.py*|*gcov*|*clover*|*dotcover*
2121

2222
Identifies the input type (format) of the COVERAGE_FILE.
2323

@@ -67,6 +67,10 @@ As generated by **phpunit --coverage-clover**.
6767

6868
As generated by `go test -coverprofile=c.out`
6969

70+
## ./dotcover.xml *DotCover*
71+
72+
As generated by `dotnet dotcover test --dcReportType=DetailedXML --dcOutput="dotcover.xml"`
73+
7074
# ENVIRONMENT VARIABLES
7175

7276
*GIT_BRANCH*, *GIT_COMMIT_SHA*, and *GIT_COMMITTED_AT* are required. *CI_NAME*,

0 commit comments

Comments
 (0)