1919 */
2020package org .sonar .plugins .python .coverage ;
2121
22+ import org .apache .commons .io .FilenameUtils ;
2223import org .sonar .api .Properties ;
2324import org .sonar .api .Property ;
25+ import org .sonar .api .PropertyType ;
2426import org .sonar .api .batch .SensorContext ;
2527import org .sonar .api .config .Settings ;
2628import org .sonar .api .measures .CoreMetrics ;
2729import org .sonar .api .measures .CoverageMeasuresBuilder ;
2830import org .sonar .api .measures .Measure ;
2931import org .sonar .api .measures .Metric ;
32+ import org .sonar .api .measures .PropertiesBuilder ;
3033import org .sonar .api .resources .Project ;
3134import org .sonar .api .scan .filesystem .ModuleFileSystem ;
35+ import org .sonar .api .scan .filesystem .FileQuery ;
3236import org .sonar .api .utils .SonarException ;
3337import org .sonar .plugins .python .PythonReportSensor ;
38+ import org .sonar .plugins .python .Python ;
3439
3540import javax .xml .stream .XMLStreamException ;
3641
3742import java .io .File ;
43+ import java .nio .file .Path ;
44+ import java .nio .file .Paths ;
3845import java .util .HashMap ;
3946import java .util .List ;
4047import java .util .Map ;
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})
6580public 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