diff --git a/.buildkite/scripts/merge_xml.sh b/.buildkite/scripts/merge_xml.sh deleted file mode 100755 index 01d910d43946..000000000000 --- a/.buildkite/scripts/merge_xml.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Script to merge all the coverage XML files into just one file. -# It supports XML files using generic test coverage report format: -# https://docs.sonarsource.com/sonarqube/9.8/analyzing-source-code/test-coverage/generic-test-data/#generic-test-coverage - -sourceFolder="build/test-coverage" -mergedCoverageFileName="coverage_merged.xml" - -pushd "${sourceFolder}" > /dev/null -echo "Generating ${mergedCoverageFileName} into ${sourceFolder}..." -echo '' > "${mergedCoverageFileName}" -echo '' >> "${mergedCoverageFileName}" - -for file in coverage-*.xml; do - if [[ "$file" == "${mergedCoverageFileName}" ]]; then - continue - fi - echo " - Adding ${file}" - sed '1d;$d' "$file" | awk '//' >> "${mergedCoverageFileName}" -done - -echo '' >> "${mergedCoverageFileName}" -echo 'Done' - -popd > /dev/null - diff --git a/.buildkite/scripts/run_sonar_scanner.sh b/.buildkite/scripts/run_sonar_scanner.sh index 55650d26ee7a..e64a966c8c17 100755 --- a/.buildkite/scripts/run_sonar_scanner.sh +++ b/.buildkite/scripts/run_sonar_scanner.sh @@ -1,6 +1,12 @@ #!/bin/bash + +source .buildkite/scripts/common.sh + set -euo pipefail +add_bin_path +with_mage + run_sonar_scanner() { local message="" echo "--- Download coverage reports and merge them" @@ -15,7 +21,7 @@ run_sonar_scanner() { fi echo "Merge all coverage reports" - .buildkite/scripts/merge_xml.sh + mage mergeCoverage echo "--- Execute sonar scanner CLI" /scan-source-code.sh diff --git a/dev/coverage/coverage.go b/dev/coverage/coverage.go new file mode 100644 index 000000000000..b9b5eab394ed --- /dev/null +++ b/dev/coverage/coverage.go @@ -0,0 +1,143 @@ +// 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. + +// File partially copied from elastic-package. + +package coverage + +import ( + "bytes" + "encoding/xml" + "fmt" + "os" +) + +// GenericCoverage is the root element for a Cobertura XML report. +type GenericCoverage struct { + XMLName xml.Name `xml:"coverage"` + Version int64 `xml:"version,attr"` + Files []*GenericFile `xml:"file"` + Timestamp int64 `xml:"-"` + TestType string `xml:",comment"` +} + +type GenericFile struct { + Path string `xml:"path,attr"` + Lines []*GenericLine `xml:"lineToCover"` +} + +type GenericLine struct { + LineNumber int64 `xml:"lineNumber,attr"` + Covered bool `xml:"covered,attr"` +} + +func (c *GenericCoverage) Bytes() ([]byte, error) { + out, err := xml.MarshalIndent(&c, "", " ") + if err != nil { + return nil, fmt.Errorf("unable to format test results as Coverage: %w", err) + } + + var buffer bytes.Buffer + buffer.WriteString(xml.Header) + buffer.WriteString("\n") + buffer.Write(out) + return buffer.Bytes(), nil +} + +func (c *GenericFile) merge(b *GenericFile) error { + // Merge files + for _, coverageLine := range b.Lines { + found := false + foundId := 0 + for idx, existingLine := range c.Lines { + if existingLine.LineNumber == coverageLine.LineNumber { + found = true + foundId = idx + break + } + } + if !found { + c.Lines = append(c.Lines, coverageLine) + } else { + c.Lines[foundId].Covered = c.Lines[foundId].Covered || coverageLine.Covered + } + } + return nil +} + +// merge merges two coverage reports. +func (c *GenericCoverage) Merge(other *GenericCoverage) error { + // Merge files + for _, coverageFile := range other.Files { + var target *GenericFile + for _, existingFile := range c.Files { + if existingFile.Path == coverageFile.Path { + target = existingFile + break + } + } + if target != nil { + if err := target.merge(coverageFile); err != nil { + return err + } + } else { + c.Files = append(c.Files, coverageFile) + } + } + return nil +} + +func ReadGenericCoverage(path string) (*GenericCoverage, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open failed: %w", err) + } + defer f.Close() + + dec := xml.NewDecoder(f) + + var coverage GenericCoverage + err = dec.Decode(&coverage) + if err != nil { + return nil, fmt.Errorf("xml decode failed: %w", err) + } + + return &coverage, nil +} + +func MergeGenericCoverageFiles(paths []string, output string) error { + f, err := os.Create(output) + if err != nil { + return fmt.Errorf("cannot open file %s to write merged coverage: %w", output, err) + } + defer f.Close() + + var coverage *GenericCoverage + for _, path := range paths { + c, err := ReadGenericCoverage(path) + if err != nil { + return fmt.Errorf("failed to read coverage from %s: %w", path, err) + } + if coverage == nil { + coverage = c + continue + } + err = coverage.Merge(c) + if err != nil { + return fmt.Errorf("failed to merge coverage from %s: %w", path, err) + } + } + + d, err := coverage.Bytes() + if err != nil { + return fmt.Errorf("failed to encode merged coverage: %w", err) + } + + _, err = f.Write(d) + if err != nil { + return fmt.Errorf("cannot write merged coverage to %s: %w", output, err) + } + + return nil +} diff --git a/dev/coverage/coverage_test.go b/dev/coverage/coverage_test.go new file mode 100644 index 000000000000..4f25c60c4db5 --- /dev/null +++ b/dev/coverage/coverage_test.go @@ -0,0 +1,31 @@ +// 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 coverage + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMergeCoverage(t *testing.T) { + coverageFiles, err := filepath.Glob("testdata/test-coverage-*.xml") + require.NoError(t, err) + + expectedCoverage, err := ReadGenericCoverage("testdata/expected-test-coverage.xml") + require.NoError(t, err) + + output := filepath.Join(t.TempDir(), "coverage-merged.xml") + + err = MergeGenericCoverageFiles(coverageFiles, output) + require.NoError(t, err) + + mergedCoverage, err := ReadGenericCoverage(output) + require.NoError(t, err) + + assert.EqualValues(t, expectedCoverage, mergedCoverage) +} diff --git a/dev/coverage/testdata/expected-test-coverage.xml b/dev/coverage/testdata/expected-test-coverage.xml new file mode 100644 index 000000000000..4335bd4f67c8 --- /dev/null +++ b/dev/coverage/testdata/expected-test-coverage.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/coverage/testdata/test-coverage-1.xml b/dev/coverage/testdata/test-coverage-1.xml new file mode 100644 index 000000000000..4275ab18537f --- /dev/null +++ b/dev/coverage/testdata/test-coverage-1.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/coverage/testdata/test-coverage-2.xml b/dev/coverage/testdata/test-coverage-2.xml new file mode 100644 index 000000000000..4d5f1888407c --- /dev/null +++ b/dev/coverage/testdata/test-coverage-2.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev/coverage/testdata/test-coverage-3.xml b/dev/coverage/testdata/test-coverage-3.xml new file mode 100644 index 000000000000..3e1e45f0b180 --- /dev/null +++ b/dev/coverage/testdata/test-coverage-3.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/magefile.go b/magefile.go index 054a67c5acc1..7f3177e806a2 100644 --- a/magefile.go +++ b/magefile.go @@ -7,6 +7,7 @@ package main import ( + "fmt" "io" "os" "path/filepath" @@ -16,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/elastic/integrations/dev/codeowners" + "github.com/elastic/integrations/dev/coverage" ) var ( @@ -51,6 +53,14 @@ func ImportBeats() error { return sh.Run("go", args...) } +func MergeCoverage() error { + coverageFiles, err := filepath.Glob("build/test-coverage/coverage-*.xml") + if err != nil { + return fmt.Errorf("glob failed: %w", err) + } + return coverage.MergeGenericCoverageFiles(coverageFiles, "build/test-coverage/coverage_merged.xml") +} + func build() error { mg.Deps(buildImportBeats) return nil