This repository has been archived by the owner on Jan 30, 2023. It is now read-only.
JUnit report sensor #143
Merged
Merged
JUnit report sensor #143
Changes from 14 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
40f6c3b
WIP Unit tests sensor.
mwz 00f8a1e
Tweak debug messages.
mwz 657983a
Save test metrics.
mwz f019abe
Renamed the sensor; improved logging.
mwz a21ffd5
Update log tests.
mwz 6a57a7a
Address some PR comments.
mwz ce6ecf8
Minor refactoring of file resolution.
mwz 06964ad
Improved logging.
mwz 34dfb6b
Improve logging.
mwz bf3686c
Update logic for filtering junit report files.
mwz ca4a468
Refactor utils.
mwz 7c5e8cd
Refactor utils.
mwz 51aa893
Add config syntax tests.
mwz 6644a75
Add some sensor tests.
mwz c8d9884
Add file system syntax tests.
mwz 4903118
Add sensor context syntax tests.
mwz 79634e0
Add a syntax package object.
mwz 76ba759
Refactor syntax into implicit classes.
mwz dd0446f
Refactor option conversions.
mwz 08cf5b6
Add a new file system test.
mwz 1ec5cc5
More sensor tests.
mwz 50e6a5d
Report parser tests.
mwz 8d4f1c8
Remove a trailing comma.
mwz 3a5f129
Change logging level.
mwz bd2e976
Add sonar.tests property to the example set projects.
mwz e58775f
Make tweaks to filtering of junit files.
mwz 5c4d653
Make parsing junit reports safer.
mwz 4607cef
Update example mvn projects.
mwz 036d17e
Update example grade projects.
mwz 2df6b1c
Use getStringArray to get a list of paths from the config.
mwz b1411d7
Drop the ’sonar.junit.disable’ property (the sensor isn’t executed wi…
mwz e6c9f0f
Rename defaultPath to DefaultPaths.
mwz File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/main/scala/com/mwz/sonar/scala/junit/JUnitReport.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Sonar Scala Plugin | ||
* Copyright (C) 2018 All contributors | ||
* | ||
* This program is free software; you can redistribute it and/or | ||
* modify it under the terms of the GNU Lesser General Public | ||
* License as published by the Free Software Foundation; either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with this program; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
*/ | ||
package com.mwz.sonar.scala | ||
package junit | ||
|
||
private[junit] final case class JUnitReport( | ||
name: String, | ||
tests: Int, | ||
errors: Int, | ||
failures: Int, | ||
skipped: Int, | ||
time: Float | ||
) |
127 changes: 127 additions & 0 deletions
127
src/main/scala/com/mwz/sonar/scala/junit/JUnitReportParser.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
* Sonar Scala Plugin | ||
* Copyright (C) 2018 All contributors | ||
* | ||
* This program is free software; you can redistribute it and/or | ||
* modify it under the terms of the GNU Lesser General Public | ||
* License as published by the Free Software Foundation; either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with this program; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
*/ | ||
package com.mwz.sonar.scala | ||
package junit | ||
|
||
import java.io.File | ||
import java.nio.file.Path | ||
|
||
import com.mwz.sonar.scala.util.Log | ||
import org.sonar.api.batch.ScannerSide | ||
import org.sonar.api.batch.fs.{FilePredicate, FileSystem, InputFile} | ||
|
||
import scala.collection.JavaConverters._ | ||
import scala.xml.{Elem, XML} | ||
|
||
trait JUnitReportParserAPI { | ||
|
||
/** | ||
* Parse JUnit report files from the given directory | ||
* and return a map from input files to the parsed reports. | ||
*/ | ||
def parse(tests: List[Path], directories: List[File]): Map[InputFile, JUnitReport] | ||
} | ||
|
||
@ScannerSide | ||
final class JUnitReportParser(fileSystem: FileSystem) extends JUnitReportParserAPI { | ||
private[this] val log = Log(classOf[JUnitReportParser], "junit") | ||
|
||
def parse(tests: List[Path], directories: List[File]): Map[InputFile, JUnitReport] = { | ||
// Get report files - xml files starting with "TEST-". | ||
val reports: List[File] = reportFiles(directories) | ||
|
||
// Parse report files. | ||
val unitTestReports: List[JUnitReport] = parseReportFiles(reports) | ||
if (unitTestReports.nonEmpty) | ||
log.debug(s"JUnit test reports:\n${unitTestReports.mkString(", ")}") | ||
|
||
// Convert package names into files. | ||
resolveFiles(tests, unitTestReports) | ||
} | ||
|
||
/** | ||
* Get report files - xml files starting with "TEST-". | ||
*/ | ||
private[junit] def reportFiles(directories: List[File]): List[File] = { | ||
val reportFiles: List[File] = directories.filter(_.isDirectory).flatMap { dir => | ||
dir.listFiles( | ||
(_, name) => | ||
!name.startsWith("TEST-") && | ||
!name.startsWith("TESTS-") && | ||
name.endsWith(".xml") | ||
// TODO: Is this also the case for the Maven surefire plugin? | ||
) | ||
} | ||
|
||
if (directories.isEmpty) | ||
log.error(s"The paths ${directories.mkString(", ")} are not valid directories.") | ||
else if (reportFiles.isEmpty) | ||
log.error(s"No report files found in ${directories.mkString(", ")}.") | ||
|
||
reportFiles | ||
} | ||
|
||
/** | ||
* Parse report files. | ||
*/ | ||
private[junit] def parseReportFiles(reports: List[File]): List[JUnitReport] = { | ||
reports.map { file => | ||
val xml: Elem = XML.loadFile(file) | ||
JUnitReport( | ||
name = xml \@ "name", | ||
tests = (xml \@ "tests").toInt, | ||
errors = (xml \@ "errors").toInt, | ||
failures = (xml \@ "failures").toInt, | ||
skipped = (xml \@ "skipped").toInt, | ||
time = (xml \@ "time").toFloat | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Convert package names into files. | ||
*/ | ||
private[junit] def resolveFiles( | ||
tests: List[Path], | ||
reports: List[JUnitReport] | ||
): Map[InputFile, JUnitReport] = { | ||
reports | ||
.groupBy(_.name) | ||
.flatMap { | ||
case (name, reports) => | ||
val path: String = name.replace(".", "/") | ||
val files: List[Path] = tests.map(_.resolve(s"$path.scala")) | ||
val predicates: List[FilePredicate] = | ||
files.map(f => fileSystem.predicates.hasPath(f.toString)) | ||
|
||
val inputFiles: Iterable[InputFile] = | ||
fileSystem | ||
.inputFiles( | ||
fileSystem.predicates.or(predicates.asJava) | ||
) | ||
.asScala | ||
|
||
if (files.isEmpty) | ||
log.error(s"The following files were not found: ${files.mkString(", ")}") | ||
|
||
// Collect all of the input files. | ||
inputFiles.flatMap(file => reports.headOption.map((file, _))) | ||
} | ||
} | ||
} |
146 changes: 146 additions & 0 deletions
146
src/main/scala/com/mwz/sonar/scala/junit/JUnitSensor.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/* | ||
* Sonar Scala Plugin | ||
* Copyright (C) 2018 All contributors | ||
* | ||
* This program is free software; you can redistribute it and/or | ||
* modify it under the terms of the GNU Lesser General Public | ||
* License as published by the Free Software Foundation; either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with this program; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
*/ | ||
package com.mwz.sonar.scala | ||
package junit | ||
|
||
import java.io.File | ||
import java.nio.file.Path | ||
|
||
import cats.instances.list._ | ||
import com.mwz.sonar.scala.util.Log | ||
import com.mwz.sonar.scala.util.syntax.ConfigSyntax._ | ||
import com.mwz.sonar.scala.util.syntax.FileSystemSyntax._ | ||
import com.mwz.sonar.scala.util.syntax.SensorContextSyntax._ | ||
import org.sonar.api.batch.fs.{FileSystem, InputFile} | ||
import org.sonar.api.batch.sensor.{Sensor, SensorContext, SensorDescriptor} | ||
import org.sonar.api.config.Configuration | ||
import org.sonar.api.measures.CoreMetrics | ||
|
||
import scala.collection.JavaConverters._ | ||
|
||
/** | ||
* Scala JUnit sensor. | ||
* Parses JUnit XML reports and saves test metrics. | ||
*/ | ||
final class JUnitSensor( | ||
config: Configuration, | ||
fs: FileSystem, // TODO: Is the injected fileSystem different from context.fileSystem? | ||
untTestsReportParser: JUnitReportParserAPI | ||
) extends Sensor { | ||
import JUnitSensor._ // scalastyle:ignore org.scalastyle.scalariform.ImportGroupingChecker | ||
|
||
private[this] val log = Log(classOf[JUnitSensor], "junit") | ||
|
||
override def describe(descriptor: SensorDescriptor): Unit = { | ||
descriptor | ||
.name(SensorName) | ||
.onlyOnLanguage(Scala.LanguageKey) | ||
.onlyOnFileType(InputFile.Type.TEST) | ||
.onlyWhenConfiguration(shouldEnableSensor) | ||
} | ||
|
||
override def execute(context: SensorContext): Unit = { | ||
log.info("Initializing the Scala JUnit sensor.") | ||
|
||
// Get the test paths. | ||
val tests: List[Path] = testPaths(config) | ||
log.debug(s"The source prefixes are: ${tests.mkString("[", ",", "]")}.") | ||
|
||
// Get the junit report paths. | ||
val reports: List[Path] = reportPaths(config) | ||
log.debug(s"The JUnit report paths are: ${reports.mkString("[", ",", "]")}.") | ||
|
||
// Get test input files | ||
val inputFiles: Iterable[InputFile] = | ||
context.fileSystem | ||
.inputFiles( | ||
context.fileSystem.predicates.and( | ||
context.fileSystem.predicates.hasLanguage(Scala.LanguageKey), | ||
context.fileSystem.predicates.hasType(InputFile.Type.TEST) | ||
) | ||
) | ||
.asScala | ||
|
||
if (inputFiles.nonEmpty) | ||
log.debug(s"Input test files: \n${inputFiles.mkString(", ")}") | ||
else | ||
log.warn(s"No test files found for module ${context.module.key}.") | ||
|
||
// Resolve test directories. | ||
val testDirectories: List[File] = fs.resolve(tests) | ||
if (testDirectories.isEmpty) | ||
log.error(s"The following test directories were not found: ${reports.mkString(", ")}.") | ||
|
||
// Resolve JUnit report directories. | ||
val reportDirectories: List[File] = fs.resolve(reports) | ||
if (reportDirectories.isEmpty) | ||
log.error(s"The following JUnit test report path(s) were not found : ${reports.mkString(", ")}.") | ||
|
||
// Parse the reports. | ||
val parsedReports: Map[InputFile, JUnitReport] = untTestsReportParser.parse(tests, reportDirectories) | ||
|
||
// Save test metrics for each file. | ||
save(context, parsedReports) | ||
} | ||
|
||
/** | ||
* Save test metrics. | ||
*/ | ||
private[junit] def save( | ||
context: SensorContext, | ||
reports: Map[InputFile, JUnitReport] | ||
): Unit = { | ||
if (reports.nonEmpty) | ||
log.debug(s"Parsed reports:\n${reports.mkString(", ")}") | ||
else | ||
log.info("No test metrics were saved by this sensor.") | ||
|
||
reports.foreach { | ||
case (file, report) => | ||
log.debug(s"Saving junit test metrics for $file.") | ||
context.saveMeasure[Integer](file, CoreMetrics.SKIPPED_TESTS, report.skipped) | ||
context.saveMeasure[Integer](file, CoreMetrics.TESTS, report.tests - report.skipped) | ||
context.saveMeasure[Integer](file, CoreMetrics.TEST_ERRORS, report.errors) | ||
context.saveMeasure[Integer](file, CoreMetrics.TEST_FAILURES, report.failures) | ||
context.saveMeasure[java.lang.Long]( | ||
file, | ||
CoreMetrics.TEST_EXECUTION_TIME, | ||
(report.time * 1000).longValue | ||
) | ||
} | ||
} | ||
} | ||
|
||
object JUnitSensor { | ||
val SensorName = "Scala JUnit Sensor" | ||
val TestsPropertyKey = "sonar.tests" | ||
val DefaultTests = "src/test/scala" | ||
val ReportsPropertyKey = "sonar.junit.reportPaths" | ||
val DefaultReportPaths = "target/test-reports" | ||
val DisablePropertyKey = "sonar.junit.disable" | ||
|
||
private[junit] def testPaths(conf: Configuration): List[Path] = | ||
conf.getPaths(TestsPropertyKey, DefaultTests) | ||
|
||
private[junit] def reportPaths(conf: Configuration): List[Path] = | ||
conf.getPaths(ReportsPropertyKey, DefaultReportPaths) | ||
|
||
private[junit] def shouldEnableSensor(conf: Configuration): Boolean = | ||
!conf.getValue[Boolean](DisablePropertyKey) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this sensor only works for test files.
Thus, you may add
.onlyOnFileType(InputFile.Type.TEST)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes definitely