Skip to content

Commit d50ac6f

Browse files
author
Waleri Enns
committed
SONARPY-41: Zero coverage for all files, which are missing in the coverage reports
1 parent fad51ff commit d50ac6f

6 files changed

Lines changed: 192 additions & 33 deletions

File tree

integration-tests/features/importing_coverage.feature

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ Feature: Importing coverage data
44
I want to import my coverage metric values into SonarQube
55
In order to be able to use relevant SonarQube features
66

7-
Scenario: Importing a valid coverage report
7+
8+
Scenario: Importing coverage reports
89
GIVEN the python project "coverage_project"
910

10-
WHEN I run "sonar-runner"
11+
WHEN I run sonar-runner with following options:
12+
"""
13+
-Dsonar.python.coverage.reportPath=ut-coverage.xml
14+
-Dsonar.python.coverage.itReportPath=it-coverage.xml
15+
-Dsonar.python.coverage.overallReportPath=it-coverage.xml
16+
"""
1117

1218
THEN the analysis finishes successfully
1319
AND the analysis log contains no error or warning messages
@@ -20,5 +26,58 @@ Feature: Importing coverage data
2026
| it_line_coverage | 50 |
2127
| it_branch_coverage | 0 |
2228
| overall_coverage | 40 |
23-
| overall_line_coverage | 50 |
24-
| overall_branch_coverage | 0 |
29+
| overall_line_coverage | 50 |
30+
| overall_branch_coverage | 0 |
31+
32+
33+
Scenario: Importing coverage reports zeroing coverage for untouched files
34+
GIVEN the python project "coverage_project"
35+
36+
WHEN I run sonar-runner with following options:
37+
"""
38+
-Dsonar.python.coverage.reportPath=ut-coverage.xml
39+
-Dsonar.python.coverage.itReportPath=it-coverage.xml
40+
-Dsonar.python.coverage.overallReportPath=it-coverage.xml
41+
-Dsonar.python.coverage.forceZeroCoverage=True
42+
"""
43+
44+
THEN the analysis finishes successfully
45+
AND the analysis log contains no error or warning messages
46+
AND the following metrics have following values:
47+
| metric | value |
48+
| coverage | 50 |
49+
| line_coverage | 42.9 |
50+
| branch_coverage | 100 |
51+
| it_coverage | 25 |
52+
| it_line_coverage | 28.6 |
53+
| it_branch_coverage | 0 |
54+
| overall_coverage | 25 |
55+
| overall_line_coverage | 28.6 |
56+
| overall_branch_coverage | 0 |
57+
58+
59+
Scenario: Zeroing coverage measures without importing reports
60+
61+
If we dont pass coverage reports *and* request zeroing untouched
62+
files at the same time, all coverage measures, except the branch
63+
ones, should be 'zero'. The branch coverage measures remain 'None',
64+
since its currently ignored by the 'force zero...'
65+
implementation
66+
67+
GIVEN the python project "coverage_project"
68+
69+
WHEN I run "sonar-runner -Dsonar.python.coverage.forceZeroCoverage=True"
70+
71+
THEN the analysis finishes successfully
72+
AND the analysis log contains no error or warning messages
73+
AND the following metrics have following values:
74+
| metric | value |
75+
| coverage | 0 |
76+
| line_coverage | 0 |
77+
| branch_coverage | None |
78+
| it_coverage | 0 |
79+
| it_line_coverage | 0 |
80+
| it_branch_coverage | None |
81+
| overall_coverage | 0 |
82+
| overall_line_coverage | 0 |
83+
| overall_branch_coverage | None |

integration-tests/features/steps/test_execution_statistics.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,7 @@ def step_impl(context, project):
4242

4343
@when(u'I run "{command}"')
4444
def step_impl(context, command):
45-
context.log = "_%s_.log" % context.project
46-
projecthome = os.path.join(TESTDATADIR, context.project)
47-
with open(context.log, "w") as logfile:
48-
rc = subprocess.call(command,
49-
cwd=projecthome,
50-
stdout=logfile, stderr=logfile,
51-
shell=True)
52-
context.rc = rc
45+
_run_command(context, command)
5346

5447

