Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to handle "no source root found" in a custom way #525

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,12 @@ credentials.sbt
.idea_modules
*.iml

# VSCode with Metals specific
.bsp
.bloop
.metals
.vscode
metals.sbt

# npm specific
node_modules/
61 changes: 42 additions & 19 deletions reporter/src/main/scala/scoverage/reporter/BaseReportWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,62 @@ package scoverage.reporter

import java.io.File

class BaseReportWriter(
sourceDirectories: Seq[File],
/** Abstract report writer.
*
* @param sourceRoots list of source directories
* @param outputDir directory where to store the reports
* @param outputEncoding encoding to use when writing files
* @param recoverNoSourceRoot specifies how to handle source paths that are outside of the source roots.
*/
abstract class BaseReportWriter(
sourceRoots: Seq[File],
outputDir: File,
sourceEncoding: Option[String]
outputEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
) {

// Source paths in canonical form WITH trailing file separator
private val formattedSourcePaths: Seq[String] =
sourceDirectories
sourceRoots
.filter(_.isDirectory)
.map(_.getCanonicalPath + File.separator)
.map(_.getCanonicalPath + File.separatorChar)

/** Converts absolute path to relative one if any of the source directories is it's parent.
* If there is no parent directory, the path is returned unchanged (absolute).
/** Converts an absolute path to a path relative to the reporter's source directories (aka "source roots").
* If the path is not in the source roots, returns None.
*
* @param src absolute file path in canonical form
* @return `Some(relativePath)` if `src` is in the source roots, else `None`
*/
def relativeSource(src: String): String =
def relativeSource(src: String): Option[String] =
relativeSource(src, formattedSourcePaths)

private def relativeSource(src: String, sourcePaths: Seq[String]): String = {
private def relativeSource(
src: String,
sourceRoots: Seq[String]
): Option[String] = {
// We need the canonical path for the given src because our formattedSourcePaths are canonical
val canonicalSrc = new File(src).getCanonicalPath
val sourceRoot: Option[String] =
sourcePaths.find(sourcePath => canonicalSrc.startsWith(sourcePath))
sourceRoot match {
case Some(path: String) => canonicalSrc.replace(path, "")
case _ =>
val fmtSourcePaths: String = sourcePaths.mkString("'", "', '", "'")
throw new RuntimeException(
s"No source root found for '$canonicalSrc' (source roots: $fmtSourcePaths)"
);
}
sourceRoots
.find(root => canonicalSrc.startsWith(root))
.map(root => canonicalSrc.substring(root.length))
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, maybe personal preference so take it or leave it

Suggested change
.find(root => canonicalSrc.startsWith(root))
.map(root => canonicalSrc.substring(root.length))
.collectFirst {
case root if canonicalSrc.startsWith(root) =>
canonicalSrc.substring(root.length)
}

Copy link
Author

@TheElectronWill TheElectronWill Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I hesitated 😄

.orElse(recoverNoSourceRoot(new File(canonicalSrc), formattedSourcePaths))
}
}
object BaseReportWriter {

/** Specifies how to handle source path that are outside of the source roots.
* Takes the source path (as a canonical File) and returns:
* - `None` to skip the element
* - `Some(newPath)` to use `newPath` instead
*
* The function may of course take additional actions, such as logging a warning,
* throwing an error, etc.
*/
type PathRecoverer = (File, Seq[String]) => Option[String]

/** Throws an exception */
def failIfNoSourceRoot(f: File, roots: Seq[String]): Option[String] =
throw new RuntimeException(
s"No source root found for '${f.getPath}' (source roots: $roots)"
)
}
59 changes: 36 additions & 23 deletions reporter/src/main/scala/scoverage/reporter/CoberturaXmlWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@ import scoverage.domain.MeasuredPackage
class CoberturaXmlWriter(
sourceDirectories: Seq[File],
outputDir: File,
sourceEncoding: Option[String]
) extends BaseReportWriter(sourceDirectories, outputDir, sourceEncoding) {
sourceEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think we'll need to handle this a bit differently. These would be breaking changes for all the reporters, meaning another major bump, and I don't think we want to do another major atm. Can we potentially switch these up to either have a default implementation for this or another way that won't break compat for existing tools that are using this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand.
I can make the default throw an exception (i.e. keep the old behavior) and allow the "recoverer" to be added up.

) extends BaseReportWriter(
sourceDirectories,
outputDir,
sourceEncoding,
recoverNoSourceRoot
) {

def this(baseDir: File, outputDir: File, sourceEncoding: Option[String]) = {
this(Seq(baseDir), outputDir, sourceEncoding)
def this(
baseDir: File,
outputDir: File,
sourceEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
) = {
this(Seq(baseDir), outputDir, sourceEncoding, recoverNoSourceRoot)
}

def write(coverage: Coverage): Unit = {
Expand Down Expand Up @@ -49,24 +60,26 @@ class CoberturaXmlWriter(
</method>
}

def klass(klass: MeasuredClass): Node = {
<class name={klass.fullClassName}
filename={relativeSource(klass.source)}
line-rate={DoubleFormat.twoFractionDigits(klass.statementCoverage)}
branch-rate={DoubleFormat.twoFractionDigits(klass.branchCoverage)}
complexity="0">
<methods>
{klass.methods.map(method)}
</methods>
<lines>
{
klass.statements.map(stmt => <line
number={stmt.line.toString}
hits={stmt.count.toString}
branch={stmt.branch.toString}/>)
}
</lines>
</class>
def klass(klass: MeasuredClass): Option[Node] = {
relativeSource(klass.source).map(sourcePath => {
<class name={klass.fullClassName}
filename={sourcePath}
line-rate={DoubleFormat.twoFractionDigits(klass.statementCoverage)}
branch-rate={DoubleFormat.twoFractionDigits(klass.branchCoverage)}
complexity="0">
<methods>
{klass.methods.map(method)}
</methods>
<lines>
{
klass.statements.map(stmt => <line
number={stmt.line.toString}
hits={stmt.count.toString}
branch={stmt.branch.toString}/>)
}
</lines>
</class>
})
}

def pack(pack: MeasuredPackage): Node = {
Expand All @@ -75,7 +88,7 @@ class CoberturaXmlWriter(
branch-rate={DoubleFormat.twoFractionDigits(pack.branchCoverage)}
complexity="0">
<classes>
{pack.classes.map(klass)}
{pack.classes.flatMap(klass)}
</classes>
</package>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,49 @@ import scoverage.domain.MeasuredPackage
class ScoverageHtmlWriter(
sourceDirectories: Seq[File],
outputDir: File,
sourceEncoding: Option[String]
) extends BaseReportWriter(sourceDirectories, outputDir, sourceEncoding) {
sourceEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
) extends BaseReportWriter(
sourceDirectories,
outputDir,
sourceEncoding,
recoverNoSourceRoot
) {

// to be used by gradle-scoverage plugin
def this(
sourceDirectories: Array[File],
outputDir: File,
sourceEncoding: Option[String]
) = {
this(sourceDirectories.toSeq, outputDir, sourceEncoding)
this(
sourceDirectories.toSeq,
outputDir,
sourceEncoding,
BaseReportWriter.failIfNoSourceRoot
)
}

// for backward compatibility only
@deprecated
def this(sourceDirectories: Seq[File], outputDir: File) = {
this(sourceDirectories, outputDir, None);
this(
sourceDirectories,
outputDir,
None,
BaseReportWriter.failIfNoSourceRoot
);
}

// for backward compatibility only
@deprecated
def this(sourceDirectory: File, outputDir: File) = {
this(Seq(sourceDirectory), outputDir)
this(
Seq(sourceDirectory),
outputDir,
None,
BaseReportWriter.failIfNoSourceRoot
)
}

def write(coverage: Coverage): Unit = {
Expand Down Expand Up @@ -81,16 +104,25 @@ class ScoverageHtmlWriter(

private def writeFile(mfile: MeasuredFile): Unit = {
// each highlighted file is written out using the same structure as the original file.
val file = new File(outputDir, relativeSource(mfile.source) + ".html")
val sourcePath = relativeSource(mfile.source).getOrElse(
throw new RuntimeException(
s"Expected the file $mfile to be in the source roots"
)
)
val htmlPath = sourcePath + ".html"
val file = new File(outputDir, htmlPath)
file.getParentFile.mkdirs()
IOUtils.writeToFile(file, filePage(mfile).toString(), sourceEncoding)
IOUtils.writeToFile(
file,
filePage(mfile, htmlPath).toString(),
sourceEncoding
)
}

private def packageOverviewRelativePath(pkg: MeasuredPackage) =
pkg.name.replace("<empty>", "(empty)") + ".html"

private def filePage(mfile: MeasuredFile): Node = {
val filename = relativeSource(mfile.source) + ".html"
private def filePage(mfile: MeasuredFile, filename: String): Node = {
val css =
"table.codegrid { font-family: monospace; font-size: 12px; width: auto!important; }" +
"table.statementlist { width: auto!important; font-size: 13px; } " +
Expand Down Expand Up @@ -236,18 +268,18 @@ class ScoverageHtmlWriter(
</tr>
</thead>
<tbody>
{classes.toSeq.sortBy(_.fullClassName) map classRow}
{classes.toSeq.sortBy(_.fullClassName).flatMap(classRow)}
</tbody>
</table>
}

def classRow(klass: MeasuredClass): Node = {
def classRow(klass: MeasuredClass): Option[Node] = {
relativeSource(klass.source).map(path => classRow(klass, path))
}

def classRow(klass: MeasuredClass, relativeSourcePath: String): Node = {
val filename: String = {

val fileRelativeToSource = new File(
relativeSource(klass.source) + ".html"
)
val fileRelativeToSource = new File(relativeSourcePath + ".html")
val path = fileRelativeToSource.getParent
val value = fileRelativeToSource.getName

Expand Down
41 changes: 25 additions & 16 deletions reporter/src/main/scala/scoverage/reporter/ScoverageXmlWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ class ScoverageXmlWriter(
sourceDirectories: Seq[File],
outputDir: File,
debug: Boolean,
sourceEncoding: Option[String]
) extends BaseReportWriter(sourceDirectories, outputDir, sourceEncoding) {
sourceEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
) extends BaseReportWriter(
sourceDirectories,
outputDir,
sourceEncoding,
recoverNoSourceRoot
) {

def this(
sourceDir: File,
outputDir: File,
debug: Boolean,
sourceEncoding: Option[String]
sourceEncoding: Option[String],
recoverNoSourceRoot: BaseReportWriter.PathRecoverer
) = {
this(Seq(sourceDir), outputDir, debug, sourceEncoding)
this(Seq(sourceDir), outputDir, debug, sourceEncoding, recoverNoSourceRoot)
}

def write(coverage: Coverage): Unit = {
Expand Down Expand Up @@ -97,17 +104,19 @@ class ScoverageXmlWriter(
</method>
}

private def klass(klass: MeasuredClass): Node = {
<class name={klass.fullClassName}
filename={relativeSource(klass.source)}
statement-count={klass.statementCount.toString}
statements-invoked={klass.invokedStatementCount.toString}
statement-rate={klass.statementCoverageFormatted}
branch-rate={klass.branchCoverageFormatted}>
<methods>
{klass.methods.map(method)}
</methods>
</class>
private def klass(klass: MeasuredClass): Option[Node] = {
relativeSource(klass.source).map(sourcePath => {
<class name={klass.fullClassName}
filename={sourcePath}
statement-count={klass.statementCount.toString}
statements-invoked={klass.invokedStatementCount.toString}
statement-rate={klass.statementCoverageFormatted}
branch-rate={klass.branchCoverageFormatted}>
<methods>
{klass.methods.map(method)}
</methods>
</class>
})
}

private def pack(pack: MeasuredPackage): Node = {
Expand All @@ -116,7 +125,7 @@ class ScoverageXmlWriter(
statements-invoked={pack.invokedStatementCount.toString}
statement-rate={pack.statementCoverageFormatted}>
<classes>
{pack.classes.map(klass)}
{pack.classes.flatMap(klass)}
</classes>
</package>
}
Expand Down
Loading