Skip to content
Open
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
6 changes: 6 additions & 0 deletions convert/jenkinsjson/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,12 @@ func collectStepsWithID(currentNode jenkinsjson.Node, stepGroupWithId *[]StepGro
case "cobertura":
*stepWithIDList = append(*stepWithIDList, StepWithID{Step: jenkinsjson.ConvertCobertura(currentNode, variables), ID: id})

case "recordCoverage":
stepsList := jenkinsjson.ConvertRecordCoverage(currentNode, variables)
for _, step := range stepsList {
*stepWithIDList = append(*stepWithIDList, StepWithID{Step: step, ID: id})
}

case "slackSend":
*stepWithIDList = append(*stepWithIDList, StepWithID{Step: jenkinsjson.ConvertSlackSend(currentNode, variables), ID: id})

Expand Down
91 changes: 91 additions & 0 deletions convert/jenkinsjson/convertTestFiles/recordCoverage/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
pipeline {
agent any

tools {
maven 'M3'
jdk 'jdk8'
}

stages {
stage('Checkout SCM') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: "*/master"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'CleanCheckout']],
submoduleCfg: [],
userRemoteConfigs: [[url: 'https://github.com/syamv/game-of-life.git']]
])
}
}

// Using Maven for build
stage('Maven Build') {
steps {
sh 'mvn clean verify org.jacoco:jacoco-maven-plugin:prepare-agent package jacoco:report'
sh 'curl https://raw.githubusercontent.com/jenkinsci/cobertura-plugin/ff0973461f60c28dbc0bf5ab4853a5aec4798210/src/test/resources/hudson/plugins/cobertura/coverage-with-lots-of-data.xml > coverage.xml'
}
}

stage('Jacoco Reports') {
steps {
recordCoverage(
tools: [
[parser: 'JACOCO'],
[parser: 'COBERTURA', pattern: '**/coverage.xml']
],
id: 'jacoco',
name: 'JaCoCo Coverage',
enabledForFailure: true,
sourceCodeRetention: 'EVERY_BUILD',
qualityGates: [
[threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', unstable: true],
[threshold: 60.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true]
]
)
}
}
}

// recordCoverage step
stages {
stage('Checkout SCM') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: "*/master"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'CleanCheckout']],
submoduleCfg: [],
userRemoteConfigs: [[url: 'https://github.com/syamv/game-of-life.git']]
])
}
}

// Using Maven for build
stage('Maven Build') {
steps {
sh 'mvn clean verify org.jacoco:jacoco-maven-plugin:prepare-agent package jacoco:report'
sh 'curl https://raw.githubusercontent.com/jenkinsci/cobertura-plugin/ff0973461f60c28dbc0bf5ab4853a5aec4798210/src/test/resources/hudson/plugins/cobertura/coverage-with-lots-of-data.xml > coverage.xml'
}
}

