/* * Copyright (C) 2018-2020 All sonar-scala 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 General Lesser Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mwz.sonar.scala package scoverage import java.nio.file.{Path, Paths} import scala.xml.{Node, XML} import cats.syntax.semigroup.catsSyntaxSemigroup import com.mwz.sonar.scala.util.PathUtils import org.sonar.api.scanner.ScannerSide /** [[ScoverageMetrics]] of a component. */ private[scoverage] final case class Scoverage( statements: Int, coveredStatements: Int, statementCoverage: Double, branches: Int, coveredBranches: Int, branchCoverage: Double ) /** * The coverage information of an entire project. * It is composed of: * - the overall [[ScoverageMetrics]] of the project. * - the coverage information of each file of the project. */ private[scoverage] final case class ProjectCoverage( projectScoverage: Scoverage, filesCoverage: Map[String, FileCoverage] ) /** * The coverage information of a file. * It is composed of: * - the overall [[ScoverageMetrics]] of the file. * - the coverage information of each line of the file. */ private[scoverage] final case class FileCoverage( fileScoverage: Scoverage, linesCoverage: LinesCoverage ) trait ScoverageReportParserAPI { def parse(scoverageReportPath: Path, projectPath: Path, sourcePrefixes: List[Path]): ProjectCoverage } /** Scoverage XML reports parser. */ @ScannerSide final class ScoverageReportParser extends ScoverageReportParserAPI { /** Parses the scoverage report from a file and returns the ProjectCoverage. */ override def parse( scoverageReportPath: Path, projectPath: Path, sourcePrefixes: List[Path] ): ProjectCoverage = { val scoverageXMLReport = XML.loadFile(scoverageReportPath.toFile) val projectScoverage = extractScoverageFromNode(scoverageXMLReport) val classCoverages = for { classNode <- scoverageXMLReport \\ "class" scoverageFilename = Paths.get(classNode \@ "filename") filename <- sourcePrefixes map { prefix => // We call stripOutPrefix twice here to get the full path to the filenames from Scoverage report, // relative to the current project and the sources prefix. // E.g. both module1/sources/File.scala as well as sources/File.scala will return File.scala as a result. val filename = PathUtils.stripOutPrefix( PathUtils.stripOutPrefix(projectPath, prefix), scoverageFilename ) (prefix, filename) } collectFirst { case (prefix, filename) if PathUtils.cwd.resolve(prefix).resolve(filename).toFile.exists => prefix.resolve(filename).toString } classScoverage = extractScoverageFromNode(classNode) lines: Seq[(Int, Int)] = for { statement <- (classNode \\ "statement").filter(node => !(node \@ "ignored").toBoolean) lineNum = (statement \@ "line").toInt count = (statement \@ "invocation-count").toInt } yield lineNum -> count linesCoverage = lines .groupMapReduce { case (lineNum, _) => lineNum } { case (_, count) => count }(_ + _) classCoverage = FileCoverage(classScoverage, linesCoverage) } yield filename -> classCoverage // Merge the class coverages by filename. val files = classCoverages .groupMapReduce { case (fileName, _) => fileName } { case (_, classCoverage) => classCoverage }(_ |+| _) ProjectCoverage(projectScoverage, files) } /** Extracts the scoverage metrics form a class or project node. */ private[scoverage] def extractScoverageFromNode(node: Node): Scoverage = { val branches = (node \\ "statement") .filter(node => !(node \@ "ignored").toBoolean && (node \@ "branch").toBoolean) val coveredBranches = branches.filter(statement => (statement \@ "invocation-count").toInt > 0) Scoverage( statements = (node \@ "statement-count").toInt, coveredStatements = (node \@ "statements-invoked").toInt, statementCoverage = (node \@ "statement-rate").toDouble, branches = branches.size, coveredBranches = coveredBranches.size, branchCoverage = (node \@ "branch-rate").toDouble ) } }