Skip to content

Commit f4f5ccd

Browse files
authored
Add best-effort coverage reports per test type (#1915)
Ensure that each executed test type marks some content as covered, so we can have an idea of the files covered by tests, and the test types executed. Before we reported if a test type has been executed by marking a line in a manifest as covered, but this fails if the manifest doesn't have enough lines. At the moment the following files are fully or partially marked as covered if an specific type of test is executed: Pipeline tests: - No changes in this PR, we have coverage based on ES stats. Asset tests: - All files under the kibana directory. Policy tests: - All manifests. - All template files in the executed data stream. Static tests: - Sample events. System tests - All manifests. - Fields files. The rest of non-development files are marked as not covered.
1 parent 72545b0 commit f4f5ccd

File tree

20 files changed

+445
-385
lines changed

20 files changed

+445
-385
lines changed

cmd/testrunner.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ func testRunnerAssetCommandAction(cmd *cobra.Command, args []string) error {
175175
PackageRootPath: packageRootPath,
176176
KibanaClient: kibanaClient,
177177
GlobalTestConfig: globalTestConfig.Asset,
178+
WithCoverage: testCoverage,
179+
CoverageType: testCoverageFormat,
178180
})
179181

180182
results, err := testrunner.RunSuite(ctx, runner)
@@ -264,6 +266,8 @@ func testRunnerStaticCommandAction(cmd *cobra.Command, args []string) error {
264266
DataStreams: dataStreams,
265267
FailOnMissingTests: failOnMissing,
266268
GlobalTestConfig: globalTestConfig.Static,
269+
WithCoverage: testCoverage,
270+
CoverageType: testCoverageFormat,
267271
})
268272

269273
results, err := testrunner.RunSuite(ctx, runner)
@@ -572,6 +576,8 @@ func testRunnerSystemCommandAction(cmd *cobra.Command, args []string) error {
572576
DeferCleanup: deferCleanup,
573577
RunIndependentElasticAgent: false,
574578
GlobalTestConfig: globalTestConfig.System,
579+
WithCoverage: testCoverage,
580+
CoverageType: testCoverageFormat,
575581
})
576582

577583
logger.Debugf("Running suite...")
@@ -683,6 +689,8 @@ func testRunnerPolicyCommandAction(cmd *cobra.Command, args []string) error {
683689
FailOnMissingTests: failOnMissing,
684690
GenerateTestResult: generateTestResult,
685691
GlobalTestConfig: globalTestConfig.Policy,
692+
WithCoverage: testCoverage,
693+
CoverageType: testCoverageFormat,
686694
})
687695

688696
results, err := testrunner.RunSuite(ctx, runner)

internal/packages/assets.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Asset struct {
3434
ID string `json:"id"`
3535
Type AssetType `json:"type"`
3636
DataStream string
37+
SourcePath string
3738
}
3839

3940
// String method returns a string representation of the asset
@@ -181,8 +182,9 @@ func loadFileBasedAssets(kibanaAssetsFolderPath string, assetType AssetType) ([]
181182
}
182183

183184
asset := Asset{
184-
ID: assetID,
185-
Type: assetType,
185+
ID: assetID,
186+
Type: assetType,
187+
SourcePath: assetPath,
186188
}
187189
assets = append(assets, asset)
188190
}

internal/testrunner/coberturacoverage.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"bytes"
99
"encoding/xml"
1010
"fmt"
11-
"path/filepath"
12-
"sort"
1311
)
1412