stage('Jacoco Reports') {
steps {
recordCoverage(
tools: [
[parser: 'JACOCO'],
[parser: 'COBERTURA', pattern: '**/coverage.xml']
],
id: 'jacoco',
name: 'JaCoCo Coverage',
enabledForFailure: true,
sourceCodeRetention: 'EVERY_BUILD',
qualityGates: [
[threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', unstable: true],
[threshold: 60.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true]
]
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"spanId": "6ecee1eddbe32116",
"traceId": "12d895e96975a48dda404a618ab650f5",
"parent": "recordCoverage01",
"all-info": "span(name: recordCoverage, spanId: 6ecee1eddbe32116, parentSpanId: 74da9c1de6070122, traceId: 12d895e96975a48dda404a618ab650f5, attr: ci.pipeline.run.user:SYSTEM;harness-attribute:{\n \"name\" : \"JaCoCo Coverage\",\n \"qualityGates\" : [ {\n \"threshold\" : 6E+1,\n \"metric\" : \"LINE\",\n \"baseline\" : \"PROJECT\",\n \"unstable\" : true\n }, {\n \"threshold\" : 6E+1,\n \"metric\" : \"BRANCH\",\n \"baseline\" : \"PROJECT\",\n \"unstable\" : true\n } ],\n \"enabledForFailure\" : true,\n \"id\" : \"jacoco\",\n \"tools\" : [ {\n \"parser\" : \"JACOCO\"\n }, {\n \"parser\" : \"COBERTURA\",\n \"pattern\" : \"**/coverage.xml\"\n } ],\n \"sourceCodeRetention\" : \"EVERY_BUILD\"\n};harness-attribute-extra-pip: io.jenkins.plugins.opentelemetry.MigrateHarnessUrlChildAction@7784a684:io.jenkins.plugins.opentelemetry.MigrateHarnessUrlChildAction@7784a684;harness-attribute-extra-pip: org.jenkinsci.plugins.workflow.actions.TimingAction@163a6de0:org.jenkinsci.plugins.workflow.actions.TimingAction@163a6de0;harness-others:;jenkins.pipeline.step.id:50;jenkins.pipeline.step.name:Record code coverage results;jenkins.pipeline.step.plugin.name:coverage;jenkins.pipeline.step.plugin.version:1.16.1;jenkins.pipeline.step.type:recordCoverage;)",
"name": "recordCoverage01 #5",
"attributesMap": {
"harness-attribute-extra-pip: org.jenkinsci.plugins.workflow.actions.TimingAction@163a6de0": "org.jenkinsci.plugins.workflow.actions.TimingAction@163a6de0",
"harness-others": "",
"jenkins.pipeline.step.name": "Record code coverage results",
"ci.pipeline.run.user": "SYSTEM",
"jenkins.pipeline.step.id": "50",
"harness-attribute-extra-pip: io.jenkins.plugins.opentelemetry.MigrateHarnessUrlChildAction@7784a684": "io.jenkins.plugins.opentelemetry.MigrateHarnessUrlChildAction@7784a684",
"jenkins.pipeline.step.type": "recordCoverage",
"harness-attribute": "{\n \"name\" : \"JaCoCo Coverage\",\n \"qualityGates\" : [ {\n \"threshold\" : 6E+1,\n \"metric\" : \"LINE\",\n \"baseline\" : \"PROJECT\",\n \"unstable\" : true\n }, {\n \"threshold\" : 6E+1,\n \"metric\" : \"BRANCH\",\n \"baseline\" : \"PROJECT\",\n \"unstable\" : true\n } ],\n \"enabledForFailure\" : true,\n \"id\" : \"jacoco\",\n \"tools\" : [ {\n \"parser\" : \"JACOCO\"\n }, {\n \"parser\" : \"COBERTURA\",\n \"pattern\" : \"**/coverage.xml\"\n } ],\n \"sourceCodeRetention\" : \"EVERY_BUILD\"\n}",
"jenkins.pipeline.step.plugin.name": "coverage",
"jenkins.pipeline.step.plugin.version": "1.16.1"
},
"type": "Run Phase Span",
"parentSpanId": "74da9c1de6070122",
"parameterMap": {
"sourceCodeRetention": "EVERY_BUILD",
"name": "JaCoCo Coverage",
"qualityGates": [
{
"unstable": true,
"metric": "LINE",
"threshold": 60,
"baseline": "PROJECT"
},
{
"unstable": true,
"metric": "BRANCH",
"threshold": 60,
"baseline": "PROJECT"
}
],
"enabledForFailure": true,
"id": "jacoco",
"tools": [
{"parser": "JACOCO"},
{
"parser": "COBERTURA",
"pattern": "**/coverage.xml"
}
]
},
"spanName": "recordCoverage"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
- stepGroup:
identifier: Jacoco_reportse7f8bc
name: Jacoco reports
steps:
- step:
identifier: recordcoverage6ecee1
name: recordCoverage
spec:
image: plugins/coverage-report
settings:
fail_on_threshold: "true"
source_code_encoding: UTF-8
threshold_branch: "60"
threshold_line: "60"
tool: jacoco-xml
timeout: ""
type: Plugin
- step:
identifier: recordcoverage6ecee11
name: recordCoverage
spec:
image: plugins/coverage-report
settings:
fail_on_threshold: "true"
reports_path_pattern: '**/coverage.xml'
source_code_encoding: UTF-8
threshold_branch: "60"
threshold_line: "60"
tool: cobertura
timeout: ""
type: Plugin
102 changes: 102 additions & 0 deletions convert/jenkinsjson/json/recordCoverage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package json

import (
"fmt"
harness "github.com/drone/spec/dist/go"
)

var QualityGatesMap = map[string]interface{}{
"MODULE": "threshold_module",
"CLASS": "threshold_class",
"FILE": "threshold_file",
"PACKAGE": "threshold_package",
"LINE": "threshold_line",
"METHOD": "threshold_method",
"INSTRUCTION": "threshold_instruction",
"BRANCH": "threshold_branch",
"COMPLEXITY": "threshold_complexity",
"COMPLEXITY_DENSITY": "threshold_complexity_density",
"LOC": "threshold_loc",
}

func ConvertRecordCoverage(node Node, variables map[string]string) []*harness.Step {

s, err := ToJsonStringFromStruct(node)
if err != nil {
fmt.Println("Error converting json to struct:", err)
return nil
}

rcn, err := ToStructFromJsonString[RecordCoverageNode](s)
if err != nil {
fmt.Println("Error converting json to struct:", err)
return nil
}

var stepsMap []map[string]interface{}

for _, cvg := range rcn.ParameterMap.Tools {
stepMap := map[string]interface{}{}
switch cvg.Parser {
case "JACOCO":
stepMap["tool"] = "jacoco-xml"
case "COBERTURA":
stepMap["tool"] = "cobertura"
default:
fmt.Println("Unknown coverage parser:", cvg.Parser)
}
if cvg.Pattern != "" {
stepMap["reports_path_pattern"] = cvg.Pattern
}
stepMap["source_code_encoding"] = "UTF-8"
stepMap["fail_on_threshold"] = rcn.ParameterMap.EnabledForFailure

for _, qualityGate := range rcn.ParameterMap.QualityGates {
if val, ok := QualityGatesMap[qualityGate.Metric]; ok {
stepMap[val.(string)] = qualityGate.Threshold
} else {
fmt.Println("Unknown metric:", qualityGate.Metric)
}
}

stepsMap = append(stepsMap, stepMap)
}

var stepsList []*harness.Step
for _, stepMap := range stepsMap {
withProperties := map[string]interface{}{}
step := &harness.Step{
Name: node.SpanName,
Id: SanitizeForId(node.SpanName, node.SpanId),
Type: "plugin",
Spec: &harness.StepPlugin{
Image: "plugins/coverage-report",
Inputs: withProperties,
With: withProperties,
},
}
step.Spec.(*harness.StepPlugin).With = stepMap
stepsList = append(stepsList, step)
}

return stepsList
}

type RecordCoverageNode struct {
ParameterMap struct {
SourceCodeRetention string `json:"sourceCodeRetention"`
Name string `json:"name"`
QualityGates []struct {
Unstable bool `json:"unstable"`
Metric string `json:"metric"`
Threshold float64 `json:"threshold"`
Baseline string `json:"baseline"`
} `json:"qualityGates"`
EnabledForFailure bool `json:"enabledForFailure"`
Id string `json:"id"`
Tools []struct {
Parser string `json:"parser"`
Pattern string `json:"pattern,omitempty"`
} `json:"tools"`
} `json:"parameterMap"`
}
56 changes: 56 additions & 0 deletions convert/jenkinsjson/json/recordCoverage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package json

import (
"encoding/json"
"github.com/google/go-cmp/cmp"
"os"
"path/filepath"
"testing"
)

func TestRecordCoverage(t *testing.T) {
jsonFilePath := "../convertTestFiles/recordCoverage/recordCoverage.json"
workingDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current working directory: %v", err)
}

filePath := filepath.Join(workingDir, jsonFilePath)
jsonData, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read JSON file: %v", err)
}

var node Node
if err := json.Unmarshal(jsonData, &node); err != nil {
t.Fatalf("Failed to decode JSON: %v", err)
}

tmpTestStepsList := ConvertRecordCoverage(node, make(map[string]string))
if len(tmpTestStepsList) == 0 {
t.Fatalf("Failed to convert JSON to struct")
}

for i, tmpTestStep := range tmpTestStepsList {
var expectedMap map[string]interface{}
if err := json.Unmarshal([]byte(expectedRecordCoverageStepsList[i]), &expectedMap); err != nil {
t.Fatalf("Failed to convert expected JSON to map: %v", err)
}
jsonBytes, err := json.Marshal(tmpTestStep)
if err != nil {
t.Fatalf("Failed to marshal struct: %v", err)
}
var actualMap map[string]interface{}
if err := json.Unmarshal(jsonBytes, &actualMap); err != nil {
t.Fatalf("Failed to unmarshal JSON to map: %v", err)
}
if diff := cmp.Diff(expectedMap, actualMap); diff != "" {
t.Fatalf("JSON comparison failed:\n%s", diff)
}
}
}

var expectedRecordCoverageStepsList = []string{
`{"id":"recordCoverage6ecee1","name":"recordCoverage","type":"plugin","spec":{"image":"plugins/coverage-report","with":{"fail_on_threshold":true,"source_code_encoding":"UTF-8","threshold_branch":60,"threshold_line":60,"tool":"jacoco-xml"}}}`,
`{"id":"recordCoverage6ecee1","name":"recordCoverage","type":"plugin","spec":{"image":"plugins/coverage-report","with":{"fail_on_threshold":true,"reports_path_pattern":"**/coverage.xml","source_code_encoding":"UTF-8","threshold_branch":60,"threshold_line":60,"tool":"cobertura"}}}`,
}
43 changes: 42 additions & 1 deletion samples/jenkins/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -1400,5 +1400,46 @@ line3'''
}
}
}
}

// recordCoverage step
stages {
stage('Checkout SCM') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: "*/master"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'CleanCheckout']],
submoduleCfg: [],
userRemoteConfigs: [[url: 'https://github.com/syamv/game-of-life.git']]
])
}
}

// Using Maven for build
stage('Maven Build') {
steps {
sh 'mvn clean verify org.jacoco:jacoco-maven-plugin:prepare-agent package jacoco:report'
sh 'curl https://raw.githubusercontent.com/jenkinsci/cobertura-plugin/ff0973461f60c28dbc0bf5ab4853a5aec4798210/src/test/resources/hudson/plugins/cobertura/coverage-with-lots-of-data.xml > coverage.xml'
}
}

stage('Jacoco Reports') {
steps {
recordCoverage(
tools: [
[parser: 'JACOCO'],
[parser: 'COBERTURA', pattern: '**/coverage.xml']
],
id: 'jacoco',
name: 'JaCoCo Coverage',
enabledForFailure: true,
sourceCodeRetention: 'EVERY_BUILD',
qualityGates: [
[threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', unstable: true],
[threshold: 60.0, metric: 'BRANCH', baseline: 'PROJECT', unstable: true]
]
)
}
}
}
}