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:
parent
d0762f5db9
commit
b213af1089
@ -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
|
||||
|
@ -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"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
105
pkg/sonar/componentService.go
Normal file
105
pkg/sonar/componentService.go
Normal 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
|
||||
}
|
95
pkg/sonar/componentService_test.go
Normal file
95
pkg/sonar/componentService_test.go
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
@ -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 ...
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user