Skip to content

Commit 1209dfa

Browse files
Switched to source directory relative pathes, sanitizing reported pathes
Justification: - sanitizing the reported file pathes to a common format makes the sensor simpler - it is a precondition for implementing proper directory coverage - tests run now under unix and windows, easier to develop The sonar.sources property can be read from the settings. By mapping all reported pathes (absolute / base dir relative / source dir relative) against the actual file tree it is possible to converte all of them to the same source dir relative format. For instance /home/src/main/scala/folder/test0.scala => folder/test0.scala folder/test1.scala => folder/test1.scala /src/main/scala/test2.scala => test2.scala ...
1 parent 6d1afc9 commit 1209dfa

File tree

9 files changed

+212
-48
lines changed

9 files changed

+212
-48
lines changed

plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
*/
2020
package com.buransky.plugins.scoverage
2121

22+
import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
23+
2224
/**
2325
* Interface for Scoverage report parser.
2426
*
2527
* @author Rado Buransky
2628
*/
2729
trait ScoverageReportParser {
28-
def parse(reportFilePath: String): ProjectStatementCoverage
30+
def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage
2931
}
3032

3133
/**
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Sonar Scoverage Plugin
3+
* Copyright (C) 2013 Rado Buransky
4+
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public
17+
* License along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
19+
*/
20+
package com.buransky.plugins.scoverage.pathcleaner
21+
22+
import java.io.File
23+
import org.apache.commons.io.FileUtils
24+
import org.apache.commons.io.FileUtils
25+
import BruteForceSequenceMatcher._
26+
import com.buransky.plugins.scoverage.util.PathUtil
27+
import scala.collection.JavaConversions._
28+
import org.sonar.api.utils.log.Loggers
29+
30+
object BruteForceSequenceMatcher {
31+
32+
val extensions = Array[String]("java", "scala")
33+
34+
type PathSeq = Seq[String]
35+
}
36+
37+
/**
38+
* Helper that allows to convert a report path into a source folder relative path by testing it against
39+
* the tree of source files.
40+
*
41+
* Assumes that all report paths of a given report have a common root. Dependent of the scoverage
42+
* report this root is either something outside the actual project (absolute path), the base dir of the project
43+
* (report path relative to base dir) or some sub folder of the project.
44+
*
45+
* By reverse mapping a report path against the tree of all file children of the source folder the correct filesystem file
46+
* can be found and the report path can be converted into a source dir relative path. *
47+
*
48+
* @author Michael Zinsmaier
49+
*/
50+
class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathSanitizer {
51+
52+
private val sourceDir = initSourceDir()
53+
require(sourceDir.isAbsolute)
54+
require(sourceDir.isDirectory)
55+
56+
private val log = Loggers.get(classOf[BruteForceSequenceMatcher])
57+
private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size
58+
private val filesMap = initFilesMap()
59+
60+
61+
def getSourceRelativePath(reportPath: PathSeq): Option[PathSeq] = {
62+
// match with file system map of files
63+
val relPathOption = for {
64+
absPathCandidates <- filesMap.get(reportPath.last)
65+
path <- absPathCandidates.find(absPath => absPath.endsWith(reportPath))
66+
} yield path.drop(sourcePathLength)
67+
68+
relPathOption
69+
}
70+
71+
// mock able helpers that allow us to remove the dependency to the real file system during tests
72+
73+
private[pathcleaner] def initSourceDir(): File = {
74+
val sourceDir = new File(baseDir, sourcePath)
75+
sourceDir
76+
}
77+
78+
private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = {
79+
val srcFiles = FileUtils.iterateFiles(sourceDir, extensions, true)
80+
val paths = srcFiles.map(file => PathUtil.splitPath(file.getAbsolutePath)).toSeq
81+
82+
// group them by filename, in case multiple files have the same name
83+
paths.groupBy(path => path.last)
84+
}
85+
86+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Sonar Scoverage Plugin
3+
* Copyright (C) 2013 Rado Buransky
4+
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public
17+
* License along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
19+
*/
20+
package com.buransky.plugins.scoverage.pathcleaner
21+
22+
/**
23+
* @author Michael Zinsmaier
24+
*/
25+
trait PathSanitizer {
26+
27+
/** tries to convert the given path such that it is relative to the
28+
* projects/modules source directory.
29+
*
30+
* @return Some(source folder relative path) or None if the path cannot be converted
31+
*/
32+
def getSourceRelativePath(path: Seq[String]): Option[Seq[String]]
33+
34+
}

plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import org.sonar.api.scan.filesystem.PathResolver
3535
import org.sonar.api.utils.log.Loggers
3636

3737
import scala.collection.JavaConversions._
38+
import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher
39+
import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
3840

3941
/**
4042
* Main sensor for importing Scoverage report to Sonar.
@@ -53,7 +55,16 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem
5355
scoverageReportPath match {
5456
case Some(reportPath) =>
5557
// Single-module project
56-
processProject(scoverageReportParser.parse(reportPath), project, context)
58+
val srcOption = Option(settings.getString(project.getName() + ".sonar.sources"))
59+
val sonarSources = srcOption match {
60+
case Some(src) => src
61+
case None => {
62+
log.warn(s"could not find settings key ${project.getName()}.sonar.sources assuming src/main/scala.")
63+
"src/main/scala"
64+
}
65+
}
66+
val pathSanitizer = createPathSanitizer(sonarSources)
67+
processProject(scoverageReportParser.parse(reportPath, pathSanitizer), project, context, sonarSources)
5768

5869
case None =>
5970
// Multi-module project has report path set for each module individually
@@ -63,6 +74,9 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem
6374

6475
override val toString = getClass.getSimpleName
6576

77+
protected def createPathSanitizer(sonarSources: String): PathSanitizer
78+
= new BruteForceSequenceMatcher(fileSystem.baseDir(), sonarSources)
79+
6680
private lazy val scoverageReportPath: Option[String] = {
6781
settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match {
6882
case null => None
@@ -127,14 +141,14 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem
127141
}
128142
}
129143

130-
private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext) {
144+
private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext, sonarSources: String) {
131145
// Save measures
132146
saveMeasures(context, project, projectCoverage)
133147

134148
log.info(LogUtil.f("Statement coverage for " + project.getKey + " is " + ("%1.2f" format projectCoverage.rate)))
135149

136150
// Process children
137-
processChildren(projectCoverage.children, context, "")
151+
processChildren(projectCoverage.children, context, sonarSources)
138152
}
139153

140154
private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext,
@@ -147,9 +161,8 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem
147161
val path = appendFilePath(directory, fileCoverage.name)
148162
val p = fileSystem.predicates()
149163

150-
val pathPredicate = if (new io.File(path).isAbsolute) p.hasAbsolutePath(path) else p.matchesPathPattern("**/" + path)
151164
val files = fileSystem.inputFiles(p.and(
152-
pathPredicate,
165+
p.hasRelativePath(path),
153166
p.hasLanguage(scala.getKey),
154167
p.hasType(InputFile.Type.MAIN))).toList
155168

@@ -164,10 +177,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem
164177
saveLineCoverage(fileCoverage.statements, scalaSourceFile, context)
165178

166179
case None => {
167-
fileSystem.inputFiles(p.all()).foreach { inputFile =>
168-
log.debug(inputFile.absolutePath())
169-
}
170-
log.warn(s"File not found in file system! [$pathPredicate]")
180+
log.warn(s"File not found in file system! [$path]")
171181
}
172182
}
173183
}

plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ import scala.collection.mutable
3030
import scala.io.Source
3131
import scala.xml.parsing.ConstructingParser
3232
import scala.xml.{MetaData, NamespaceBinding, Text}
33+
import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
3334

3435
/**
3536
* Scoverage XML parser based on ConstructingParser provided by standard Scala library.
3637
*
3738
* @author Rado Buransky
3839
*/
39-
class XmlScoverageReportConstructingParser(source: Source) extends ConstructingParser(source, false) {
40+
class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSanitizer) extends ConstructingParser(source, false) {
4041
private val log = Loggers.get(classOf[XmlScoverageReportConstructingParser])
4142

4243
private val CLASS_ELEMENT = "class"
@@ -163,7 +164,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP
163164
val files = fileStatementCoverage(statementsInFile)
164165

165166
// Transform file paths to chain of case classes
166-
val chained = files.map(fsc => pathToChain(fsc._1, fsc._2))
167+
val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)).flatten
167168

168169
// Merge chains into one tree
169170
val root = DirOrFile("", Nil, None)
@@ -173,31 +174,42 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP
173174
root.toProjectStatementCoverage
174175
}
175176

176-
private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = {
177+
private def pathToChain(filePath: String, coverage: FileStatementCoverage): Option[DirOrFile] = {
178+
// helper
179+
def convertToDirOrFile(relPath: Seq[String]) = {
180+
// Get directories
181+
val dirs = for (i <- 0 to relPath.length - 2)
182+
yield DirOrFile(relPath(i), Nil, None)
183+
184+
// Chain directories
185+
for (i <- 0 to dirs.length - 2)
186+
dirs(i).children = List(dirs(i + 1))
187+
188+
// Get file
189+
val file = DirOrFile(relPath(relPath.length - 1).toString, Nil, Some(coverage))
190+
191+
if (dirs.isEmpty) {
192+
// File in root dir
193+
file
194+
} else {
195+
// Append file
196+
dirs.last.children = List(file)
197+
dirs.head
198+
}
199+
}
200+
201+
// processing
177202
val path = PathUtil.splitPath(filePath)
178203

179204
if (path.length < 1)
180205
throw new ScoverageException("Path cannot be empty!")
181206

182-
// Get directories
183-
val dirs = for (i <- 0 to path.length - 2)
184-
yield DirOrFile(path(i), Nil, None)
185-
186-
// Chain directories
187-
for (i <- 0 to dirs.length - 2)
188-
dirs(i).children = List(dirs(i + 1))
189-
190-
// Get file
191-
val file = DirOrFile(path(path.length - 1).toString, Nil, Some(coverage))
192-
193-
if (dirs.isEmpty) {
194-
// File in root dir
195-
file
196-
}
197-
else {
198-
// Append file
199-
dirs.last.children = List(file)
200-
dirs.head
207+
pathSanitizer.getSourceRelativePath(path) match {
208+
case Some(relPath) => Some(convertToDirOrFile(relPath))
209+
case None => {
210+
log.warn(s"skipping file coverage results for $path, was not able to retrieve the file in the configured source dir")
211+
None
212+
}
201213
}
202214
}
203215

plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageExcept
2424
import org.sonar.api.utils.log.Loggers
2525

2626
import scala.io.Source
27+
import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
2728

2829
/**
2930
* Bridge between parser implementation and coverage provider.
@@ -33,13 +34,13 @@ import scala.io.Source
3334
class XmlScoverageReportParser extends ScoverageReportParser {
3435
private val log = Loggers.get(classOf[XmlScoverageReportParser])
3536

36-
def parse(reportFilePath: String): ProjectStatementCoverage = {
37+
def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage = {
3738
require(reportFilePath != null)
3839
require(!reportFilePath.trim.isEmpty)
3940

4041
log.debug(LogUtil.f("Reading report. [" + reportFilePath + "]"))
4142

42-
val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath))
43+
val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath), pathSanitizer)
4344
parser.parse()
4445
}
4546

plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import org.sonar.api.resources.Project.AnalysisType
3636
import org.sonar.api.scan.filesystem.PathResolver
3737

3838
import scala.collection.JavaConversions._
39+
import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
40+
import org.mockito.Matchers.any
3941

4042

4143
@RunWith(classOf[JUnitRunner])
@@ -94,22 +96,20 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar {
9496
when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport)
9597
when(fileSystem.baseDir).thenReturn(moduleBaseDir)
9698
when(fileSystem.predicates).thenReturn(filePredicates)
97-
when(fileSystem.inputFiles(org.mockito.Matchers.any[FilePredicate]())).thenReturn(Nil)
99+
when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil)
98100
when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile)
99-
when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage)
101+
when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage)
100102

101103
// Execute
102104
analyse(project, context)
103-
104-
verify(filePredicates).hasAbsolutePath("/home/a.scala")
105-
verify(filePredicates).matchesPathPattern("**/x/b.scala")
106105
}
107106

108107
class AnalyseScoverageSensorScope extends ScoverageSensorScope {
109108
val project = mock[Project]
110109
val context = new TestSensorContext
111110

112111
override protected lazy val scoverageReportParser = mock[ScoverageReportParser]
112+
override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer]
113113
}
114114

115115
class ScoverageSensorScope extends {

0 commit comments

Comments
 (0)