1513
func init() {
@@ -200,64 +198,3 @@ func (c *CoberturaCoverage) Merge(other CoverageReport) error {
200198
}
201199
return nil
202200
}
203-
204-
func transformToCoberturaReport(details *testCoverageDetails, baseFolder string, timestamp int64) *CoberturaCoverage {
205-
var classes []*CoberturaClass
206-
lineNumberTestType := lineNumberPerTestType(string(details.testType))
207-
208-
// sort data streams to ensure same ordering in coverage arrays
209-
sortedDataStreams := make([]string, 0, len(details.dataStreams))
210-
for dataStream := range details.dataStreams {
211-
sortedDataStreams = append(sortedDataStreams, dataStream)
212-
}
213-
sort.Strings(sortedDataStreams)
214-
215-
for _, dataStream := range sortedDataStreams {
216-
testCases := details.dataStreams[dataStream]
217-
218-
if dataStream == "" && details.packageType == "integration" {
219-
continue // ignore tests running in the package context (not data stream), mostly referring to installed assets
220-
}
221-
222-
var methods []*CoberturaMethod
223-
var lines []*CoberturaLine
224-
225-
if len(testCases) == 0 {
226-
methods = append(methods, &CoberturaMethod{
227-
Name: "Missing",
228-
Lines: []*CoberturaLine{{Number: lineNumberTestType, Hits: 0}},
229-
})
230-
lines = append(lines, []*CoberturaLine{{Number: lineNumberTestType, Hits: 0}}...)
231-
} else {
232-
methods = append(methods, &CoberturaMethod{
233-
Name: "OK",
234-
Lines: []*CoberturaLine{{Number: lineNumberTestType, Hits: 1}},
235-
})
236-
lines = append(lines, []*CoberturaLine{{Number: lineNumberTestType, Hits: 1}}...)
237-
}
238-
239-
fileName := filepath.Join(baseFolder, details.packageName, "data_stream", dataStream, "manifest.yml")
240-
if dataStream == "" {
241-
// input package
242-
fileName = filepath.Join(baseFolder, details.packageName, "manifest.yml")
243-
}
244-
245-
aClass := &CoberturaClass{
246-
Name: string(details.testType),
247-
Filename: fileName,
248-
Methods: methods,
249-
Lines: lines,
250-
}
251-
classes = append(classes, aClass)
252-
}
253-
254-
return &CoberturaCoverage{
255-
Timestamp: timestamp,
256-
Packages: []*CoberturaPackage{
257-
{
258-
Name: details.packageName,
259-
Classes: classes,
260-
},
261-
},
262-
}
263-
}

