mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-30 05:59:39 +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 {
|
if err != nil {
|
||||||
return err
|
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)
|
log.Entry().Debugf("Influx values: %v", influx.sonarqube_data.fields)
|
||||||
err = SonarUtils.WriteReport(SonarUtils.ReportData{
|
err = SonarUtils.WriteReport(SonarUtils.ReportData{
|
||||||
ServerURL: taskReport.ServerURL,
|
ServerURL: taskReport.ServerURL,
|
||||||
@ -246,6 +256,7 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
|
|||||||
Minor: influx.sonarqube_data.fields.minor_issues,
|
Minor: influx.sonarqube_data.fields.minor_issues,
|
||||||
Info: influx.sonarqube_data.fields.info_issues,
|
Info: influx.sonarqube_data.fields.info_issues,
|
||||||
},
|
},
|
||||||
|
Coverage: *cov,
|
||||||
}, sonar.workingDir, ioutil.WriteFile)
|
}, sonar.workingDir, ioutil.WriteFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -62,6 +62,9 @@ type sonarExecuteScanInflux struct {
|
|||||||
major_issues int
|
major_issues int
|
||||||
minor_issues int
|
minor_issues int
|
||||||
info_issues int
|
info_issues int
|
||||||
|
coverage float32
|
||||||
|
branch_coverage float32
|
||||||
|
line_coverage float32
|
||||||
}
|
}
|
||||||
tags struct {
|
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: "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: "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: "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
|
errCount := 0
|
||||||
@ -526,7 +532,7 @@ func sonarExecuteScanMetadata() config.StepData {
|
|||||||
Type: "influx",
|
Type: "influx",
|
||||||
Parameters: []map[string]interface{}{
|
Parameters: []map[string]interface{}{
|
||||||
{"Name": "step_data"}, {"fields": []map[string]string{{"name": "sonar"}}},
|
{"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
|
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) {
|
func TestRunSonar(t *testing.T) {
|
||||||
mockRunner := mock.ExecMockRunner{}
|
mockRunner := mock.ExecMockRunner{}
|
||||||
mockDownloadClient := mockDownloader{shouldFail: false}
|
mockDownloadClient := mockDownloader{shouldFail: false}
|
||||||
@ -119,6 +151,7 @@ func TestRunSonar(t *testing.T) {
|
|||||||
// add response handler
|
// 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.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.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) {
|
t.Run("default", func(t *testing.T) {
|
||||||
// init
|
// init
|
||||||
|
@ -53,6 +53,31 @@ func TestSonarIssueSearch(t *testing.T) {
|
|||||||
// assert.NotEmpty(t, result.Organizations)
|
// 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) {
|
func TestPiperGithubPublishRelease(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
token := os.Getenv("PIPER_INTEGRATION_GITHUB_TOKEN")
|
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
|
//ReportData is representing the data of the step report JSON
|
||||||
type ReportData struct {
|
type ReportData struct {
|
||||||
ServerURL string `json:"serverUrl"`
|
ServerURL string `json:"serverUrl"`
|
||||||
ProjectKey string `json:"projectKey"`
|
ProjectKey string `json:"projectKey"`
|
||||||
TaskID string `json:"taskId"`
|
TaskID string `json:"taskId"`
|
||||||
ChangeID string `json:"changeID,omitempty"`
|
ChangeID string `json:"changeID,omitempty"`
|
||||||
BranchName string `json:"branchName,omitempty"`
|
BranchName string `json:"branchName,omitempty"`
|
||||||
Organization string `json:"organization,omitempty"`
|
Organization string `json:"organization,omitempty"`
|
||||||
NumberOfIssues Issues `json:"numberOfIssues"`
|
NumberOfIssues Issues `json:"numberOfIssues"`
|
||||||
|
Coverage SonarCoverage `json:"coverage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issues ...
|
// Issues ...
|
||||||
|
@ -19,7 +19,7 @@ func writeToFileMock(f string, d []byte, p os.FileMode) error {
|
|||||||
|
|
||||||
func TestWriteReport(t *testing.T) {
|
func TestWriteReport(t *testing.T) {
|
||||||
// init
|
// 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{
|
testData := ReportData{
|
||||||
ServerURL: "https://sonarcloud.io",
|
ServerURL: "https://sonarcloud.io",
|
||||||
ProjectKey: "Piper-Validation/Golang",
|
ProjectKey: "Piper-Validation/Golang",
|
||||||
@ -30,6 +30,11 @@ func TestWriteReport(t *testing.T) {
|
|||||||
Minor: 3,
|
Minor: 3,
|
||||||
Info: 4,
|
Info: 4,
|
||||||
},
|
},
|
||||||
|
Coverage: SonarCoverage{
|
||||||
|
Coverage: 13.7,
|
||||||
|
BranchCoverage: 42,
|
||||||
|
LineCoverage: 37.1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
// test
|
// test
|
||||||
err := WriteReport(testData, "", writeToFileMock)
|
err := WriteReport(testData, "", writeToFileMock)
|
||||||
|
@ -288,6 +288,12 @@ spec:
|
|||||||
type: int
|
type: int
|
||||||
- name: info_issues
|
- name: info_issues
|
||||||
type: int
|
type: int
|
||||||
|
- name: coverage
|
||||||
|
type: float32
|
||||||
|
- name: branch_coverage
|
||||||
|
type: float32
|
||||||
|
- name: line_coverage
|
||||||
|
type: float32
|
||||||
containers:
|
containers:
|
||||||
- name: sonar
|
- name: sonar
|
||||||
image: sonarsource/sonar-scanner-cli:4.6
|
image: sonarsource/sonar-scanner-cli:4.6
|
||||||
|
Loading…
x
Reference in New Issue
Block a user