1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-28 05:47:08 +02:00

Add Coverage Info to sonarscan.json (#3262)

* Add coverage metrics to report + influx

* Write unit tests

* Add integration test for Sonar Measures Component Service
This commit is contained in:
Marc Bormeth 2021-12-08 09:02:12 +01:00 committed by GitHub
parent d0762f5db9
commit b213af1089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 296 additions and 9 deletions

View File

@ -231,6 +231,16 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
if err != nil {
return err
}
componentService := SonarUtils.NewMeasuresComponentService(taskReport.ServerURL, config.Token, taskReport.ProjectKey, config.Organization, apiClient)
cov, err := componentService.GetCoverage()
if err != nil {
return err // No wrap, description already added one level below
}
influx.sonarqube_data.fields.coverage = cov.Coverage
influx.sonarqube_data.fields.branch_coverage = cov.BranchCoverage
influx.sonarqube_data.fields.line_coverage = cov.LineCoverage
log.Entry().Debugf("Influx values: %v", influx.sonarqube_data.fields)
err = SonarUtils.WriteReport(SonarUtils.ReportData{
ServerURL: taskReport.ServerURL,
@ -246,6 +256,7 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
Minor: influx.sonarqube_data.fields.minor_issues,
Info: influx.sonarqube_data.fields.info_issues,
},
Coverage: *cov,
}, sonar.workingDir, ioutil.WriteFile)
if err != nil {
return err

View File

@ -62,6 +62,9 @@ type sonarExecuteScanInflux struct {
major_issues int
minor_issues int
info_issues int
coverage float32
branch_coverage float32
line_coverage float32
}
tags struct {
}
@ -81,6 +84,9 @@ func (i *sonarExecuteScanInflux) persist(path, resourceName string) {
{valType: config.InfluxField, measurement: "sonarqube_data", name: "major_issues", value: i.sonarqube_data.fields.major_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "minor_issues", value: i.sonarqube_data.fields.minor_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "info_issues", value: i.sonarqube_data.fields.info_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "coverage", value: i.sonarqube_data.fields.coverage},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "branch_coverage", value: i.sonarqube_data.fields.branch_coverage},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "line_coverage", value: i.sonarqube_data.fields.line_coverage},
}
errCount := 0
@ -526,7 +532,7 @@ func sonarExecuteScanMetadata() config.StepData {
Type: "influx",
Parameters: []map[string]interface{}{
{"Name": "step_data"}, {"fields": []map[string]string{{"name": "sonar"}}},
{"Name": "sonarqube_data"}, {"fields": []map[string]string{{"name": "blocker_issues"}, {"name": "critical_issues"}, {"name": "major_issues"}, {"name": "minor_issues"}, {"name": "info_issues"}}},
{"Name": "sonarqube_data"}, {"fields": []map[string]string{{"name": "blocker_issues"}, {"name": "critical_issues"}, {"name": "major_issues"}, {"name": "minor_issues"}, {"name": "info_issues"}, {"name": "coverage"}, {"name": "branch_coverage"}, {"name": "line_coverage"}}},
},
},
},

View File