internal/testrunner/coverage.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package testrunner
6+
7+
import (
8+
"bufio"
9+
"errors"
10+
"fmt"
11+
"io"
12+
"io/fs"
13+
"os"
14+
"path/filepath"
15+
"strings"
16+
"time"
17+
18+
"github.com/elastic/elastic-package/internal/files"
19+
)
20+
21+
// GenerateBasePackageCoverageReport generates a coverage report where all files under the root path are
22+
// marked as not covered. It ignores files under _dev directories.
23+
func GenerateBasePackageCoverageReport(pkgName, rootPath, format string) (CoverageReport, error) {
24+
repoPath, err := files.FindRepositoryRootDirectory()
25+
if err != nil {
26+
return nil, fmt.Errorf("failed to find repository root directory: %w", err)
27+
}
28+
29+
var coverage CoverageReport
30+
err = filepath.WalkDir(rootPath, func(match string, d fs.DirEntry, err error) error {
31+
if err != nil {
32+
return err
33+
}
34+
if d.IsDir() {
35+
if d.Name() == "_dev" {
36+
return fs.SkipDir
37+
}
38+
return nil
39+
}
40+
41+
fileCoverage, err := generateBaseFileCoverageReport(repoPath, pkgName, match, format, false)
42+
if err != nil {
43+
return fmt.Errorf("failed to generate base coverage for \"%s\": %w", match, err)
44+
}
45+
if coverage == nil {
46+
coverage = fileCoverage
47+
return nil
48+
}
49+
50+
err = coverage.Merge(fileCoverage)
51+
if err != nil {
52+
return fmt.Errorf("cannot merge coverages: %w", err)
53+
}
54+
55+
return nil
56+
})
57+
// If the directory is not found, give it as valid, will return an empty coverage. This is also useful for mocked tests.
58+
if err != nil && !errors.Is(err, os.ErrNotExist) {
59+
return nil, fmt.Errorf("failed to walk package directory %s: %w", rootPath, err)
60+
}
61+
return coverage, nil
62+
}
63+
64+
// GenerateBaseFileCoverageReport generates a coverage report for a given file, where all the file is marked as covered or uncovered.
65+
func GenerateBaseFileCoverageReport(pkgName, path, format string, covered bool) (CoverageReport, error) {
66+
repoPath, err := files.FindRepositoryRootDirectory()
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to find repository root directory: %w", err)
69+
}
70+
71+
return generateBaseFileCoverageReport(repoPath, pkgName, path, format, covered)
72+
}
73+
74+
// GenerateBaseFileCoverageReport generates a coverage report for all the files matching any of the given patterns. The complete
75+
// files are marked as fully covered or uncovered depending on the given value.
76+
func GenerateBaseFileCoverageReportGlob(pkgName string, patterns []string, format string, covered bool) (CoverageReport, error) {
77+
repoPath, err := files.FindRepositoryRootDirectory()
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to find repository root directory: %w", err)
80+
}
81+
82+
var coverage CoverageReport
83+
for _, pattern := range patterns {
84+
matches, err := filepath.Glob(pattern)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
for _, match := range matches {
90+
fileCoverage, err := generateBaseFileCoverageReport(repoPath, pkgName, match, format, covered)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to generate base coverage for \"%s\": %w", match, err)
93+
}
94+
if coverage == nil {
95+
coverage = fileCoverage
96+
continue
97+
}
98+
99+
err = coverage.Merge(fileCoverage)
100+
if err != nil {
101+
return nil, fmt.Errorf("cannot merge coverages: %w", err)
102+
}
103+
}
104+
}
105+
return coverage, nil
106+
}
107+
108+
func generateBaseFileCoverageReport(repoPath, pkgName, path, format string, covered bool) (CoverageReport, error) {
109+
switch format {
110+
case "cobertura":
111+
return generateBaseCoberturaFileCoverageReport(repoPath, pkgName, path, covered)
112+
case "generic":
113+
return generateBaseGenericFileCoverageReport(repoPath, pkgName, path, covered)
114+
default:
115+
return nil, fmt.Errorf("unknwon coverage format %s", format)
116+
}
117+
}
118+
119+
func generateBaseCoberturaFileCoverageReport(repoPath, pkgName, path string, covered bool) (*CoberturaCoverage, error) {
120+
coveragePath, err := filepath.Rel(repoPath, path)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed to obtain path inside repository for %s", path)
123+
}
124+
ext := filepath.Ext(path)
125+
class := CoberturaClass{
126+
Name: pkgName + "." + strings.TrimSuffix(filepath.Base(path), ext),
127+
Filename: coveragePath,
128+
}
129+
pkg := CoberturaPackage{
130+
Name: pkgName,
131+
Classes: []*CoberturaClass{
132+
&class,
133+
},
134+
}
135+
coverage := CoberturaCoverage{
136+
Sources: []*CoberturaSource{
137+
{
138+
Path: path,
139+
},
140+
},
141+
Packages: []*CoberturaPackage{
142+
&pkg,
143+
},
144+
Timestamp: time.Now().UnixNano(),
145+
}
146+
147+
f, err := os.Open(path)
148+
if err != nil {
149+
return nil, fmt.Errorf("failed to open file: %v", err)
150+
}
151+
defer f.Close()
152+
153+
hits := int64(0)
154+
if covered {
155+
hits = 1
156+
}
157+
lines, err := countReaderLines(f)
158+
if err != nil {
159+
return nil, fmt.Errorf("failed to count lines in file: %w", err)
160+
}
161+
for i := range lines {
162+
line := CoberturaLine{
163+
Number: i + 1,
164+
Hits: hits,
165+
}
166+
class.Lines = append(class.Lines, &line)
167+
}
168+
coverage.LinesValid = int64(lines)
169+
coverage.LinesCovered = int64(lines) * hits
170+
171+
return &coverage, nil
172+
}
173+
174+
func generateBaseGenericFileCoverageReport(repoPath, _, path string, covered bool) (*GenericCoverage, error) {
175+
coveragePath, err := filepath.Rel(repoPath, path)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to obtain path inside repository for %s", path)
178+
}
179+
file := GenericFile{
180+
Path: coveragePath,
181+
}
182+
coverage := GenericCoverage{
183+
Version: 1,
184+
Timestamp: time.Now().UnixNano(),
185+
TestType: fmt.Sprintf("Coverage for %s", coveragePath),
186+
Files: []*GenericFile{
187+
&file,
188+
},
189+
}
190+
191+
f, err := os.Open(path)
192+
if err != nil {
193+
return nil, fmt.Errorf("failed to open file: %v", err)
194+
}
195+
defer f.Close()
196+
197+
lines, err := countReaderLines(f)
198+
if err != nil {
199+
return nil, fmt.Errorf("failed to count lines in file: %w", err)
200+
}
201+
for i := range lines {
202+
line := GenericLine{
203+
LineNumber: int64(i) + 1,
204+
Covered: covered,
205+
}
206+
file.Lines = append(file.Lines, &line)
207+
}
208+
209+
return &coverage, nil
210+
}
211+
212+
func countReaderLines(r io.Reader) (int, error) {
213+
count := 0
214+
buffered := bufio.NewReader(r)
215+
for {
216+
c, _, err := buffered.ReadRune()
217+
if errors.Is(err, io.EOF) {
218+
break
219+
}
220+
if err != nil {
221+
return 0, fmt.Errorf("failed to read rune: %w", err)
222+
}
223+
if c != '\n' {
224+
continue
225+
}
226+
count += 1
227+
}
228+
return count, nil
229+
}

0 commit comments

Comments
 (0)