Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EngSys] adding coverage tools #15076

Merged
merged 19 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions eng/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Packages": [
{
"Name": "azidentity",
"CoverageGoal": 0.68
},
{
"Name": "azcore",
"CoverageGoal": 0.68
}
]
}
2 changes: 2 additions & 0 deletions eng/pipelines/templates/jobs/archetype-sdk-client.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
parameters:
ServiceDirectory: ''
CoverageGoal: 0.95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the coverage go tool gains complexity/scope, I think it would be good to follow a convention of an SDK directory config file to store this type of info. As it is in this PR, you can't run the coverage locally to see if it will pass upstream without knowing to manually look up the CoverageGoal in ci.yml. It gets even harder if you want to run coverage for the whole repo (e.g. for cross-cutting changes).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking in general for go is that we want to put as little context in the yaml as possible. In an ideal world the devops yaml would only function as a light orchestration wrapper and not contain any sdk-specific context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an example of another repo that has a config file? Or to store this in a single config file and specifying the information per library?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it's an open question, perhaps just a config.json file for easy interoperability? I know js uses the convention of putting custom key/value config data into the package.json file (e.g. for samples configuration). I don't think there are as many examples in other repos.

If you think it's weird/unnecessary, I wouldn't disagree as right now there would be only tool that uses it. As we add more tooling that needs to have per-sdk behavior it could be helpful to have a common place for this kind of stuff that is amenable to both local and pipeline scripts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benbp I added a config.json file in the eng directory. Right now it just has a list of packages and their code coverage goal but I think this will be a good way to expand any needs for it in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's going to be in eng and contain all package info, it can probably be a dedicated file called coverage.json instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with config.json instead because it may contain more info in the future other than coverage information.

RunTests: false

stages:
Expand Down Expand Up @@ -54,6 +55,7 @@ stages:
Scope: $(SCOPE)
Image: $(vm.image)
GoVersion: $(go.version)
CoverageGoal: ${{ parameters.CoverageGoal }}
RunTests: ${{ parameters.RunTests }}

- job: Analyze
Expand Down
13 changes: 1 addition & 12 deletions eng/pipelines/templates/steps/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,7 @@ steps:
env:
GO111MODULE: 'on'

- pwsh: |
$coverageFiles = [Collections.Generic.List[String]]@()
Get-Childitem -recurse -path $(SCOPE) -filter coverage.txt | foreach-object {
$covFile = $_.FullName
Write-Host "Adding $covFile to the list of code coverage files"
$coverageFiles.Add($covFile)
}
gocovmerge $coverageFiles > mergedCoverage.txt
gocov convert ./mergedCoverage.txt > ./coverage.json
# gocov converts rely on standard input
Get-Content ./coverage.json | gocov-xml > ./coverage.xml
Get-Content ./coverage.json | gocov-html > ./coverage.html
- pwsh: ../eng/scripts/create_coverage.ps1
displayName: 'Generate Coverage XML'
workingDirectory: '${{parameters.GoWorkspace}}sdk'

Expand Down
19 changes: 19 additions & 0 deletions eng/scripts/create_coverage.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#Requires -Version 7.0

$coverageFiles = [Collections.Generic.List[String]]@()
Get-ChildItem -recurse -path . -filter coverage.txt | ForEach-Object {
$covFile = $_.FullName
Write-Host "Adding $covFile to the list of code coverage files"
$coverageFiles.Add($covFile)
}

# merge coverage files
gocovmerge $coverageFiles > mergedCoverage.txt
gocov convert ./mergedCoverage.txt > ./coverage.json

# gocov converts rely on standard input
Get-Content ./coverage.json | gocov-xml > ./coverage.xml
Get-Content ./coverage.json | gocov-html > ./coverage.html

