Skip to content

Refactor ScoverageOptions out to its own file. #393

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

Merged
merged 1 commit into from
Sep 29, 2021
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package scoverage

/** Base options that can be passed into scoverage
*
* @param excludedPackages packages to be excluded in coverage
* @param excludedFiles files to be excluded in coverage
* @param excludedSymbols symbols to be excluded in coverage
* @param dataDir the directory that the coverage files should be written to
* @param reportTestName whether or not the test names should be reported
* @param sourceRoot the source root of your project
*/
case class ScoverageOptions(
excludedPackages: Seq[String],
excludedFiles: Seq[String],
excludedSymbols: Seq[String],
dataDir: String,
reportTestName: Boolean,
sourceRoot: String
)

object ScoverageOptions {

private[scoverage] val help = Some(
Seq(
"-P:scoverage:dataDir:<pathtodatadir> where the coverage files should be written\n",
"-P:scoverage:sourceRoot:<pathtosourceRoot> the root dir of your sources, used for path relativization\n",
"-P:scoverage:excludedPackages:<regex>;<regex> semicolon separated list of regexs for packages to exclude",
"-P:scoverage:excludedFiles:<regex>;<regex> semicolon separated list of regexs for paths to exclude",
"-P:scoverage:excludedSymbols:<regex>;<regex> semicolon separated list of regexs for symbols to exclude",
"-P:scoverage:extraAfterPhase:<phaseName> phase after which scoverage phase runs (must be after typer phase)",
"-P:scoverage:extraBeforePhase:<phaseName> phase before which scoverage phase runs (must be before patmat phase)",
" Any classes whose fully qualified name matches the regex will",
" be excluded from coverage."
).mkString("\n")
)

private def parseExclusionOption(
inOption: String
): Seq[String] =
inOption
.split(";")
.collect {
case value if value.trim().nonEmpty => value.trim()
}
.toIndexedSeq

private val ExcludedPackages = "excludedPackages:(.*)".r
private val ExcludedFiles = "excludedFiles:(.*)".r
private val ExcludedSymbols = "excludedSymbols:(.*)".r
private val DataDir = "dataDir:(.*)".r
private val SourceRoot = "sourceRoot:(.*)".r
private val ExtraAfterPhase = "extraAfterPhase:(.*)".r
private val ExtraBeforePhase = "extraBeforePhase:(.*)".r

/** Default that is _only_ used for initializing purposes. dataDir and
* sourceRoot are both just empty strings here, but we nevery actually
* allow for this to be the case when the plugin runs, and this is checked
* before it does.
*/
def default() = ScoverageOptions(
excludedPackages = Seq.empty,
excludedFiles = Seq.empty,
excludedSymbols = Seq(
"scala.reflect.api.Exprs.Expr",
"scala.reflect.api.Trees.Tree",
"scala.reflect.macros.Universe.Tree"
),
dataDir = "",
reportTestName = false,
sourceRoot = ""
)

def processPhaseOptions(
opts: List[String]
): (Option[String], Option[String]) = {

val afterPhase: Option[String] =
opts.collectFirst { case ExtraAfterPhase(phase) => phase }
val beforePhase: Option[String] =
opts.collectFirst { case ExtraBeforePhase(phase) => phase }

(afterPhase, beforePhase)
}

def parse(
scalacOptions: List[String],
errFn: String => Unit,
base: ScoverageOptions
): ScoverageOptions = {

var options = base

scalacOptions.foreach {
case ExcludedPackages(packages) =>
options = options.copy(excludedFiles = parseExclusionOption(packages))
case ExcludedFiles(files) =>
options = options.copy(excludedFiles = parseExclusionOption(files))
case ExcludedSymbols(symbols) =>
options = options.copy(excludedSymbols = parseExclusionOption(symbols))
case DataDir(dir) =>
options = options.copy(dataDir = dir)
case SourceRoot(root) =>
options.copy(sourceRoot = root)
// NOTE that both the extra phases are actually parsed out early on, so
// we just ignore them here
case ExtraAfterPhase(afterPhase) => ()
case ExtraBeforePhase(beforePhase) => ()
case "reportTestName" =>
options.copy(reportTestName = true)
case opt => errFn("Unknown option: " + opt)
}

options
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,126 +14,59 @@ import scala.tools.nsc.transform.TypingTransformers
/** @author Stephen Samuel */
class ScoveragePlugin(val global: Global) extends Plugin {

override val name: String = "scoverage"
override val description: String = "scoverage code coverage compiler plugin"
private val (extraAfterPhase, extraBeforePhase) = processPhaseOptions(
pluginOptions
)
override val name: String = ScoveragePlugin.name
override val description: String = ScoveragePlugin.description

// TODO I'm not 100% sure why, but historically these have been parsed out
// first. One thing to play around with in the future would be to not do this
// here and rather do it later when we utilize setOpts and instead just
// initialize then in the instrumentationCompoent. This will save us
// iterating over these options twice.
private val (extraAfterPhase, extraBeforePhase) =
ScoverageOptions.processPhaseOptions(
pluginOptions
)

val instrumentationComponent = new ScoverageInstrumentationComponent(
global,
extraAfterPhase,
extraBeforePhase
)

override val components: List[PluginComponent] = List(
instrumentationComponent
)

private def parseExclusionEntry(
entryName: String,
inOption: String
): Seq[String] =
inOption
.substring(entryName.length)
.split(";")
.map(_.trim)
.toIndexedSeq
.filterNot(_.isEmpty)

override def init(opts: List[String], error: String => Unit): Boolean = {
val options = new ScoverageOptions

for (opt <- opts) {
if (opt.startsWith("excludedPackages:")) {
options.excludedPackages = parseExclusionEntry("excludedPackages:", opt)
} else if (opt.startsWith("excludedFiles:")) {
options.excludedFiles = parseExclusionEntry("excludedFiles:", opt)
} else if (opt.startsWith("excludedSymbols:")) {
options.excludedSymbols = parseExclusionEntry("excludedSymbols:", opt)
} else if (opt.startsWith("dataDir:")) {
options.dataDir = opt.substring("dataDir:".length)
} else if (opt.startsWith("sourceRoot:")) {
options.sourceRoot = opt.substring("sourceRoot:".length())
} else if (
opt
.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")
) {
// skip here, these flags are processed elsewhere
} else if (opt == "reportTestName") {
options.reportTestName = true
} else {
error("Unknown option: " + opt)
}
}
if (!opts.exists(_.startsWith("dataDir:")))

val options =
ScoverageOptions.parse(opts, error, ScoverageOptions.default())

if (options.dataDir.isEmpty())
throw new RuntimeException(
"Cannot invoke plugin without specifying <dataDir>"
)
if (!opts.exists(_.startsWith("sourceRoot:")))

if (options.sourceRoot.isEmpty())
throw new RuntimeException(
"Cannot invoke plugin without specifying <sourceRoot>"
)

instrumentationComponent.setOptions(options)
true
}

override val optionsHelp: Option[String] = Some(
Seq(
"-P:scoverage:dataDir:<pathtodatadir> where the coverage files should be written\n",
"-P:scoverage:sourceRoot:<pathtosourceRoot> the root dir of your sources, used for path relativization\n",
"-P:scoverage:excludedPackages:<regex>;<regex> semicolon separated list of regexs for packages to exclude",
"-P:scoverage:excludedFiles:<regex>;<regex> semicolon separated list of regexs for paths to exclude",
"-P:scoverage:excludedSymbols:<regex>;<regex> semicolon separated list of regexs for symbols to exclude",
"-P:scoverage:extraAfterPhase:<phaseName> phase after which scoverage phase runs (must be after typer phase)",
"-P:scoverage:extraBeforePhase:<phaseName> phase before which scoverage phase runs (must be before patmat phase)",
" Any classes whose fully qualified name matches the regex will",
" be excluded from coverage."
).mkString("\n")
)
override val optionsHelp: Option[String] = ScoverageOptions.help

// copied from scala 2.11
private def pluginOptions: List[String] = {
// Process plugin options of form plugin:option
def namec = name + ":"
global.settings.pluginOptions.value filter (_ startsWith namec) map (_ stripPrefix namec)
}

private def processPhaseOptions(
opts: List[String]
): (Option[String], Option[String]) = {
var afterPhase: Option[String] = None
var beforePhase: Option[String] = None
for (opt <- opts) {
if (opt.startsWith("extraAfterPhase:")) {
afterPhase = Some(opt.substring("extraAfterPhase:".length))
}
if (opt.startsWith("extraBeforePhase:")) {
beforePhase = Some(opt.substring("extraBeforePhase:".length))
}
}
(afterPhase, beforePhase)
global.settings.pluginOptions.value
.filter(_.startsWith(namec))
.map(_.stripPrefix(namec))
}
}

// TODO refactor this into a case class. We'll also refactor how we parse the
// options to get rid of all these vars
class ScoverageOptions {
var excludedPackages: Seq[String] = Nil
var excludedFiles: Seq[String] = Nil
var excludedSymbols: Seq[String] = Seq(
"scala.reflect.api.Exprs.Expr",
"scala.reflect.api.Trees.Tree",
"scala.reflect.macros.Universe.Tree"
)
var dataDir: String = IOUtils.getTempPath
var reportTestName: Boolean = false
// TODO again, we'll refactor this later so this won't have a default here.
// However for tests we'll have to create this. However, make sure you create
// either both in temp or neither in temp, since on windows your temp dir
// will be in another drive, so the relativize functinality won't work if
// correctly.
var sourceRoot: String = IOUtils.getTempPath
}

class ScoverageInstrumentationComponent(
val global: Global,
extraAfterPhase: Option[String],
Expand All @@ -147,7 +80,7 @@ class ScoverageInstrumentationComponent(
val statementIds = new AtomicInteger(0)
val coverage = new Coverage

override val phaseName: String = "scoverage-instrumentation"
override val phaseName: String = ScoveragePlugin.phaseName
override val runsAfter: List[String] =
List("typer") ::: extraAfterPhase.toList
override val runsBefore: List[String] =
Expand All @@ -158,7 +91,7 @@ class ScoverageInstrumentationComponent(
* You must call "setOptions" before running any commands that rely on
* the options.
*/
private var options: ScoverageOptions = new ScoverageOptions()
private var options: ScoverageOptions = ScoverageOptions.default()
private var coverageFilter: CoverageFilter = AllCoverageFilter

private val isScalaJsEnabled: Boolean = {
Expand Down Expand Up @@ -194,7 +127,6 @@ class ScoverageInstrumentationComponent(
s"Instrumentation completed [${coverage.statements.size} statements]"
)

// TODO do we need to verify this sourceRoot exists? How does semanticdb do this?
Serializer.serialize(
coverage,
Serializer.coverageFile(options.dataDir),
Expand Down Expand Up @@ -920,3 +852,9 @@ class ScoverageInstrumentationComponent(
}
}
}

object ScoveragePlugin {
val name: String = "scoverage"
val description: String = "scoverage code coverage compiler plugin"
val phaseName: String = "scoverage-instrumentation"
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class RegexCoverageFilterTest extends FunSuite {
)
}

val options = new ScoverageOptions()
val options = ScoverageOptions.default()

test("isSymbolIncluded should return true for empty excludes") {
assert(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("x"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ class ScoverageCompiler(
val instrumentationComponent =
new ScoverageInstrumentationComponent(this, None, None)

instrumentationComponent.setOptions(new ScoverageOptions())
val coverageOptions = ScoverageOptions
.default()
.copy(dataDir = IOUtils.getTempPath)
.copy(sourceRoot = IOUtils.getTempPath)

instrumentationComponent.setOptions(coverageOptions)
val testStore = new ScoverageTestStoreComponent(this)
val validator = new PositionValidator(this)

Expand Down