internal/testrunner/coveragereport.go (172 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package testrunner
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/elastic/elastic-package/internal/builder"
"github.com/elastic/elastic-package/internal/multierror"
)
type CoverageReport interface {
TimeStamp() int64
Merge(CoverageReport) error
Bytes() ([]byte, error)
}
var coverageReportFormatters = []string{}
// registerCoverageReporterFormat registers a test coverage report formatter.
func registerCoverageReporterFormat(name string) {
coverageReportFormatters = append(coverageReportFormatters, name)
}
func CoverageFormatsList() []string {
return coverageReportFormatters
}
type testCoverageDetails struct {
packageName string
packageType string
testType TestType
dataStreams map[string][]string // <data_stream> : <test case 1, test case 2, ...>
coverage CoverageReport // For tests to provide custom Coverage results.
errors multierror.Error
}
func newTestCoverageDetails(packageName, packageType string, testType TestType) *testCoverageDetails {
return &testCoverageDetails{packageName: packageName, packageType: packageType, testType: testType, dataStreams: map[string][]string{}}
}
func (tcd *testCoverageDetails) withUncoveredDataStreams(dataStreams []string) *testCoverageDetails {
for _, wt := range dataStreams {
tcd.dataStreams[wt] = []string{}
}
return tcd
}
func (tcd *testCoverageDetails) withCoverage(coverage CoverageReport) *testCoverageDetails {
tcd.coverage = coverage
return tcd
}
func (tcd *testCoverageDetails) withTestResults(results []TestResult) *testCoverageDetails {
for _, result := range results {
if _, ok := tcd.dataStreams[result.DataStream]; !ok {
tcd.dataStreams[result.DataStream] = []string{}
}
tcd.dataStreams[result.DataStream] = append(tcd.dataStreams[result.DataStream], result.Name)
if tcd.coverage != nil && result.Coverage != nil {
if err := tcd.coverage.Merge(result.Coverage); err != nil {
tcd.errors = append(tcd.errors, fmt.Errorf("can't merge coverage for test `%s`: %w", result.Name, err))
}
} else if tcd.coverage == nil {
tcd.coverage = result.Coverage
}
}
return tcd
}
// WriteCoverage function calculates test coverage for the given package.
// It requires to execute tests for all data streams (same test type), so the coverage can be calculated properly.
func WriteCoverage(packageRootPath, packageName, packageType string, testType TestType, results []TestResult, format string) error {
report, err := createCoverageReport(packageRootPath, packageName, packageType, testType, results, format)
if err != nil {
return fmt.Errorf("can't create coverage report: %w", err)
}
if report == nil {
return fmt.Errorf("coverage not found for test type %s", testType)
}
err = writeCoverageReportFile(report, packageName, string(testType))
if err != nil {
return fmt.Errorf("can't write test coverage report file: %w", err)
}
return nil
}
func createCoverageReport(packageRootPath, packageName, packageType string, testType TestType, results []TestResult, format string) (CoverageReport, error) {
details, err := collectTestCoverageDetails(packageRootPath, packageName, packageType, testType, results, format)
if err != nil {
return nil, fmt.Errorf("can't collect test coverage details: %w", err)
}
// Use provided coverage report
return details.coverage, nil
}
func collectTestCoverageDetails(packageRootPath, packageName, packageType string, testType TestType, results []TestResult, format string) (*testCoverageDetails, error) {
withoutTests, err := findDataStreamsWithoutTests(packageRootPath, testType)
if err != nil {
return nil, fmt.Errorf("can't find data streams without tests: %w", err)
}
emptyCoverage, err := GenerateBasePackageCoverageReport(packageName, packageRootPath, format)
if err != nil {
return nil, fmt.Errorf("can't generate initial base coverage report: %w", err)
}
details := newTestCoverageDetails(packageName, packageType, testType).
withUncoveredDataStreams(withoutTests).
withCoverage(emptyCoverage).
withTestResults(results)
if len(details.errors) > 0 {
return nil, details.errors
}
return details, nil
}
func findDataStreamsWithoutTests(packageRootPath string, testType TestType) ([]string, error) {
var noTests []string
dataStreamDir := filepath.Join(packageRootPath, "data_stream")
dataStreams, err := os.ReadDir(dataStreamDir)
if errors.Is(err, os.ErrNotExist) {
return noTests, nil // there are packages that don't have any data streams (fleet_server, security_detection_engine)
} else if err != nil {
return nil, fmt.Errorf("can't list data streams directory: %w", err)
}
for _, dataStream := range dataStreams {
if !dataStream.IsDir() {
continue
}
expected, err := verifyTestExpected(packageRootPath, dataStream.Name(), testType)
if err != nil {
return nil, fmt.Errorf("can't verify if test is expected: %w", err)
}
if !expected {
continue
}
dataStreamTestPath := filepath.Join(packageRootPath, "data_stream", dataStream.Name(), "_dev", "test", string(testType))
_, err = os.Stat(dataStreamTestPath)
if errors.Is(err, os.ErrNotExist) {
noTests = append(noTests, dataStream.Name())
continue
}
if err != nil {
return nil, fmt.Errorf("can't stat path: %s: %w", dataStreamTestPath, err)
}
}
return noTests, nil
}
// verifyTestExpected function checks if tests are actually expected.
// Pipeline tests require an ingest pipeline to be defined in the data stream.
func verifyTestExpected(packageRootPath string, dataStreamName string, testType TestType) (bool, error) {
if testType != "pipeline" {
return true, nil
}
ingestPipelinePath := filepath.Join(packageRootPath, "data_stream", dataStreamName, "elasticsearch", "ingest_pipeline")
_, err := os.Stat(ingestPipelinePath)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("can't stat path: %s: %w", ingestPipelinePath, err)
}
return true, nil
}
func writeCoverageReportFile(report CoverageReport, packageName, testType string) error {
dest, err := testCoverageReportsDir()
if err != nil {
return fmt.Errorf("could not determine test coverage reports folder: %w", err)
}
// Create test coverage reports folder if it doesn't exist
_, err = os.Stat(dest)
if err != nil && errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(dest, 0755); err != nil {
return fmt.Errorf("could not create test coverage reports folder: %w", err)
}
}
fileName := fmt.Sprintf("coverage-%s-%s-%d-report.xml", packageName, testType, report.TimeStamp())
filePath := filepath.Join(dest, fileName)
b, err := report.Bytes()
if err != nil {
return fmt.Errorf("can't marshal test coverage report: %w", err)
}
if err := os.WriteFile(filePath, b, 0644); err != nil {
return fmt.Errorf("could not write test coverage report file: %w", err)
}
return nil
}
func testCoverageReportsDir() (string, error) {
buildDir, err := builder.BuildDirectory()
if err != nil {
return "", fmt.Errorf("locating build directory failed: %w", err)
}
return filepath.Join(buildDir, "test-coverage"), nil
}