@ -108,6 +108,38 @@ ceTaskId=AXERR2JBbm9IiM5TEST
ceTaskUrl=` + sonarServerURL + `/api/ce/task?id=AXERR2JBbm9IiMTEST
`
const measuresComponentResponse = `
{
"component": {
"key": "com.sap.piper.test",
"name": "com.sap.piper.test",
"qualifier": "TRK",
"measures": [
{
"metric": "line_coverage",
"value": "80.4",
"bestValue": false
},
{
"metric": "branch_coverage",
"value": "81.0",
"bestValue": false
},
{
"metric": "coverage",
"value": "80.7",
"bestValue": false
},
{
"metric": "extra_valie",
"value": "42.7",
"bestValue": false
}
]
}
}
`
func TestRunSonar(t *testing.T) {
mockRunner := mock.ExecMockRunner{}
mockDownloadClient := mockDownloader{shouldFail: false}
@ -119,6 +151,7 @@ func TestRunSonar(t *testing.T) {
// add response handler
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, `{ "task": { "componentId": "AXERR2JBbm9IiM5TEST", "status": "SUCCESS" }}`))
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`))
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointMeasuresComponent+"", httpmock.NewStringResponder(http.StatusOK, measuresComponentResponse))
t.Run("default", func(t *testing.T) {
// init

View File

@ -53,6 +53,31 @@ func TestSonarIssueSearch(t *testing.T) {
// assert.NotEmpty(t, result.Organizations)
}
func TestSonarMeasuresComponentSearch(t *testing.T) {
t.Parallel()
// init
token := os.Getenv("PIPER_INTEGRATION_SONAR_TOKEN")
require.NotEmpty(t, token, "SonarQube API Token is missing")
host := os.Getenv("PIPER_INTEGRATION_SONAR_HOST")
if len(host) == 0 {
host = "https://sonarcloud.io"
}
organization := os.Getenv("PIPER_INTEGRATION_SONAR_ORGANIZATION")
if len(organization) == 0 {
organization = "sap-1"
}
componentKey := os.Getenv("PIPER_INTEGRATION_SONAR_PROJECT")
if len(componentKey) == 0 {
componentKey = "SAP_jenkins-library"
}
componentService := sonar.NewMeasuresComponentService(host, token, componentKey, organization, &piperhttp.Client{})
// test
_, err := componentService.GetCoverage()
// assert
assert.NoError(t, err)
}
func TestPiperGithubPublishRelease(t *testing.T) {
t.Parallel()
token := os.Getenv("PIPER_INTEGRATION_GITHUB_TOKEN")

View File

@ -0,0 +1,105 @@
package sonar
import (
"net/http"
"strconv"
"github.com/SAP/jenkins-library/pkg/log"
sonargo "github.com/magicsong/sonargo/sonar"
"github.com/pkg/errors"
)
// EndpointIssuesSearch API endpoint for https://sonarcloud.io/web_api/api/measures/component
const EndpointMeasuresComponent = "measures/component"
// ComponentService ...
type ComponentService struct {
Organization string
Project string
apiClient *Requester
}
type SonarCoverage struct {
Coverage float32 `json:"coverage,omitempty"`
LineCoverage float32 `json:"lineCoverage,omitempty"`
BranchCoverage float32 `json:"branchCoverage,omitempty"`
}
// GetCoverage ...
func (service *ComponentService) Component(options *sonargo.MeasuresComponentOption) (*sonargo.MeasuresComponentObject, *http.Response, error) {
request, err := service.apiClient.create("GET", EndpointMeasuresComponent, options)
if err != nil {
return nil, nil, err
}
// use custom HTTP client to send request
response, err := service.apiClient.send(request)
if err != nil {
return nil, nil, err
}
// reuse response verrification from sonargo
err = sonargo.CheckResponse(response)
if err != nil {
return nil, response, err
}
// decode JSON response
result := new(sonargo.MeasuresComponentObject)
err = service.apiClient.decode(response, result)
if err != nil {
return nil, response, err
}
return result, response, nil
}
func (service *ComponentService) GetCoverage() (*SonarCoverage, error) {
options := sonargo.MeasuresComponentOption{
Component: service.Project,
MetricKeys: "coverage,branch_coverage,line_coverage",
}
component, response, _ := service.Component(&options)
// reuse response verification from sonargo
err := sonargo.CheckResponse(response)
if err != nil {
return nil, errors.Wrap(err, "Failed to get coverage from Sonar measures/component API")
}
measures := component.Component.Measures
cov := &SonarCoverage{}
for _, element := range measures {
val, err := parseMeasureValuef32(*element)
if err != nil {
return nil, err
}
switch element.Metric {
case "coverage":
cov.Coverage = val
case "branch_coverage":
cov.BranchCoverage = val
case "line_coverage":
cov.LineCoverage = val
default:
log.Entry().Debugf("Received unhandled coverage metric from Sonar measures/component API. (Metric: %s, Value: %s)", element.Metric, element.Value)
}
}
return cov, nil
}
// NewMeasuresComponentService returns a new instance of a service for the measures/component endpoint.
func NewMeasuresComponentService(host, token, project, organization string, client Sender) *ComponentService {
return &ComponentService{
Organization: organization,
Project: project,
apiClient: NewAPIClient(host, token, client),
}
}
func parseMeasureValuef32(measure sonargo.SonarMeasure) (float32, error) {
str := measure.Value
f64, err := strconv.ParseFloat(str, 32)
if err != nil {
return 0.0, errors.Wrap(err, "Invalid value found in measure "+measure.Metric+": "+measure.Value)
}
return float32(f64), nil
}

View File

@ -0,0 +1,95 @@
package sonar
import (
"net/http"
"testing"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
)
func TestComponentService(t *testing.T) {
testURL := "https://example.org"
t.Run("success", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointMeasuresComponent+"", httpmock.NewStringResponder(http.StatusOK, responseCoverage))
// create service instance
serviceUnderTest := NewMeasuresComponentService(testURL, mock.Anything, mock.Anything, mock.Anything, sender)
// test
cov, err := serviceUnderTest.GetCoverage()
// assert
assert.NoError(t, err)
assert.Equal(t, float32(81), cov.BranchCoverage)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("invalid metric value", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointMeasuresComponent+"", httpmock.NewStringResponder(http.StatusOK, responseCoverageInvalidValue))
// create service instance
serviceUnderTest := NewMeasuresComponentService(testURL, mock.Anything, mock.Anything, mock.Anything, sender)
// test
cov, err := serviceUnderTest.GetCoverage()
// assert
assert.Error(t, err)
assert.Nil(t, cov)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
}
const responseCoverage = `{
"component": {
"key": "com.sap.piper.test",
"name": "com.sap.piper.test",
"qualifier": "TRK",
"measures": [
{
"metric": "line_coverage",
"value": "80.4",
"bestValue": false
},
{
"metric": "branch_coverage",
"value": "81.0",
"bestValue": false
},
{
"metric": "coverage",
"value": "80.7",
"bestValue": false
},
{
"metric": "extra_valie",
"value": "42.7",
"bestValue": false
}
]
}
}`
const responseCoverageInvalidValue = `{
"component": {
"key": "com.sap.piper.test",
"name": "com.sap.piper.test",
"qualifier": "TRK",
"measures": [
{
"metric": "line_coverage",
"value": "xyz",
"bestValue": false
}
]
}
}`

View File

@ -10,13 +10,14 @@ const reportFileName = "sonarscan.json"
//ReportData is representing the data of the step report JSON
type ReportData struct {
ServerURL string `json:"serverUrl"`
ProjectKey string `json:"projectKey"`
TaskID string `json:"taskId"`
ChangeID string `json:"changeID,omitempty"`
BranchName string `json:"branchName,omitempty"`
Organization string `json:"organization,omitempty"`
NumberOfIssues Issues `json:"numberOfIssues"`
ServerURL string `json:"serverUrl"`
ProjectKey string `json:"projectKey"`
TaskID string `json:"taskId"`
ChangeID string `json:"changeID,omitempty"`
BranchName string `json:"branchName,omitempty"`
Organization string `json:"organization,omitempty"`
NumberOfIssues Issues `json:"numberOfIssues"`
Coverage SonarCoverage `json:"coverage"`
}
// Issues ...

View File

@ -19,7 +19,7 @@ func writeToFileMock(f string, d []byte, p os.FileMode) error {
func TestWriteReport(t *testing.T) {
// init
const expected = `{"serverUrl":"https://sonarcloud.io","projectKey":"Piper-Validation/Golang","taskId":"mock.Anything","numberOfIssues":{"blocker":0,"critical":1,"major":2,"minor":3,"info":4}}`
const expected = `{"serverUrl":"https://sonarcloud.io","projectKey":"Piper-Validation/Golang","taskId":"mock.Anything","numberOfIssues":{"blocker":0,"critical":1,"major":2,"minor":3,"info":4},"coverage":{"coverage":13.7,"lineCoverage":37.1,"branchCoverage":42}}`
testData := ReportData{
ServerURL: "https://sonarcloud.io",
ProjectKey: "Piper-Validation/Golang",
@ -30,6 +30,11 @@ func TestWriteReport(t *testing.T) {
Minor: 3,
Info: 4,
},
Coverage: SonarCoverage{
Coverage: 13.7,
BranchCoverage: 42,
LineCoverage: 37.1,
},
}
// test
err := WriteReport(testData, "", writeToFileMock)

View File

@ -288,6 +288,12 @@ spec:
type: int
- name: info_issues
type: int
- name: coverage
type: float32
- name: branch_coverage
type: float32
- name: line_coverage
type: float32
containers:
- name: sonar
image: sonarsource/sonar-scanner-cli:4.6