5548
@then(u'the analysis finishes successfully')
@@ -60,7 +53,7 @@ def step_impl(context):
6053
@then(u'the analysis log contains no error or warning messages')
6154
def step_impl(context):
6255
badlines, _errors, _warnings = analyselog(context.log)
63-
56+
6457
assert len(badlines) == 0,\
6558
("Found following errors and/or warnings lines in the logfile:\n"
6659
+ "".join(badlines)
@@ -115,3 +108,21 @@ def step_impl(context):
115108
if pattern.match(line):
116109
return True
117110
return False
111+
112+
113+
@when(u'I run sonar-runner with following options')
114+
def step_impl(context):
115+
arguments = [line for line in context.text.split("\n") if line != '']
116+
command = "sonar-runner " + " ".join(arguments)
117+
_run_command(context, command)
118+
119+
120+
def _run_command(context, command):
121+
context.log = "_%s_.log" % context.project
122+
projecthome = os.path.join(TESTDATADIR, context.project)
123+
with open(context.log, "w") as logfile:
124+
rc = subprocess.call(command,
125+
cwd=projecthome,
126+
stdout=logfile, stderr=logfile,
127+
shell=True)
128+
context.rc = rc

integration-tests/testdata/coverage_project/sonar-project.properties

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,3 @@ sonar.sourceEncoding=UTF8
77
# Comma-separated paths to directories with sources (required)
88
sonar.sources=src
99
sonar.exclusions=**/test_*.py
10-
11-
sonar.python.coverage.reportPath=ut-coverage.xml
12-
sonar.python.coverage.itReportPath=it-coverage.xml
13-
# TODO: pass proper overall coverage
14-
sonar.python.coverage.overallReportPath=it-coverage.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def some_function(n):
2+
for i in range(100):
3+
pass
4+
5+
i = 0
6+
j = 1
7+
k = 2

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.codehaus.sonar-plugins</groupId>
77
<artifactId>parent</artifactId>
8-
<version>18</version>
8+
<version>19</version>
99
</parent>
1010

1111
<groupId>org.codehaus.sonar-plugins.python</groupId>

sonar-python-plugin/src/main/java/org/sonar/plugins/python/coverage/PythonCoverageSensor.java

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,29 @@
1919
*/
2020
package org.sonar.plugins.python.coverage;
2121

22+
import org.apache.commons.io.FilenameUtils;
2223
import org.sonar.api.Properties;
2324
import org.sonar.api.Property;
25+
import org.sonar.api.PropertyType;
2426
import org.sonar.api.batch.SensorContext;
2527
import org.sonar.api.config.Settings;
2628
import org.sonar.api.measures.CoreMetrics;
2729
import org.sonar.api.measures.CoverageMeasuresBuilder;
2830
import org.sonar.api.measures.Measure;
2931
import org.sonar.api.measures.Metric;
32+
import org.sonar.api.measures.PropertiesBuilder;
3033
import org.sonar.api.resources.Project;
3134
import org.sonar.api.scan.filesystem.ModuleFileSystem;
35+
import org.sonar.api.scan.filesystem.FileQuery;
3236
import org.sonar.api.utils.SonarException;
3337
import org.sonar.plugins.python.PythonReportSensor;
38+
import org.sonar.plugins.python.Python;
3439

3540
import javax.xml.stream.XMLStreamException;
3641

3742
import java.io.File;
43+
import java.nio.file.Path;
44+
import java.nio.file.Paths;
3845
import java.util.HashMap;
3946
import java.util.List;
4047
import java.util.Map;
@@ -60,12 +67,20 @@
6067
name = "Path to overall (combined UT+IT) coverage report(s)",
6168
description = "Path to a report containing overall test coverage data (i.e. test coverage gained by all tests of all kinds), relative to projects root. Ant patterns are accepted. The reports have to conform to the Cobertura XML format.",
6269
global = false,
70+
project = true),
71+
@Property(
72+
key = PythonCoverageSensor.FORCE_ZERO_COVERAGE_KEY,
73+
type = PropertyType.BOOLEAN,
74+
defaultValue = "false",
75+
name = "Assign zero line coverage to source files without coverage report(s)",
76+
description = "If 'True', assign zero line coverage to source files without coverage report(s), which results in a more realistic overall Technical Debt value.",
77+
global = false,
6378
project = true)
6479
})
6580
public class PythonCoverageSensor extends PythonReportSensor {
66-
private static final int UNIT_TEST_COVERAGE = 0;
67-
public static final int IT_TEST_COVERAGE = 1;
68-
public static final int OVERALL_TEST_COVERAGE = 2;
81+
private enum CoverageType {
82+
UT_COVERAGE, IT_COVERAGE, OVERALL_COVERAGE
83+
}
6984

7085
public static final String REPORT_PATH_KEY = "sonar.python.coverage.reportPath";
7186
public static final String IT_REPORT_PATH_KEY = "sonar.python.coverage.itReportPath";
@@ -74,6 +89,8 @@ public class PythonCoverageSensor extends PythonReportSensor {
7489
public static final String IT_DEFAULT_REPORT_PATH = "coverage-reports/it-coverage-*.xml";
7590
public static final String OVERALL_DEFAULT_REPORT_PATH = "coverage-reports/overall-coverage-*.xml";
7691

92+
public static final String FORCE_ZERO_COVERAGE_KEY = "sonar.python.coverage.forceZeroCoverage";
93+
7794
private CoberturaParser parser = new CoberturaParser();
7895

7996
public PythonCoverageSensor(Settings conf, ModuleFileSystem fileSystem) {
@@ -87,19 +104,92 @@ public void analyse(Project project, SensorContext context) {
87104
List<File> reports = getReports(conf, baseDir, REPORT_PATH_KEY, DEFAULT_REPORT_PATH);
88105
LOG.debug("Parsing coverage reports");
89106
Map<String, CoverageMeasuresBuilder> coverageMeasures = parseReports(reports);
90-
saveMeasures(project, context, coverageMeasures, UNIT_TEST_COVERAGE);
107+
saveMeasures(project, context, coverageMeasures, CoverageType.UT_COVERAGE);
91108

92109
LOG.debug("Parsing integration test coverage reports");
93110
List<File> itReports = getReports(conf, baseDir, IT_REPORT_PATH_KEY, IT_DEFAULT_REPORT_PATH);
94-
coverageMeasures = parseReports(itReports);
95-
saveMeasures(project, context, coverageMeasures, IT_TEST_COVERAGE);
111+
Map<String, CoverageMeasuresBuilder> itCoverageMeasures = parseReports(itReports);
112+
saveMeasures(project, context, itCoverageMeasures, CoverageType.IT_COVERAGE);
96113

97114
LOG.debug("Parsing overall test coverage reports");
98115
List<File> overallReports = getReports(conf, baseDir, OVERALL_REPORT_PATH_KEY, OVERALL_DEFAULT_REPORT_PATH);
99116
Map<String, CoverageMeasuresBuilder> overallCoverageMeasures = parseReports(overallReports);
100-
saveMeasures(project, context, overallCoverageMeasures, OVERALL_TEST_COVERAGE);
117+
saveMeasures(project, context, overallCoverageMeasures, CoverageType.OVERALL_COVERAGE);
118+
119+
if (conf.getBoolean(FORCE_ZERO_COVERAGE_KEY)){
120+
LOG.debug("Zeroing coverage information for untouched files");
121+
122+
zeroMeasuresWithoutReports(project, context, coverageMeasures,
123+
itCoverageMeasures, overallCoverageMeasures);
124+
}
125+
}
126+
127+
private void zeroMeasuresWithoutReports(Project project,
128+
SensorContext context,
129+
Map<String, CoverageMeasuresBuilder> coverageMeasures,
130+
Map<String, CoverageMeasuresBuilder> itCoverageMeasures,
131+
Map<String, CoverageMeasuresBuilder> overallCoverageMeasures
132+
) {
133+
for (File file : fileSystem.files(FileQuery.onSource().onLanguage(Python.KEY))) {
134+
org.sonar.api.resources.File resource = org.sonar.api.resources.File.fromIOFile(file, project);
135+
if (fileExist(context, resource)) {
136+
137+
Path baseDir = Paths.get(FilenameUtils.normalize(fileSystem.baseDir().getPath()));
138+
String filePath = baseDir.relativize(Paths.get(file.getAbsolutePath())).toString();
139+
140+
if (coverageMeasures.get(filePath) == null) {
141+
saveZeroValueForResource(resource, filePath, context, CoverageType.UT_COVERAGE);
142+
}
143+
144+
if (itCoverageMeasures.get(filePath) == null) {
145+
saveZeroValueForResource(resource, filePath, context, CoverageType.IT_COVERAGE);
146+
}
147+
148+
if (overallCoverageMeasures.get(filePath) == null) {
149+
saveZeroValueForResource(resource, filePath, context, CoverageType.OVERALL_COVERAGE);
150+
}
151+
}
152+
}
101153
}
102154

155+
private void saveZeroValueForResource(org.sonar.api.resources.File resource,
156+
String filePath, SensorContext context,
157+
CoverageType ctype) {
158+
Measure ncloc = context.getMeasure(resource, CoreMetrics.NCLOC);
159+
if (ncloc != null && ncloc.getValue() > 0) {
160+
String coverageKind = "unit test ";
161+
Metric hitsDataMetric = CoreMetrics.COVERAGE_LINE_HITS_DATA;
162+
Metric linesToCoverMetric = CoreMetrics.LINES_TO_COVER;
163+
Metric uncoveredLinesMetric = CoreMetrics.UNCOVERED_LINES;
164+
165+
switch(ctype){
166+
case IT_COVERAGE:
167+
coverageKind = "integration test ";
168+
hitsDataMetric = CoreMetrics.IT_COVERAGE_LINE_HITS_DATA;
169+
linesToCoverMetric = CoreMetrics.IT_LINES_TO_COVER;
170+
uncoveredLinesMetric = CoreMetrics.IT_UNCOVERED_LINES;
171+
break;
172+
case OVERALL_COVERAGE:
173+
coverageKind = "overall ";
174+
hitsDataMetric = CoreMetrics.OVERALL_COVERAGE_LINE_HITS_DATA;
175+
linesToCoverMetric = CoreMetrics.OVERALL_LINES_TO_COVER;
176+
uncoveredLinesMetric = CoreMetrics.OVERALL_UNCOVERED_LINES;
177+
default:
178+
}
179+
180+
LOG.debug("Zeroing {}coverage measures for file '{}'", coverageKind, filePath);
181+
182+
PropertiesBuilder<Integer, Integer> lineHitsData = new PropertiesBuilder<Integer, Integer>(hitsDataMetric);
183+
for (int i = 1; i <= context.getMeasure(resource, CoreMetrics.LINES).getIntValue(); ++i) {
184+
lineHitsData.add(i, 0);
185+
}
186+
context.saveMeasure(resource, lineHitsData.build());
187+
context.saveMeasure(resource, linesToCoverMetric, ncloc.getValue());
188+
context.saveMeasure(resource, uncoveredLinesMetric, ncloc.getValue());
189+
}
190+
}
191+
192+
103193
private Map<String, CoverageMeasuresBuilder> parseReports(List<File> reports) {
104194
Map<String, CoverageMeasuresBuilder> coverageMeasures = new HashMap<String, CoverageMeasuresBuilder>();
105195
for (File report : reports) {
@@ -115,25 +205,22 @@ private Map<String, CoverageMeasuresBuilder> parseReports(List<File> reports) {
115205
private void saveMeasures(Project project,
116206
SensorContext context,
117207
Map<String, CoverageMeasuresBuilder> coverageMeasures,
118-
int coveragetype) {
208+
CoverageType ctype) {
119209
FileResolver fileResolver = new FileResolver(project, fileSystem);
120210
for(Map.Entry<String, CoverageMeasuresBuilder> entry: coverageMeasures.entrySet()) {
121211
String filePath = entry.getKey();
122212
org.sonar.api.resources.File pythonfile = fileResolver.getFile(filePath);
123213
if (fileExist(context, pythonfile)) {
124214
LOG.debug("Saving coverage measures for file '{}'", filePath);
125215
for (Measure measure : entry.getValue().createMeasures()) {
126-
switch (coveragetype) {
127-
case UNIT_TEST_COVERAGE:
128-
break;
129-
case IT_TEST_COVERAGE:
216+
switch (ctype) {
217+
case IT_COVERAGE:
130218
measure = convertToItMeasure(measure);
131219
break;
132-
case OVERALL_TEST_COVERAGE:
220+
case OVERALL_COVERAGE:
133221
measure = convertForOverall(measure);
134222
break;
135223
default:
136-
break;
137224
}
138225
context.saveMeasure(pythonfile, measure);
139226
}

0 commit comments

Comments
 (0)