# use internal tool to fail if coverage is too low
go run ../tools/internal/coverage/main.go
2 changes: 1 addition & 1 deletion eng/scripts/get_module_dirs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Param(
$modDirs = [Collections.Generic.List[String]]@()

# find each module directory under $serviceDir
Get-Childitem -recurse -path $serviceDir -filter go.mod | foreach-object {
Get-ChildItem -recurse -path $serviceDir -filter go.mod | ForEach-Object {
$cdir = $_.Directory
Write-Host "Adding $cdir to list of module paths"
$modDirs.Add($cdir)
Expand Down
2 changes: 1 addition & 1 deletion eng/scripts/get_test_dirs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Param(
$testDirs = [Collections.Generic.List[String]]@()

# find each directory under $serviceDir that contains Go test files
Get-Childitem -recurse -path $serviceDir -filter *_test.go | foreach-object {
Get-ChildItem -recurse -path $serviceDir -filter *_test.go | ForEach-Object {
$cdir = $_.Directory
$tests = Select-String -Path $_ 'Test' -AllMatches

Expand Down
2 changes: 1 addition & 1 deletion sdk/to/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pr:
paths:
include:
- sdk/to/

stages:
- template: ../../eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
Expand Down
14 changes: 7 additions & 7 deletions sdk/to/to.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TimePtr(t time.Time) *time.Time {

// Int32PtrArray returns an array of *int32 from the specified values.
func Int32PtrArray(vals ...int32) []*int32 {
arr := make([]*int32, len(vals), len(vals))
arr := make([]*int32, len(vals))
for i := range vals {
arr[i] = Int32Ptr(vals[i])
}
Expand All @@ -53,7 +53,7 @@ func Int32PtrArray(vals ...int32) []*int32 {

// Int64PtrArray returns an array of *int64 from the specified values.
func Int64PtrArray(vals ...int64) []*int64 {
arr := make([]*int64, len(vals), len(vals))
arr := make([]*int64, len(vals))
for i := range vals {
arr[i] = Int64Ptr(vals[i])
}
Expand All @@ -62,7 +62,7 @@ func Int64PtrArray(vals ...int64) []*int64 {

// Float32PtrArray returns an array of *float32 from the specified values.
func Float32PtrArray(vals ...float32) []*float32 {
arr := make([]*float32, len(vals), len(vals))
arr := make([]*float32, len(vals))
for i := range vals {
arr[i] = Float32Ptr(vals[i])
}
Expand All @@ -71,7 +71,7 @@ func Float32PtrArray(vals ...float32) []*float32 {

// Float64PtrArray returns an array of *float64 from the specified values.
func Float64PtrArray(vals ...float64) []*float64 {
arr := make([]*float64, len(vals), len(vals))
arr := make([]*float64, len(vals))
for i := range vals {
arr[i] = Float64Ptr(vals[i])
}
Expand All @@ -80,7 +80,7 @@ func Float64PtrArray(vals ...float64) []*float64 {

// BoolPtrArray returns an array of *bool from the specified values.
func BoolPtrArray(vals ...bool) []*bool {
arr := make([]*bool, len(vals), len(vals))
arr := make([]*bool, len(vals))
for i := range vals {
arr[i] = BoolPtr(vals[i])
}
Expand All @@ -89,7 +89,7 @@ func BoolPtrArray(vals ...bool) []*bool {

// StringPtrArray returns an array of *string from the specified values.
func StringPtrArray(vals ...string) []*string {
arr := make([]*string, len(vals), len(vals))
arr := make([]*string, len(vals))
for i := range vals {
arr[i] = StringPtr(vals[i])
}
Expand All @@ -98,7 +98,7 @@ func StringPtrArray(vals ...string) []*string {

// TimePtrArray returns an array of *time.Time from the specified values.
func TimePtrArray(vals ...time.Time) []*time.Time {
arr := make([]*time.Time, len(vals), len(vals))
arr := make([]*time.Time, len(vals))
for i := range vals {
arr[i] = TimePtr(vals[i])
}
Expand Down
3 changes: 3 additions & 0 deletions tools/internal/coverage/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Azure/azure-sdk-for-go/tools/internal/coverage

go 1.16
137 changes: 137 additions & 0 deletions tools/internal/coverage/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)

type CodeCoverage struct {
Packages []Package `json:"Packages"`
}

type Package struct {
Name string `json:"name"`
CoverageGoal float64 `json:"CoverageGoal"`
}

const (
coverageXmlFile = "coverage.xml"
)

var configFile, _ = filepath.Abs(filepath.Join("..", "eng", "config.json"))

func check(e error) {
if e != nil {
log.Fatal(e)
}
}

var coverageFiles []string

func filterFiles(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(path, "coverage.xml") {
coverageFiles = append(coverageFiles, path)
}
return nil
}

func FindCoverageFiles(p string) {
err := filepath.Walk(".", filterFiles)
check(err)
}

func ReadConfigData() *CodeCoverage {
jsonFile, err := os.Open(configFile)
check(err)
defer jsonFile.Close()

byteValue, err := ioutil.ReadAll(jsonFile)
check(err)

var cov CodeCoverage
err = json.Unmarshal([]byte(byteValue), &cov)
check(err)
return &cov
}

// This supports doing a single package at a time. If this needs to be expanded in the future
// this method will have to return a []*float64 for each packages goal
func findCoverageGoal(covFiles []string, configData *CodeCoverage) float64 {
for _, covFile := range covFiles {
for _, p := range configData.Packages {
if strings.Contains(covFile, p.Name) {
return p.CoverageGoal
}
}
}
fmt.Println("WARNING: Could not find a coverage goal, defaulting to 95%.")
return 0.95
}

func main() {
coverageFiles = make([]string, 0)
rootPath, err := filepath.Abs(".")
check(err)

FindCoverageFiles(rootPath)

configData := ReadConfigData()
coverageGoal := findCoverageGoal(coverageFiles, configData)

fmt.Printf("Failing if the coverage is below %.2f\n", coverageGoal)

coverageValues := make([]float64, 0)
for _, coverageFile := range coverageFiles {
xmlFile, err := os.Open(coverageFile)
check(err)
defer xmlFile.Close()

byteValue, err := ioutil.ReadAll(xmlFile)
check(err)

re := regexp.MustCompile(`<coverage line-rate=\"\d\.\d+\"`)
coverageValue := re.Find(byteValue) //, -1)
if coverageValue == nil {
log.Fatalf("Could not match regexp to coverage.xml file.")
}

parts := strings.Split(string(coverageValue), "=")
coverageNumber := parts[1]

coverageNumber = coverageNumber[1 : len(coverageNumber)-1]
coverageFloat, err := strconv.ParseFloat(coverageNumber, 32)
check(err)
coverageValues = append(coverageValues, coverageFloat)
}

if len(coverageValues) != len(coverageFiles) {
fmt.Printf("Found %d coverage values in %d coverage files\n", len(coverageValues), len(coverageFiles))
}

failedCoverage := false
for i := range coverageValues {
status := "Succeeded"
if coverageValues[i] < coverageGoal {
status = "Failed"
}
fmt.Printf("Status: %v\tCoverage file: %v\t Coverage Amount: %.4f\n", status, coverageFiles[i], coverageValues[i])
if coverageValues[i] < coverageGoal {
failedCoverage = true
}
}

if failedCoverage {
fmt.Println("Coverage step failed")
os.Exit(1)
}
}