From cae999b0c0e7529a6a1a01bb10296b9c135e6e19 Mon Sep 17 00:00:00 2001 From: Alistair Johnson Date: Sun, 9 Oct 2016 15:19:55 +0200 Subject: [PATCH 1/3] Intial implementation --- .gitignore | 23 +- .travis.yml | 22 + build.sbt | 90 +++ project/build.properties | 1 + project/plugins.sbt | 5 + .../src/main/resources/scalac-plugin.xml | 4 + .../src/main/scala/scoverage/Constants.scala | 13 + .../main/scala/scoverage/CoverageFilter.scala | 102 +++ .../main/scala/scoverage/DoubleFormat.scala | 19 + .../src/main/scala/scoverage/IOUtils.scala | 85 +++ .../src/main/scala/scoverage/Location.scala | 65 ++ .../src/main/scala/scoverage/Serializer.scala | 109 +++ .../src/main/scala/scoverage/coverage.scala | 192 ++++++ .../src/main/scala/scoverage/plugin.scala | 634 ++++++++++++++++++ .../src/main/scala/scoverage/Invoker.scala | 7 + .../src/main/scala/scoverage/InvokerJ.java | 64 ++ .../src/main/scala/scoverage/Invoker.scala | 71 ++ .../src/main/scala/scoverage/Platform.scala | 21 + version.sbt | 1 + 19 files changed, 1518 insertions(+), 10 deletions(-) create mode 100644 .travis.yml create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala create mode 100644 scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala create mode 100644 scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java create mode 100644 scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala create mode 100644 scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala create mode 100644 version.sbt diff --git a/.gitignore b/.gitignore index c58d83b..f5d8cca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,20 @@ -*.class *.log -# sbt specific -.cache -.history -.lib/ -dist/* +# SBT specific target/ -lib_managed/ -src_managed/ project/boot/ project/plugins/project/ +# Eclipse specific +.classpath +.project +.settings/ + # Scala-IDE specific -.scala_dependencies -.worksheet +.cache-main +.cache-tests + +# IntelliJ IDEA specific +.idea +.idea_modules +*.iml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bbef7ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: scala + +script: + - sbt ++$TRAVIS_SCALA_VERSION test + +matrix: + include: + - jdk: oraclejdk7 + scala: 2.10.6 + - jdk: oraclejdk7 + scala: 2.11.8 + - jdk: oraclejdk8 + scala: 2.12.0-RC1 + +before_cache: + - find "$HOME/.sbt/" -name '*.lock' -print0 | xargs -0 rm + - find "$HOME/.ivy2/" -name 'ivydata-*.properties' -print0 | xargs -0 rm + +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..2b7102c --- /dev/null +++ b/build.sbt @@ -0,0 +1,90 @@ +import sbt._ +import sbt.Keys._ +import sbtrelease.ReleasePlugin.autoImport._ +import com.typesafe.sbt.pgp.PgpKeys + + +val Org = "org.scoverage" +//val MockitoVersion = "1.10.19" +val JUnitVersion = "0.9" + +val appSettings = Seq( + organization := Org, + crossVersion := CrossVersion.full, // because compiler api is not binary compatible + scalaVersion := "2.11.8", + crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-M3", "2.12.0-M4","2.12.0-M5","2.12.0-RC1", "2.12.0-RC1-ceaf419"), + fork in Test := false, + publishMavenStyle := true, + publishArtifact in Test := false, + parallelExecution in Test := false, + scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8"), + javacOptions := { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, scalaMajor)) if scalaMajor < 12 => Seq("-source", "1.7", "-target", "1.7") + case _ => Seq() + } + }, + concurrentRestrictions in Global += Tags.limit(Tags.Test, 1), + publishTo := { + val nexus = "https://oss.sonatype.org/" + if (isSnapshot.value) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + }, + pomExtra := { + https://github.com/scoverage/scalac-scoverage-plugin-core + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + git@github.com:scoverage/scalac-scoverage-plugin-core.git + scm:git@github.com:scoverage/scalac-scoverage-plugin-core.git + + + + sksamuel + Stephen Samuel + http://github.com/sksamuel + + + }, + pomIncludeRepository := { + _ => false + } + ) ++ Seq( + releaseCrossBuild := true, + releasePublishArtifactsAction := PgpKeys.publishSigned.value + ) + +lazy val root = Project("scalac-scoverage", file(".")) + .settings(name := "scalac-scoverage") + .settings(appSettings: _*) + .settings(publishArtifact := false) + .aggregate(plugin, runtimeJava, runtimeScala) + + +lazy val runtimeJava = Project("scalac-scoverage-runtime-java", file("scalac-scoverage-runtime-java")) + .settings(name := "scalac-scoverage-runtime-java") + .settings(appSettings: _*) + +lazy val runtimeScala = Project("scalac-scoverage-runtime-scala", file("scalac-scoverage-runtime-scala")) + .settings(name := "scalac-scoverage-runtime-scala") + .settings(appSettings: _*) + +lazy val plugin = Project("scalac-scoverage-plugin", file("scalac-scoverage-plugin")) + //.dependsOn(`scalac-scoverage-runtime-java` % "test") + .settings(name := "scalac-scoverage-plugin") + .settings(appSettings: _*) + .settings(libraryDependencies ++= Seq( + // "org.mockito" % "mockito-all" % MockitoVersion % "test", + // "org.scalatest" %% "scalatest" % ScalatestVersion % "test", + "com.novocode" % "junit-interface" % "0.9" % "test", + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided" excludeAll(ExclusionRule(organization="org.scala-lang.modules")), + "org.joda" % "joda-convert" % "1.6" % "test", + "joda-time" % "joda-time" % "2.3" % "test" + )) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..35c88ba --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.12 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..d9d8a31 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") + +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.1") + +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") diff --git a/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml b/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml new file mode 100644 index 0000000..76fd451 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml @@ -0,0 +1,4 @@ + + scoverage + scoverage.ScoveragePlugin + \ No newline at end of file diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala new file mode 100644 index 0000000..3259e3b --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala @@ -0,0 +1,13 @@ +package scoverage + +object Constants { + // the file that contains the statement mappings + val CoverageFileName = "scoverage.coverage.xml" + // the final scoverage report + val XMLReportFilename = "scoverage.xml" + val XMLReportFilenameWithDebug = "scoverage-debug.xml" + // directory that contains all the measurement data but not reports + val DataDir = "scoverage-data" + // the prefix the measurement files have + val MeasurementsPrefix = "scoverage.measurements." +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala new file mode 100644 index 0000000..1636d08 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala @@ -0,0 +1,102 @@ +package scoverage + +import scala.collection.mutable +import scala.reflect.internal.util.{Position, SourceFile} + +/** + * Methods related to filtering the instrumentation and coverage. + * + * @author Stephen Samuel + */ +trait CoverageFilter { + def isClassIncluded(className: String): Boolean + def isFileIncluded(file: SourceFile): Boolean + def isLineIncluded(position: Position): Boolean + def isSymbolIncluded(symbolName: String): Boolean + def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] +} + +object AllCoverageFilter extends CoverageFilter { + override def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = Nil + override def isLineIncluded(position: Position): Boolean = true + override def isClassIncluded(className: String): Boolean = true + override def isFileIncluded(file: SourceFile): Boolean = true + override def isSymbolIncluded(symbolName: String): Boolean = true +} + +class RegexCoverageFilter(excludedPackages: Seq[String], + excludedFiles: Seq[String], + excludedSymbols: Seq[String]) extends CoverageFilter { + + val excludedClassNamePatterns = excludedPackages.map(_.r.pattern) + val excludedFilePatterns = excludedFiles.map(_.r.pattern) + val excludedSymbolPatterns = excludedSymbols.map(_.r.pattern) + + /** + * We cache the excluded ranges to avoid scanning the source code files + * repeatedly. For a large project there might be a lot of source code + * data, so we only hold a weak reference. + */ + val linesExcludedByScoverageCommentsCache: mutable.Map[SourceFile, List[Range]] = mutable.WeakHashMap.empty + + final val scoverageExclusionCommentsRegex = + """(?ms)^\s*//\s*(\$COVERAGE-OFF\$).*?(^\s*//\s*\$COVERAGE-ON\$|\Z)""".r + + /** + * True if the given className has not been excluded by the + * `excludedPackages` option. + */ + override def isClassIncluded(className: String): Boolean = { + excludedClassNamePatterns.isEmpty || !excludedClassNamePatterns.exists(_.matcher(className).matches) + } + + override def isFileIncluded(file: SourceFile): Boolean = { + def isFileMatch(file: SourceFile) = excludedFilePatterns.exists(_.matcher(file.path.replace(".scala", "")).matches) + excludedFilePatterns.isEmpty || !isFileMatch(file) + } + + /** + * True if the line containing `position` has not been excluded by a magic comment. + */ + def isLineIncluded(position: Position): Boolean = { + if (position.isDefined) { + val excludedLineNumbers = getExcludedLineNumbers(position.source) + val lineNumber = position.line + !excludedLineNumbers.exists(_.contains(lineNumber)) + } else { + true + } + } + + override def isSymbolIncluded(symbolName: String): Boolean = { + excludedSymbolPatterns.isEmpty || !excludedSymbolPatterns.exists(_.matcher(symbolName).matches) + } + + /** + * Checks the given sourceFile for any magic comments which exclude lines + * from coverage. Returns a list of Ranges of lines that should be excluded. + * + * The line numbers returned are conventional 1-based line numbers (i.e. the + * first line is line number 1) + */ + def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = { + linesExcludedByScoverageCommentsCache.get(sourceFile) match { + case Some(lineNumbers) => lineNumbers + case None => + val lineNumbers = scoverageExclusionCommentsRegex.findAllIn(sourceFile.content).matchData.map { m => + // Asking a SourceFile for the line number of the char after + // the end of the file gives an exception + val endChar = math.min(m.end(2), sourceFile.content.length - 1) + // Most of the compiler API appears to use conventional + // 1-based line numbers (e.g. "Position.line"), but it appears + // that the "offsetToLine" method in SourceFile uses 0-based + // line numbers + Range( + 1 + sourceFile.offsetToLine(m.start(1)), + 1 + sourceFile.offsetToLine(endChar)) + }.toList + linesExcludedByScoverageCommentsCache.put(sourceFile, lineNumbers) + lineNumbers + } + } +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala new file mode 100644 index 0000000..0b689eb --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala @@ -0,0 +1,19 @@ +package scoverage + +import java.text.{DecimalFormat, DecimalFormatSymbols} +import java.util.Locale + +object DoubleFormat { + private[this] val twoFractionDigitsFormat: DecimalFormat = { + val fmt = new DecimalFormat() + fmt.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US)) + fmt.setMinimumIntegerDigits(1) + fmt.setMinimumFractionDigits(2) + fmt.setMaximumFractionDigits(2) + fmt.setGroupingUsed(false) + fmt + } + + def twoFractionDigits(d: Double) = twoFractionDigitsFormat.format(d) + +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala new file mode 100644 index 0000000..409eb81 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala @@ -0,0 +1,85 @@ +package scoverage + +import java.io._ + +import scala.collection.{Set, mutable} +import scala.io.Source + +/** @author Stephen Samuel */ +object IOUtils { + + def getTempDirectory: File = new File(getTempPath) + def getTempPath: String = System.getProperty("java.io.tmpdir") + + def readStreamAsString(in: InputStream): String = Source.fromInputStream(in).mkString + + private val UnixSeperator: Char = '/' + private val WindowsSeperator: Char = '\\' + private val UTF8Encoding: String = "UTF-8" + + def getName(path: String): Any = { + val index = { + val lastUnixPos = path.lastIndexOf(UnixSeperator) + val lastWindowsPos = path.lastIndexOf(WindowsSeperator) + Math.max(lastUnixPos, lastWindowsPos) + } + path.drop(index + 1) + } + + def reportFile(outputDir: File, debug: Boolean = false): File = debug match { + case true => new File(outputDir, Constants.XMLReportFilenameWithDebug) + case false => new File(outputDir, Constants.XMLReportFilename) + } + + def clean(dataDir: File): Unit = findMeasurementFiles(dataDir).foreach(_.delete) + def clean(dataDir: String): Unit = clean(new File(dataDir)) + + def writeToFile(file: File, str: String) = { + val writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), UTF8Encoding)) + try { + writer.write(str) + } finally { + writer.close() + } + } + + /** + * Returns the measurement file for the current thread. + */ + def measurementFile(dataDir: File): File = measurementFile(dataDir.getAbsolutePath) + def measurementFile(dataDir: String): File = new File(dataDir, Constants.MeasurementsPrefix + Thread.currentThread.getId) + + def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles(new File(dataDir)) + def findMeasurementFiles(dataDir: File): Array[File] = dataDir.listFiles(new FileFilter { + override def accept(pathname: File): Boolean = pathname.getName.startsWith(Constants.MeasurementsPrefix) + }) + + def reportFileSearch(baseDir: File, condition: File => Boolean): Seq[File] = { + def search(file: File): Seq[File] = file match { + case dir if dir.isDirectory => dir.listFiles().toSeq.map(search).flatten + case f if isReportFile(f) => Seq(f) + case _ => Nil + } + search(baseDir) + } + + val isMeasurementFile = (file: File) => file.getName.startsWith(Constants.MeasurementsPrefix) + val isReportFile = (file: File) => file.getName == Constants.XMLReportFilename + val isDebugReportFile = (file: File) => file.getName == Constants.XMLReportFilenameWithDebug + + // loads all the invoked statement ids from the given files + def invoked(files: Seq[File]): Set[Int] = { + val acc = mutable.Set[Int]() + files.foreach { file => + val reader = Source.fromFile(file) + for ( line <- reader.getLines() ) { + if (!line.isEmpty) { + acc += line.toInt + } + } + reader.close() + } + acc + } + +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala new file mode 100644 index 0000000..7845811 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala @@ -0,0 +1,65 @@ +package scoverage + +import scala.tools.nsc.Global + +/** + * @param packageName the name of the enclosing package + * @param className the name of the closest enclosing class + * @param fullClassName the fully qualified name of the closest enclosing class + */ +case class Location(packageName: String, + className: String, + fullClassName: String, + classType: ClassType, + method: String, + sourcePath: String) extends java.io.Serializable + +object Location { + + def apply(global: Global): global.Tree => Option[Location] = { tree => + + def packageName(s: global.Symbol): String = { + s.enclosingPackage.fullName + } + + def className(s: global.Symbol): String = { + // anon functions are enclosed in proper classes. + if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) className(s.owner) + else s.enclClass.nameString + } + + def classType(s: global.Symbol): ClassType = { + if (s.enclClass.isTrait) ClassType.Trait + else if (s.enclClass.isModuleOrModuleClass) ClassType.Object + else ClassType.Class + } + + def fullClassName(s: global.Symbol): String = { + // anon functions are enclosed in proper classes. + if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) fullClassName(s.owner) + else s.enclClass.fullNameString + } + + def enclosingMethod(s: global.Symbol): String = { + // check if we are in a proper method and return that, otherwise traverse up + if (s.enclClass.isAnonymousFunction ) enclosingMethod(s.owner) + else if (s.enclMethod.isPrimaryConstructor) "" + else Option(s.enclMethod.nameString).getOrElse("") + } + + def sourcePath(symbol: global.Symbol): String = { + Option(symbol.sourceFile).map(_.canonicalPath).getOrElse("") + } + + Option(tree.symbol) map { + symbol => + Location( + packageName(symbol), + className(symbol), + fullClassName(symbol), + classType(symbol), + enclosingMethod(symbol), + sourcePath(symbol)) + } + } +} \ No newline at end of file diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala new file mode 100644 index 0000000..2edae22 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala @@ -0,0 +1,109 @@ +package scoverage + +import java.io._ + +import scala.io.Source + +object Serializer { + + // Write out coverage data to the given data directory, using the default coverage filename + def serialize(coverage: Coverage, dataDir: String): Unit = serialize(coverage, coverageFile(dataDir)) + + // Write out coverage data to given file. + def serialize(coverage: Coverage, file: File): Unit = { + val writer = new BufferedWriter(new FileWriter(file)) + serialize(coverage, writer) + writer.close() + } + + def serialize(coverage: Coverage, writer: Writer): Unit = { + def writeStatement(stmt: Statement, writer: Writer): Unit = { + writer.write { + val xml = "" + + "" + + {stmt.source} + "" + + "" + + {stmt.location.packageName} + "" + + "" + + {stmt.location.className} + "" + + "" + + {stmt.location.classType.toString} + "" + + "" + + {stmt.location.fullClassName} + "" + + "" + + {stmt.location.method} + "" + + "" + + {stmt.location.sourcePath} + "" + + "" + + {stmt.id.toString} + "" + + "" + + {stmt.start.toString} + "" + + "" + + {stmt.end.toString} + "" + + "" + + {stmt.line.toString} + "" + + "" + + {escape(stmt.desc)} + "" + + "" + + {escape(stmt.symbolName)} + "" + + "" + + {escape(stmt.treeName)} + "" + + "" + + {stmt.branch.toString} + "" + + "" + + {stmt.count.toString} + "" + + "" + + {stmt.ignored.toString} + "" + + "" + } + } + writer.write("\n") + coverage.statements.foreach(stmt => writeStatement(stmt, writer)) + writer.write("") + } + + def coverageFile(dataDir: File): File = coverageFile(dataDir.getAbsolutePath) + def coverageFile(dataDir: String): File = new File(dataDir, Constants.CoverageFileName) + + /** + * This method ensures that the output String has only + * valid XML unicode characters as specified by the + * XML 1.0 standard. For reference, please see + * "the + * standard. This method will return an empty + * String if the input is null or empty. + * + * @param in The String whose non-valid characters we want to remove. + * @return The in String, stripped of non-valid characters. + * @see http://blog.mark-mclaren.info/2007/02/invalid-xml-characters-when-valid-utf8_5873.html + * + */ + def escape(in: String): String = { + val out = new StringBuilder() + for ( current <- Option(in).getOrElse("").toCharArray ) { + if ((current == 0x9) || (current == 0xA) || (current == 0xD) || + ((current >= 0x20) && (current <= 0xD7FF)) || + ((current >= 0xE000) && (current <= 0xFFFD)) || + ((current >= 0x10000) && (current <= 0x10FFFF))) + out.append(current) + } + out.mkString + } +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala new file mode 100644 index 0000000..6591c99 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala @@ -0,0 +1,192 @@ +package scoverage + +import java.io.File + +import scoverage.DoubleFormat.twoFractionDigits + +import scala.collection.mutable + +/** + * @author Stephen Samuel */ +case class Coverage() + extends CoverageMetrics + with MethodBuilders + with java.io.Serializable + with ClassBuilders + with PackageBuilders + with FileBuilders { + + private val statementsById = mutable.Map[Int, Statement]() + override def statements = statementsById.values + def add(stmt: Statement): Unit = statementsById.put(stmt.id, stmt) + + private val ignoredStatementsById = mutable.Map[Int, Statement]() + override def ignoredStatements = ignoredStatementsById.values + def addIgnoredStatement(stmt: Statement): Unit = ignoredStatementsById.put(stmt.id, stmt) + + + def avgClassesPerPackage = classCount / packageCount.toDouble + def avgClassesPerPackageFormatted: String = twoFractionDigits(avgClassesPerPackage) + + def avgMethodsPerClass = methodCount / classCount.toDouble + def avgMethodsPerClassFormatted: String = twoFractionDigits(avgMethodsPerClass) + + def loc = files.map(_.loc).sum + def linesPerFile = loc / fileCount.toDouble + def linesPerFileFormatted: String = twoFractionDigits(linesPerFile) + + // returns the classes by least coverage + def risks(limit: Int) = classes.toSeq.sortBy(_.statementCount).reverse.sortBy(_.statementCoverage).take(limit) + + def apply(ids: Iterable[Int]): Unit = ids foreach invoked + def invoked(id: Int): Unit = statementsById.get(id).foreach(_.invoked()) +} + +trait MethodBuilders { + def statements: Iterable[Statement] + def methods: Seq[MeasuredMethod] = { + statements.groupBy(stmt => stmt.location.packageName + "/" + stmt.location.className + "/" + stmt.location.method) + .map(arg => MeasuredMethod(arg._1, arg._2)) + .toSeq + } + def methodCount = methods.size +} + +trait PackageBuilders { + def statements: Iterable[Statement] + def packageCount = packages.size + def packages: Seq[MeasuredPackage] = { + statements.groupBy(_.location.packageName).map(arg => MeasuredPackage(arg._1, arg._2)).toSeq.sortBy(_.name) + } +} + +trait ClassBuilders { + def statements: Iterable[Statement] + def classes = statements.groupBy(_.location.fullClassName).map(arg => MeasuredClass(arg._1, arg._2)) + def classCount: Int = classes.size +} + +trait FileBuilders { + def statements: Iterable[Statement] + def files: Iterable[MeasuredFile] = statements.groupBy(_.source).map(arg => MeasuredFile(arg._1, arg._2)) + def fileCount: Int = files.size +} + +case class MeasuredMethod(name: String, statements: Iterable[Statement]) extends CoverageMetrics { + override def ignoredStatements: Iterable[Statement] = Seq() +} + +case class MeasuredClass(fullClassName: String, statements: Iterable[Statement]) + extends CoverageMetrics with MethodBuilders { + + def source: String = statements.head.source + def loc = statements.map(_.line).max + + /** + * The class name for display is the FQN minus the package, + * for example "com.a.Foo.Bar.Baz" should display as "Foo.Bar.Baz" + * and "com.a.Foo" should display as "Foo". + * + * This is used in the class lists in the package and overview pages. + */ + def displayClassName = statements.headOption.map(_.location).map { location => + location.fullClassName.stripPrefix(location.packageName + ".") + }.getOrElse(fullClassName) + + override def ignoredStatements: Iterable[Statement] = Seq() +} + +case class MeasuredPackage(name: String, statements: Iterable[Statement]) + extends CoverageMetrics with ClassCoverage with ClassBuilders with FileBuilders { + override def ignoredStatements: Iterable[Statement] = Seq() +} + +case class MeasuredFile(source: String, statements: Iterable[Statement]) + extends CoverageMetrics with ClassCoverage with ClassBuilders { + def filename = new File(source).getName + def loc = statements.map(_.line).max + + override def ignoredStatements: Iterable[Statement] = Seq() +} + +case class Statement(source: String, + location: Location, + id: Int, + start: Int, + end: Int, + line: Int, + desc: String, + symbolName: String, + treeName: String, + branch: Boolean, + var count: Int = 0, + ignored: Boolean = false) extends java.io.Serializable { + def invoked(): Unit = count = count + 1 + def isInvoked = count > 0 +} + +sealed trait ClassType +object ClassType { + case object Object extends ClassType + case object Class extends ClassType + case object Trait extends ClassType + def fromString(str: String): ClassType = { + str.toLowerCase match { + case "object" => Object + case "trait" => Trait + case _ => Class + } + } +} + +case class ClassRef(name: String) { + lazy val simpleName = name.split(".").last + lazy val getPackage = name.split(".").dropRight(1).mkString(".") +} + +object ClassRef { + def fromFilepath(path: String) = ClassRef(path.replace('/', '.')) + def apply(_package: String, className: String): ClassRef = ClassRef(_package.replace('/', '.') + "." + className) +} + +trait CoverageMetrics { + def statements: Iterable[Statement] + def statementCount: Int = statements.size + + def ignoredStatements: Iterable[Statement] + def ignoredStatementCount: Int = ignoredStatements.size + + def invokedStatements: Iterable[Statement] = statements.filter(_.count > 0) + def invokedStatementCount = invokedStatements.size + def statementCoverage: Double = if (statementCount == 0) 1 else invokedStatementCount / statementCount.toDouble + def statementCoveragePercent = statementCoverage * 100 + def statementCoverageFormatted: String = twoFractionDigits(statementCoveragePercent) + def branches: Iterable[Statement] = statements.filter(_.branch) + def branchCount: Int = branches.size + def branchCoveragePercent = branchCoverage * 100 + def invokedBranches: Iterable[Statement] = branches.filter(_.count > 0) + def invokedBranchesCount = invokedBranches.size + + /** + * @see http://stackoverflow.com/questions/25184716/scoverage-ambiguous-measurement-from-branch-coverage + */ + def branchCoverage: Double = { + // if there are zero branches, then we have a single line of execution. + // in that case, if there is at least some coverage, we have covered the branch. + // if there is no coverage then we have not covered the branch + if (branchCount == 0) { + if (statementCoverage > 0) 1 + else 0 + } else { + invokedBranchesCount / branchCount.toDouble + } + } + def branchCoverageFormatted: String = twoFractionDigits(branchCoveragePercent) +} + +trait ClassCoverage { + this: ClassBuilders => + val statements: Iterable[Statement] + def invokedClasses: Int = classes.count(_.statements.count(_.count > 0) > 0) + def classCoverage: Double = invokedClasses / classes.size.toDouble +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala new file mode 100644 index 0000000..d0bec2f --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala @@ -0,0 +1,634 @@ +package scoverage + +import java.io.File +import java.util.concurrent.atomic.AtomicInteger + +import scala.reflect.internal.ModifierFlags +import scala.reflect.internal.util.SourceFile +import scala.tools.nsc.Global +import scala.tools.nsc.plugins.{PluginComponent, Plugin} +import scala.tools.nsc.transform.{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) + val instrumentationComponent = new ScoverageInstrumentationComponent(global, extraAfterPhase, extraBeforePhase) + override val components: List[PluginComponent] = List(instrumentationComponent) + + override def processOptions(opts: List[String], error: String => Unit) { + val options = new ScoverageOptions + for (opt <- opts) { + if (opt.startsWith("excludedPackages:")) { + options.excludedPackages = opt.substring("excludedPackages:".length).split(";").map(_.trim).filterNot(_.isEmpty) + } else if (opt.startsWith("excludedFiles:")) { + options.excludedFiles = opt.substring("excludedFiles:".length).split(";").map(_.trim).filterNot(_.isEmpty) + } else if (opt.startsWith("excludedSymbols:")) { + options.excludedSymbols = opt.substring("excludedSymbols:".length).split(";").map(_.trim).filterNot(_.isEmpty) + } else if (opt.startsWith("dataDir:")) { + options.dataDir = opt.substring("dataDir:".length) + } else if (opt.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")){ + // skip here, these flags are processed elsewhere + } else { + error("Unknown option: " + opt) + } + } + if (!opts.exists(_.startsWith("dataDir:"))) + throw new RuntimeException("Cannot invoke plugin without specifying ") + instrumentationComponent.setOptions(options) + } + + override val optionsHelp: Option[String] = Some(Seq( + "-P:scoverage:dataDir: where the coverage files should be written\n", + "-P:scoverage:excludedPackages:; semicolon separated list of regexs for packages to exclude", + "-P:scoverage:excludedFiles:; semicolon separated list of regexs for paths to exclude", + "-P:scoverage:excludedSymbols:; semicolon separated list of regexs for symbols to exclude", + "-P:scoverage:extraAfterPhase: phase after which scoverage phase runs (must be after typer phase)", + "-P:scoverage:extraBeforePhase: 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")) + + // 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) + } +} + +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 +} + +class ScoverageInstrumentationComponent(val global: Global, extraAfterPhase: Option[String], extraBeforePhase: Option[String]) + extends PluginComponent + with TypingTransformers + with Transform { + + import global._ + + val statementIds = new AtomicInteger(0) + val coverage = new Coverage + + override val phaseName: String = "scoverage-instrumentation" + override val runsAfter: List[String] = List("typer") ::: extraAfterPhase.toList + override val runsBefore: List[String] = List("patmat") ::: extraBeforePhase.toList + + /** + * Our options are not provided at construction time, but shortly after, + * so they start as None. + * You must call "setOptions" before running any commands that rely on + * the options. + */ + private var options: ScoverageOptions = new ScoverageOptions() + private var coverageFilter: CoverageFilter = AllCoverageFilter + + def setOptions(options: ScoverageOptions): Unit = { + this.options = options + coverageFilter = new RegexCoverageFilter(options.excludedPackages, options.excludedFiles, options.excludedSymbols) + new File(options.dataDir).mkdirs() // ensure data directory is created + } + + override def newPhase(prev: scala.tools.nsc.Phase): Phase = new Phase(prev) { + + override def run(): Unit = { + reporter.echo(s"[info] Cleaning datadir [${options.dataDir}]") + // we clean the data directory, because if the code has changed, then the number / order of + // statements has changed by definition. So the old data would reference statements incorrectly + // and thus skew the results. + IOUtils.clean(options.dataDir) + + reporter.echo("[info] Beginning coverage instrumentation") + super.run() + reporter.echo(s"[info] Instrumentation completed [${coverage.statements.size} statements]") + + Serializer.serialize(coverage, Serializer.coverageFile(options.dataDir)) + reporter.echo(s"[info] Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]") + reporter.echo(s"[info] Will write measurement data to [${options.dataDir}]") + } + } + + protected def newTransformer(unit: CompilationUnit): Transformer = new Transformer(unit) + + class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) { + + import global._ + + // contains the location of the last node + var location: Location = null + + /** + * The 'start' of the position, if it is available, else -1 + * We cannot use 'isDefined' to test whether pos.start will work, as some + * classes (e.g. scala.reflect.internal.util.OffsetPosition have + * isDefined true, but throw on `start` + */ + def safeStart(tree: Tree): Int = scala.util.Try(tree.pos.start).getOrElse(-1) + def safeEnd(tree: Tree): Int = scala.util.Try(tree.pos.end).getOrElse(-1) + def safeLine(tree: Tree): Int = if (tree.pos.isDefined) tree.pos.line else -1 + def safeSource(tree: Tree): Option[SourceFile] = if (tree.pos.isDefined) Some(tree.pos.source) else None + + def invokeCall(id: Int): Tree = { + Apply( + Select( + Select( + Ident("scoverage"), + newTermName("Invoker") + ), + newTermName("invoked") + ), + List( + Literal( + Constant(id) + ), + Literal( + Constant(options.dataDir) + ) + ) + ) + } + + override def transform(tree: Tree) = process(tree) + + def transformStatements(trees: List[Tree]): List[Tree] = trees.map(process) + + def transformForCases(cases: List[CaseDef]): List[CaseDef] = { + // we don't instrument the synthetic case _ => false clause + cases.dropRight(1).map(c => { + treeCopy.CaseDef( + // in a for-loop we don't care about instrumenting the guards, as they are synthetically generated + c, c.pat, process(c.guard), process(c.body) + ) + }) ++ cases.takeRight(1) + } + + def transformCases(cases: List[CaseDef]): List[CaseDef] = { + cases.map(c => { + treeCopy.CaseDef( + c, c.pat, process(c.guard), process(c.body) + ) + }) + } + + def instrument(tree: Tree, original: Tree, branch: Boolean = false): Tree = { + safeSource(tree) match { + case None => + reporter.echo(s"[warn] Could not instrument [${tree.getClass.getSimpleName}/${tree.symbol}]. No pos.") + tree + case Some(source) => + val id = statementIds.incrementAndGet + val statement = Statement( + source.path, + location, + id, + safeStart(tree), + safeEnd(tree), + safeLine(tree), + original.toString, + Option(original.symbol).fold("")(_.fullNameString), + tree.getClass.getSimpleName, + branch + ) + if (tree.pos.isDefined && !isStatementIncluded(tree.pos)) { + coverage.add(statement.copy(ignored = true)) + tree + } else { + coverage.add(statement) + + val apply = invokeCall(id) + val block = Block(List(apply), tree) + localTyper.typed(atPos(tree.pos)(block)) + } + } + } + + def isClassIncluded(symbol: Symbol): Boolean = coverageFilter.isClassIncluded(symbol.fullNameString) + def isFileIncluded(source: SourceFile): Boolean = coverageFilter.isFileIncluded(source) + def isStatementIncluded(pos: Position): Boolean = coverageFilter.isLineIncluded(pos) + def isSymbolIncluded(symbol: Symbol): Boolean = coverageFilter.isSymbolIncluded(symbol.fullNameString) + + def updateLocation(t: Tree) { + Location(global)(t) match { + case Some(loc) => this.location = loc + case _ => reporter.warning(t.pos, s"[warn] Cannot update location for $t") + } + } + + def transformPartial(c: ClassDef): ClassDef = { + treeCopy.ClassDef( + c, c.mods, c.name, c.tparams, + treeCopy.Template( + c.impl, c.impl.parents, c.impl.self, c.impl.body.map { + case d: DefDef if d.name.toString == "applyOrElse" => + d.rhs match { + case Match(selector, cases) => + treeCopy.DefDef( + d, d.mods, d.name, d.tparams, d.vparamss, d.tpt, + treeCopy.Match( + // note: do not transform last case as that is the default handling + d.rhs, selector, transformCases(cases.init) :+ cases.last + ) + ) + case _ => + reporter.error(c.pos ,"Cannot instrument partial function apply. Please file bug report") + d + } + case other => other + } + ) + ) + } + + def debug(t: Tree) { + import scala.reflect.runtime.{universe => u} + reporter.echo(t.getClass.getSimpleName + ": LINE " + safeLine(t) + ": " + u.showRaw(t)) + } + + def traverseApplication(t: Tree): Tree = { + t match { + case a: ApplyToImplicitArgs => treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)) + case Apply(Select(_, name), List(fun@Function(params, body))) + if name.toString == "withFilter" && fun.symbol.isSynthetic && fun.toString.contains("check$ifrefutable$1") => t + case a: Apply => treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)) + case a: TypeApply => treeCopy.TypeApply(a, traverseApplication(a.fun), transformStatements(a.args)) + case s: Select => treeCopy.Select(s, traverseApplication(s.qualifier), s.name) + case i: Ident => i + case t: This => t + case other => process(other) + } + } + + private def isSynthetic(t: Tree): Boolean = Option(t.symbol).fold(false)(_.isSynthetic) + private def isNonSynthetic(t: Tree): Boolean = !isSynthetic(t) + private def containsNonSynthetic(t: Tree): Boolean = isNonSynthetic(t) || t.children.exists(containsNonSynthetic) + + def allConstArgs(args: List[Tree]) = args.forall(arg => arg.isInstanceOf[Literal] || arg.isInstanceOf[Ident]) + + def process(tree: Tree): Tree = { + tree match { + + // // non ranged inside ranged will break validation after typer, which only kicks in for yrangepos. + // case t if !t.pos.isRange => super.transform(t) + + // ignore macro expanded code, do not send to super as we don't want any children to be instrumented + case t if t.attachments.all.toString().contains("MacroExpansionAttachment") => t + + // /** + // * Object creation from new. + // * Ignoring creation calls to anon functions + // */ + // case a: GenericApply if a.symbol.isConstructor && a.symbol.enclClass.isAnonymousFunction => tree + // case a: GenericApply if a.symbol.isConstructor => instrument(a) + + /** + * When an apply has no parameters, or is an application of purely literals or idents + * then we can simply instrument the outer call. Ie, we can treat it all as one single statement + * for the purposes of code coverage. + * This will include calls to case apply. + */ + case a: GenericApply if allConstArgs(a.args) => instrument(a, a) + + /** + * Applications of methods with non trivial args means the args themselves + * must also be instrumented + */ + //todo remove once scala merges into Apply proper + case a: ApplyToImplicitArgs => + instrument(treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)), a) + + // handle 'new' keywords, instrumenting parameter lists + case a@Apply(s@Select(New(tpt), name), args) => + instrument(treeCopy.Apply(a, s, transformStatements(args)), a) + case a: Apply => + instrument(treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)), a) + case a: TypeApply => + instrument(treeCopy.TypeApply(a, traverseApplication(a.fun), transformStatements(a.args)), a) + + /** pattern match with syntax `Assign(lhs, rhs)`. + * This AST node corresponds to the following Scala code: + * lhs = rhs + */ + case assign: Assign => treeCopy.Assign(assign, assign.lhs, process(assign.rhs)) + + /** pattern match with syntax `Block(stats, expr)`. + * This AST node corresponds to the following Scala code: + * { stats; expr } + * If the block is empty, the `expr` is set to `Literal(Constant(()))`. + */ + case b: Block => + treeCopy.Block(b, transformStatements(b.stats), transform(b.expr)) + + // special support to handle partial functions + case c: ClassDef if c.symbol.isAnonymousFunction && + c.symbol.enclClass.superClass.nameString.contains("AbstractPartialFunction") => + if (isClassIncluded(c.symbol)) { + transformPartial(c) + } else { + c + } + + // scalac generated classes, we just instrument the enclosed methods/statements + // the location would stay as the source class + case c: ClassDef if c.symbol.isAnonymousClass || c.symbol.isAnonymousFunction => + if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol)) + super.transform(tree) + else { + c + } + + case c: ClassDef => + if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol)) { + updateLocation(c) + super.transform(tree) + } else { + c + } + + // ignore macro definitions in 2.11 + case DefDef(mods, _, _, _, _, _) if mods.isMacro => tree + + // this will catch methods defined as macros, eg def test = macro testImpl + // it will not catch macro implementations + case d: DefDef if d.symbol != null + && d.symbol.annotations.size > 0 + && d.symbol.annotations.toString() == "macroImpl" => + tree + + // will catch macro implementations, as they must end with Expr, however will catch + // any method that ends in Expr. // todo add way of allowing methods that return Expr + case d: DefDef if d.symbol != null && !isSymbolIncluded(d.tpt.symbol) => + tree + + // we can ignore primary constructors because they are just empty at this stage, the body is added later. + case d: DefDef if d.symbol.isPrimaryConstructor => tree + + /** + * Case class accessors for vals + * EG for case class CreditReject(req: MarketOrderRequest, client: ActorRef) + * def req: com.sksamuel.scoverage.samples.MarketOrderRequest + * def client: akka.actor.ActorRef + */ + case d: DefDef if d.symbol.isCaseAccessor => tree + + // Compiler generated case apply and unapply. Ignore these + case d: DefDef if d.symbol.isCaseApplyOrUnapply => tree + + /** + * Lazy stable DefDefs are generated as the impl for lazy vals. + */ + case d: DefDef if d.symbol.isStable && d.symbol.isGetter && d.symbol.isLazy => + updateLocation(d) + treeCopy.DefDef(d, d.mods, d.name, d.tparams, d.vparamss, d.tpt, process(d.rhs)) + + /** + * Stable getters are methods generated for access to a top level val. + * Should be ignored as this is compiler generated code. + * + * Eg + * def MaxCredit: scala.math.BigDecimal = CreditEngine.this.MaxCredit + * def alwaysTrue: String = InstrumentLoader.this.alwaysTrue + */ + case d: DefDef if d.symbol.isStable && d.symbol.isGetter => tree + + /** Accessors are auto generated setters and getters. + * Eg + * private def _clientName: String = + * def cancellable: akka.actor.Cancellable = PriceEngine.this.cancellable + * def cancellable_=(x$1: akka.actor.Cancellable): Unit = PriceEngine.this.cancellable = x$1 + */ + case d: DefDef if d.symbol.isAccessor => tree + + // was `abstract' for members | trait is virtual + case d: DefDef if tree.symbol.isDeferred => tree + + /** eg + * override def hashCode(): Int + * def copy$default$1: com.sksamuel.scoverage.samples.MarketOrderRequest + * def $default$3: Option[org.joda.time.LocalDate] @scala.annotation.unchecked.uncheckedVariance = scala.None + */ + case d: DefDef if d.symbol.isSynthetic => tree + + /** Match all remaining def definitions + * + * If the return type is not specified explicitly (i.e. is meant to be inferred), + * this is expressed by having `tpt` set to `TypeTree()` (but not to an `EmptyTree`!). + */ + case d: DefDef => + updateLocation(d) + treeCopy.DefDef(d, d.mods, d.name, d.tparams, d.vparamss, d.tpt, process(d.rhs)) + + case EmptyTree => tree + + // handle function bodies. This AST node corresponds to the following Scala code: vparams => body + case f: Function => + treeCopy.Function(tree, f.vparams, process(f.body)) + + case _: Ident => tree + + // the If statement itself doesn't need to be instrumented, because instrumenting the condition is + // enough to determine if the If statement was executed. + // The two procedures (then and else) are instrumented separately to determine if we entered + // both branches. + case i: If => + treeCopy.If(i, + process(i.cond), + instrument(process(i.thenp), i.thenp, branch = true), + instrument(process(i.elsep), i.elsep, branch = true)) + + case _: Import => tree + + // labeldefs are never written natively in scala + case l: LabelDef => + treeCopy.LabelDef(tree, l.name, l.params, transform(l.rhs)) + + // profile access to a literal for function args todo do we need to do this? + case l: Literal => instrument(l, l) + + // pattern match clauses will be instrumented per case + case m@Match(selector: Tree, cases: List[CaseDef]) => + // we can be fairly sure this was generated as part of a for loop + if (selector.toString.contains("check$") + && selector.tpe.annotations.mkString == "unchecked" + && m.cases.last.toString == "case _ => false") { + treeCopy.Match(tree, process(selector), transformForCases(cases)) + } else { + // if the selector was added by compiler, we don't want to instrument it.... + // that usually means some construct is being transformed into a match + if (Option(selector.symbol).exists(_.isSynthetic)) + treeCopy.Match(tree, selector, transformCases(cases)) + else + // .. but we will if it was a user match + treeCopy.Match(tree, process(selector), transformCases(cases)) + } + + // a synthetic object is a generated object, such as case class companion + case m: ModuleDef if m.symbol.isSynthetic => + updateLocation(m) + super.transform(tree) + + // user defined objects + case m: ModuleDef => + if (isFileIncluded(m.pos.source) && isClassIncluded(m.symbol)) { + updateLocation(m) + super.transform(tree) + } else { + m + } + + /** + * match with syntax `New(tpt)`. + * This AST node corresponds to the following Scala code: + * + * `new` T + * + * This node always occurs in the following context: + * + * (`new` tpt).[targs](args) + * + * For example, an AST representation of: + * + * new Example[Int](2)(3) + * + * is the following code: + * + * Apply( + * Apply( + * TypeApply( + * Select(New(TypeTree(typeOf[Example])), nme.CONSTRUCTOR) + * TypeTree(typeOf[Int])), + * List(Literal(Constant(2)))), + * List(Literal(Constant(3)))) + * + */ + case n: New => n + + case s@Select(n@New(tpt), name) => + instrument(treeCopy.Select(s, n, name), s) + + case p: PackageDef => + if (isClassIncluded(p.symbol)) treeCopy.PackageDef(p, p.pid, transformStatements(p.stats)) + else p + + // This AST node corresponds to the following Scala code: `return` expr + case r: Return => + treeCopy.Return(r, transform(r.expr)) + + /** pattern match with syntax `Select(qual, name)`. + * This AST node corresponds to the following Scala code: + * + * qualifier.selector + * + * Should only be used with `qualifier` nodes which are terms, i.e. which have `isTerm` returning `true`. + * Otherwise `SelectFromTypeTree` should be used instead. + * + * foo.Bar // represented as Select(Ident(), ) + * Foo#Bar // represented as SelectFromTypeTree(Ident(), ) + */ + case s: Select if location == null => tree + + /** + * I think lazy selects are the LHS of a lazy assign. + * todo confirm we can ignore + */ + case s: Select if s.symbol.isLazy => tree + + case s: Select => instrument(treeCopy.Select(s, traverseApplication(s.qualifier), s.name), s) + + case s: Super => tree + + // This AST node corresponds to the following Scala code: qual.this + case t: This => super.transform(tree) + + // This AST node corresponds to the following Scala code: `throw` expr + case t: Throw => instrument(tree, tree) + + // This AST node corresponds to the following Scala code: expr: tpt + case t: Typed => super.transform(tree) + + // instrument trys, catches and finally as separate blocks + case Try(t: Tree, cases: List[CaseDef], f: Tree) => + treeCopy.Try(tree, + instrument(process(t), t, branch = true), + transformCases(cases), + instrument(process(f), f, branch = true)) + + // type aliases, type parameters, abstract types + case t: TypeDef => super.transform(tree) + + case t: Template => + updateLocation(t) + treeCopy.Template(tree, t.parents, t.self, transformStatements(t.body)) + + case _: TypeTree => super.transform(tree) + + /** + * We can ignore lazy val defs as they are implemented by a generated defdef + */ + case v: ValDef if v.symbol.isLazy => + val w = v + tree + + /** + * val default: A1 => B1 = + * val x1: Any = _ + */ + case v: ValDef if v.symbol.isSynthetic => + val w = v + tree + + /** + * Vals declared in case constructors + */ + case v: ValDef if v.symbol.isParamAccessor && v.symbol.isCaseAccessor => + val w = v + tree + + // we need to remove the final mod so that we keep the code in order to check its invoked + case v: ValDef if v.mods.isFinal => + updateLocation(v) + treeCopy.ValDef(v, v.mods.&~(ModifierFlags.FINAL), v.name, v.tpt, process(v.rhs)) + + /** + * This AST node corresponds to any of the following Scala code: + * + * mods `val` name: tpt = rhs + * mods `var` name: tpt = rhs + * mods name: tpt = rhs // in signatures of function and method definitions + * self: Bar => // self-types + * + * For user defined value statements, we will instrument the RHS. + * + * This includes top level non-lazy vals. Lazy vals are generated as stable defs. + */ + case v: ValDef => + updateLocation(v) + treeCopy.ValDef(tree, v.mods, v.name, v.tpt, process(v.rhs)) + + case _ => + reporter.warning(tree.pos, "BUG: Unexpected construct: " + tree.getClass + " " + tree.symbol) + super.transform(tree) + } + } + } +} + diff --git a/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala b/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala new file mode 100644 index 0000000..c4005df --- /dev/null +++ b/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala @@ -0,0 +1,7 @@ +package scoverage + +object Invoker { + + // We explicitly call the java conversion else predef.scala will be used, and that may itself be instrumented. + def invoked(id: Int, dataDir: String) = InvokerJ.invokedJ(java.lang.Integer.valueOf(id), dataDir) +} diff --git a/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java new file mode 100644 index 0000000..970ec31 --- /dev/null +++ b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java @@ -0,0 +1,64 @@ +package scoverage; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; + +public class InvokerJ { + + private static String MeasurementsPrefix = "scoverage.measurements."; + + private static final ThreadLocal> threadFiles = + new ThreadLocal>() { + @Override protected HashMap initialValue() { + return new HashMap(); + } + }; + + private static ConcurrentHashMap ids = new ConcurrentHashMap(); + + /** + * We record that the given id has been invoked by appending its id to the coverage + * data file. + * + * This will happen concurrently on as many threads as the application is using, + * so we use one file per thread, named for the thread id. + * + * This method is not thread-safe if the threads are in different JVMs, because + * the thread IDs may collide. + * You may not use `scoverage` on multiple processes in parallel without risking + * corruption of the measurement file. + * + * @param id the id of the statement that was invoked + * @param dataDir the directory where the measurement data is held + */ + public static void invokedJ(final Integer id, final String dataDir)throws IOException { + String idStr = Integer.toString(id); + String key = new String(dataDir + idStr); + + if (!ids.containsKey(key)) { + // Each thread writes to a separate measurement file, to reduce contention + // and because file appends via FileWriter are not atomic on Windows. + HashMap files = threadFiles.get(); + if(!files.containsKey(dataDir)) + files.put(dataDir, new FileWriter(measurementFile(dataDir), true)); + FileWriter writer = files.get(dataDir); + writer.append(idStr + '\n').flush(); + + ids.put(key, Boolean.TRUE); + } + } + + private static File measurementFile(File dataDir){ + return measurementFile(dataDir.getAbsolutePath()); + } + + private static File measurementFile(String dataDir) { + StringBuilder sb = new StringBuilder(MeasurementsPrefix); + String threadId = Long.toString(Thread.currentThread().getId()); + return new File(dataDir, sb.append(threadId).toString()); + } +} diff --git a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala new file mode 100644 index 0000000..5ee0c5a --- /dev/null +++ b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala @@ -0,0 +1,71 @@ +package scoverage + +import scala.collection.{mutable, Set} +import scoverage.Platform._ + +/** @author Stephen Samuel */ +object Invoker { + + private val MeasurementsPrefix = "scoverage.measurements." + private val threadFiles = new ThreadLocal[ThreadSafeMap[String, FileWriter]] + private val ids = ThreadSafeMap.empty[(String, Int), Any] + + /** + * We record that the given id has been invoked by appending its id to the coverage + * data file. + * + * This will happen concurrently on as many threads as the application is using, + * so we use one file per thread, named for the thread id. + * + * This method is not thread-safe if the threads are in different JVMs, because + * the thread IDs may collide. + * You may not use `scoverage` on multiple processes in parallel without risking + * corruption of the measurement file. + * + * @param id the id of the statement that was invoked + * @param dataDir the directory where the measurement data is held + */ + def invoked(id: Int, dataDir: String): Unit = { + // [sam] we can do this simple check to save writing out to a file. + // This won't work across JVMs but since there's no harm in writing out the same id multiple + // times since for coverage we only care about 1 or more, (it just slows things down to + // do it more than once), anything we can do to help is good. This helps especially with code + // that is executed many times quickly, eg tight loops. + if (!ids.contains(dataDir, id)) { + // Each thread writes to a separate measurement file, to reduce contention + // and because file appends via FileWriter are not atomic on Windows. + var files = threadFiles.get() + if (files == null) { + files = ThreadSafeMap.empty[String, FileWriter] + threadFiles.set(files) + } + val writer = files.getOrElseUpdate(dataDir, new FileWriter(measurementFile(dataDir), true)) + writer.append(id.toString + '\n').flush() + + ids.put((dataDir, id), ()) + } + } + + def measurementFile(dataDir: File): File = measurementFile(dataDir.getAbsolutePath) + def measurementFile(dataDir: String): File = new File(dataDir, MeasurementsPrefix + Thread.currentThread.getId) + + def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles(new File(dataDir)) + def findMeasurementFiles(dataDir: File): Array[File] = dataDir.listFiles(new FileFilter { + override def accept(pathname: File): Boolean = pathname.getName.startsWith(MeasurementsPrefix) + }) + + // loads all the invoked statement ids from the given files + def invoked(files: Seq[File]): Set[Int] = { + val acc = mutable.Set[Int]() + files.foreach { file => + val reader = Source.fromFile(file) + for (line <- reader.getLines()) { + if (!line.isEmpty) { + acc += line.toInt + } + } + reader.close() + } + acc + } +} diff --git a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala new file mode 100644 index 0000000..19f445e --- /dev/null +++ b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala @@ -0,0 +1,21 @@ +package scoverage + +import scala.collection.concurrent.TrieMap +import scala.collection.generic.{ CanBuildFrom, MutableMapFactory } +import java.io.{ + File => SupportFile, + FileWriter => SupportFileWriter, + FileFilter => SupportFileFilter +} +import scala.io.{ Source => SupportSource } + +object Platform { + type ThreadSafeMap[A, B] = TrieMap[A, B] + lazy val ThreadSafeMap = TrieMap + + type File = SupportFile + type FileWriter = SupportFileWriter + type FileFilter = SupportFileFilter + + lazy val Source = SupportSource +} diff --git a/version.sbt b/version.sbt new file mode 100644 index 0000000..c5438ff --- /dev/null +++ b/version.sbt @@ -0,0 +1 @@ +version in ThisBuild := "2.0.0-M0" From 02212bbf8c930a436eb0af7e2706a9eee8c33f66 Mon Sep 17 00:00:00 2001 From: Alistair Johnson Date: Thu, 13 Oct 2016 15:46:47 +0200 Subject: [PATCH 2/3] Added XML module --- build.sbt | 11 ++- .../src/main/scala/scoverage/Serializer.scala | 74 ++++++++++--------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/build.sbt b/build.sbt index 2b7102c..2bb4e0a 100644 --- a/build.sbt +++ b/build.sbt @@ -10,9 +10,10 @@ val JUnitVersion = "0.9" val appSettings = Seq( organization := Org, - crossVersion := CrossVersion.full, // because compiler api is not binary compatible + scalaVersion := "2.11.8", - crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-M3", "2.12.0-M4","2.12.0-M5","2.12.0-RC1", "2.12.0-RC1-ceaf419"), + crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-RC1"), + fork in Test := false, publishMavenStyle := true, publishArtifact in Test := false, @@ -80,11 +81,15 @@ lazy val plugin = Project("scalac-scoverage-plugin", file("scalac-scoverage-plug //.dependsOn(`scalac-scoverage-runtime-java` % "test") .settings(name := "scalac-scoverage-plugin") .settings(appSettings: _*) + .settings( + crossVersion := CrossVersion.full, // because compiler api is not binary compatible + crossScalaVersions ++= Seq("2.12.0-M3", "2.12.0-M4","2.12.0-M5", "2.12.0-RC1-ceaf419") + ) .settings(libraryDependencies ++= Seq( // "org.mockito" % "mockito-all" % MockitoVersion % "test", // "org.scalatest" %% "scalatest" % ScalatestVersion % "test", "com.novocode" % "junit-interface" % "0.9" % "test", - "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided" excludeAll(ExclusionRule(organization="org.scala-lang.modules")), + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided", "org.joda" % "joda-convert" % "1.6" % "test", "joda-time" % "joda-time" % "2.3" % "test" )) diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala index 2edae22..f88f5f2 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala @@ -3,6 +3,7 @@ package scoverage import java.io._ import scala.io.Source +import scala.xml.Utility object Serializer { @@ -19,59 +20,60 @@ object Serializer { def serialize(coverage: Coverage, writer: Writer): Unit = { def writeStatement(stmt: Statement, writer: Writer): Unit = { writer.write { - val xml = "" + - "" + + val xml = + {stmt.source} - "" + - "" + + + {stmt.location.packageName} - "" + - "" + + + {stmt.location.className} - "" + - "" + + + {stmt.location.classType.toString} - "" + - "" + + + {stmt.location.fullClassName} - "" + - "" + + + {stmt.location.method} - "" + - "" + + + {stmt.location.sourcePath} - "" + - "" + + + {stmt.id.toString} - "" + - "" + + + {stmt.start.toString} - "" + - "" + + + {stmt.end.toString} - "" + - "" + + + {stmt.line.toString} - "" + - "" + + + {escape(stmt.desc)} - "" + - "" + + + {escape(stmt.symbolName)} - "" + - "" + + + {escape(stmt.treeName)} - "" + - "" + + + {stmt.branch.toString} - "" + - "" + + + {stmt.count.toString} - "" + - "" + + + {stmt.ignored.toString} - "" + - "" + + + Utility.trim(xml) + "\n" } } writer.write("\n") From 88881ae0d892541fce3c4a3e135209abe759ed4b Mon Sep 17 00:00:00 2001 From: Alistair Johnson Date: Sun, 16 Oct 2016 17:41:11 +0200 Subject: [PATCH 3/3] Added tests and more --- build.sbt | 190 +++++---- .../src/main/scala/scoverage/AssertUtil.scala | 14 + .../main/scala/scoverage/CoverageTest.scala | 35 ++ .../main/scala/scoverage/IOUtilsTest.scala | 83 ++++ .../scoverage/InvokerConcurrencyTest.scala | 63 +++ .../scoverage/InvokerMultiModuleTest.scala | 57 +++ .../main/scala/scoverage/InvokerStub.scala | 10 + .../scala/scoverage/LocationCompiler.scala | 52 +++ .../main/scala/scoverage/LocationTest.scala | 258 +++++++++++++ .../scoverage/PluginASTSupportTest.scala | 105 +++++ .../scala/scoverage/PluginCoverageTest.scala | 359 ++++++++++++++++++ .../scoverage/RegexCoverageFilterTest.scala | 226 +++++++++++ .../scala/scoverage/ScoverageCompiler.scala | 161 ++++++++ .../main/scala/scoverage/SerializerTest.scala | 35 ++ .../src/main/scala/scoverage/Serializer.scala | 3 +- .../src/main/scala/scoverage/InvokerJ.java | 1 - .../src/test/scala/scoverage/AllTests.scala | 16 + .../src/main/scala/scoverage/Invoker.scala | 30 +- .../src/main/scala/scoverage/Platform.scala | 21 - .../src/test/scala/scoverage/AllTests.scala | 16 + 20 files changed, 1612 insertions(+), 123 deletions(-) create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala create mode 100644 scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala create mode 100644 scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala delete mode 100644 scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala create mode 100644 scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala diff --git a/build.sbt b/build.sbt index 2bb4e0a..d043b44 100644 --- a/build.sbt +++ b/build.sbt @@ -5,91 +5,133 @@ import com.typesafe.sbt.pgp.PgpKeys val Org = "org.scoverage" -//val MockitoVersion = "1.10.19" -val JUnitVersion = "0.9" +val MockitoVersion = "1.10.19" +val JUnitInterfaceVersion = "0.9" +val JUnitVersion = "4.11" -val appSettings = Seq( - organization := Org, +lazy val fullCrossSettings = Seq( + crossVersion := CrossVersion.full // because compiler api is not binary compatible +) ++ allCrossSettings + +lazy val binaryCrossSettings = Seq( + crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-RC2") +) - scalaVersion := "2.11.8", - crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-RC1"), +lazy val allCrossSettings = Seq( + crossScalaVersions := Seq( + "2.10.6", + "2.11.8", + "2.12.0-M3", + "2.12.0-M4", + "2.12.0-M5", + "2.12.0-RC1-ceaf419", + "2.12.0-RC1", + "2.12.0-RC1-1e81a09", + "2.12.0-RC2") +) - fork in Test := false, - publishMavenStyle := true, - publishArtifact in Test := false, - parallelExecution in Test := false, - scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8"), - javacOptions := { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, scalaMajor)) if scalaMajor < 12 => Seq("-source", "1.7", "-target", "1.7") - case _ => Seq() - } - }, - concurrentRestrictions in Global += Tags.limit(Tags.Test, 1), - publishTo := { - val nexus = "https://oss.sonatype.org/" - if (isSnapshot.value) - Some("snapshots" at nexus + "content/repositories/snapshots") - else - Some("releases" at nexus + "service/local/staging/deploy/maven2") - }, - pomExtra := { - https://github.com/scoverage/scalac-scoverage-plugin-core - - - Apache 2 - http://www.apache.org/licenses/LICENSE-2.0 - repo - - - - git@github.com:scoverage/scalac-scoverage-plugin-core.git - scm:git@github.com:scoverage/scalac-scoverage-plugin-core.git - - - - sksamuel - Stephen Samuel - http://github.com/sksamuel - - - }, - pomIncludeRepository := { - _ => false +val appSettings = Seq( + organization := Org, + scalaVersion := "2.11.8", + fork in Test := false, + publishMavenStyle := true, + publishArtifact in Test := false, + parallelExecution in Test := false, + scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8"), + javacOptions := { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, scalaMajor)) if scalaMajor < 12 => Seq("-source", "1.7", "-target", "1.7") + case _ => Seq() } - ) ++ Seq( - releaseCrossBuild := true, - releasePublishArtifactsAction := PgpKeys.publishSigned.value + }, + libraryDependencies += + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided", + + concurrentRestrictions in Global += Tags.limit(Tags.Test, 1), + publishTo := { + val nexus = "https://oss.sonatype.org/" + + if (isSnapshot.value) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + }, + pomExtra := { + https://github.com/scoverage/scalac-scoverage-plugin-core + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + git@github.com:scoverage/scalac-scoverage-plugin-core.git + scm:git@github.com:scoverage/scalac-scoverage-plugin-core.git + + + + sksamuel + Stephen Samuel + http://github.com/sksamuel + + + }, + pomIncludeRepository := { + _ => false + } +) ++ Seq( + releaseCrossBuild := true, + releasePublishArtifactsAction := PgpKeys.publishSigned.value +) + +lazy val noPublishSettings = Seq( + publishArtifact := false, + // The above is enough for Maven repos but it doesn't prevent publishing of ivy.xml files + publish := {}, + publishLocal := {} +) + +lazy val junitSettings = Seq( + testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v"), + libraryDependencies ++= Seq( + "com.novocode" % "junit-interface" % JUnitInterfaceVersion % "test", + "junit" % "junit" % JUnitVersion % "test" ) +) lazy val root = Project("scalac-scoverage", file(".")) - .settings(name := "scalac-scoverage") - .settings(appSettings: _*) - .settings(publishArtifact := false) - .aggregate(plugin, runtimeJava, runtimeScala) - + .settings(name := "scalac-scoverage") + .settings(appSettings: _*) + .settings(allCrossSettings) + .settings(noPublishSettings) + .aggregate(plugin, runtimeJava, runtimeScala, pluginTests) lazy val runtimeJava = Project("scalac-scoverage-runtime-java", file("scalac-scoverage-runtime-java")) - .settings(name := "scalac-scoverage-runtime-java") - .settings(appSettings: _*) + .settings(name := "scalac-scoverage-runtime-java") + .settings(appSettings: _*) + .settings(binaryCrossSettings) + .settings(junitSettings) + .dependsOn(pluginTests % "test->compile") lazy val runtimeScala = Project("scalac-scoverage-runtime-scala", file("scalac-scoverage-runtime-scala")) - .settings(name := "scalac-scoverage-runtime-scala") - .settings(appSettings: _*) + .settings(name := "scalac-scoverage-runtime-scala") + .settings(appSettings: _*) + .settings(binaryCrossSettings) + .settings(junitSettings) + .dependsOn(pluginTests % "test->compile") lazy val plugin = Project("scalac-scoverage-plugin", file("scalac-scoverage-plugin")) - //.dependsOn(`scalac-scoverage-runtime-java` % "test") - .settings(name := "scalac-scoverage-plugin") - .settings(appSettings: _*) - .settings( - crossVersion := CrossVersion.full, // because compiler api is not binary compatible - crossScalaVersions ++= Seq("2.12.0-M3", "2.12.0-M4","2.12.0-M5", "2.12.0-RC1-ceaf419") - ) - .settings(libraryDependencies ++= Seq( - // "org.mockito" % "mockito-all" % MockitoVersion % "test", - // "org.scalatest" %% "scalatest" % ScalatestVersion % "test", - "com.novocode" % "junit-interface" % "0.9" % "test", - "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided", - "org.joda" % "joda-convert" % "1.6" % "test", - "joda-time" % "joda-time" % "2.3" % "test" + .settings(name := "scalac-scoverage-plugin") + .settings(appSettings: _*) + .settings(fullCrossSettings) + +lazy val pluginTests = Project("scalac-scoverage-plugin-tests", file("scalac-scoverage-plugin-tests")) + .dependsOn(plugin) + .settings(name := "scalac-scoverage-plugin-tests") + .settings(appSettings: _*) + .settings(binaryCrossSettings) + .settings(libraryDependencies ++= Seq( + "org.mockito" % "mockito-all" % MockitoVersion, + "com.novocode" % "junit-interface" % JUnitInterfaceVersion )) diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala new file mode 100644 index 0000000..0071f12 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala @@ -0,0 +1,14 @@ +package scoverage + +import scala.xml.Node + +object AssertUtil { + + implicit class TypedOps[A](v1: A) { + def ===(v2: A): Boolean = v1 == v2 + } + + implicit class NodeOps(n1: Node) { + def ===(n2: Node): Boolean = n1.strict_==(n2) + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala new file mode 100644 index 0000000..38b558c --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala @@ -0,0 +1,35 @@ +package scoverage + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** @author Stephen Samuel */ +@RunWith(classOf[JUnit4]) +class CoverageTest { + + @Test + def coverageForNoStatementsIs1() = { + val coverage = Coverage() + assertTrue(1.0 === coverage.statementCoverage) + } + + @Test + def coverageForNoInvokedStatementsIs0() = { + val coverage = Coverage() + coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 1, 2, 3, 4, "", "", "", false, 0)) + assertTrue(0.0 === coverage.statementCoverage) + } + + @Test + def coverageForInvokedStatements() = { + val coverage = Coverage() + coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 1, 2, 3, 4, "", "", "", false, 3)) + coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 2, 2, 3, 4, "", "", "", false, 0)) + coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 3, 2, 3, 4, "", "", "", false, 0)) + coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 4, 2, 3, 4, "", "", "", false, 0)) + assertTrue(0.25 === coverage.statementCoverage) + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala new file mode 100644 index 0000000..77c8bab --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala @@ -0,0 +1,83 @@ +package scoverage + +import java.io.{File, FileWriter} +import java.util.UUID + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.{After, Before, Test} +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** @author Stephen Samuel */ +@RunWith(classOf[JUnit4]) +class IOUtilsTest { + + val base = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) + + @Before def setup(): Unit = { + base.mkdir() + } + + @Test + def shouldParseMeasurementFiles() = { + val file = newTempFile("scoveragemeasurementtest.txt") + val writer = new FileWriter(file) + writer.write("1\n5\n9\n\n10\n") + writer.close() + val invokedSet = IOUtils.invoked(Seq(file)).toSet + + assertTrue(invokedSet === Set(1, 5, 9, 10)) + } + + @Test + def shouldParseMultipleMeasurementFiles() = { + val file1 = newTempFile("scoverage.measurements.11.txt") + val writer1 = new FileWriter(file1) + writer1.write("1\n5\n9\n\n10\n") + writer1.close() + val file2 = newTempFile("scoverage.measurements.22.txt") + val writer2 = new FileWriter(file2) + writer2.write("1\n7\n14\n\n2\n") + writer2.close() + + val files = IOUtils.findMeasurementFiles(file1.getParent) + val invokedSet = IOUtils.invoked(files).toSet + + assertTrue(invokedSet === Set(1, 2, 5, 7, 9, 10, 14)) + } + + @Test + def shouldDeepSearchForReportFiles() = { + + val file1 = newTempFile(Constants.XMLReportFilename) + val writer1 = new FileWriter(file1) + writer1.write("1\n3\n5\n\n\n7\n") + writer1.close() + + val file2 = newTempFile(UUID.randomUUID + "/" + Constants.XMLReportFilename) + file2.getParentFile.mkdir() + val writer2 = new FileWriter(file2) + writer2.write("2\n4\n6\n\n8\n") + writer2.close() + + val file3 = new File(file2.getParent + "/" + UUID.randomUUID + "/" + Constants.XMLReportFilename) + file3.getParentFile.mkdir() + val writer3 = new FileWriter(file3) + writer3.write("11\n20\n30\n\n44\n") + writer3.close() + + val files = IOUtils.reportFileSearch(base, IOUtils.isReportFile) + val invokedSet = IOUtils.invoked(files).toSet + + assertTrue(invokedSet === Set(1, 2, 3, 4, 5, 6, 7, 8, 11, 20, 30, 44)) + } + + @After def cleanup(): Unit = { + base.delete() + } + + private def newTempFile(file: String): File = { + new File(s"$base/$file") + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala new file mode 100644 index 0000000..f9a74e1 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala @@ -0,0 +1,63 @@ +package scoverage + +import java.io.File +import java.util.UUID +import java.util.concurrent.Executors + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.{After, Before, Test} +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import scala.collection.breakOut +import scala.concurrent._ +import scala.concurrent.duration._ + +/** + * Verify that the runtime is thread-safe + */ +@RunWith(classOf[JUnit4]) +class InvokerConcurrencyTest { + + implicit val executor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8)) + + val measurementDir = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) //"target/invoker-test.measurement.concurrent") + + @Before def setup(): Unit = { + deleteMeasurementFiles() + measurementDir.mkdirs() + } + + @Test + def callingInvokerInvokedOnMultipleThreadsDoesNotCorruptTheMeasurementFile() = { + + val testIds: Set[Int] = (1 to 1000).toSet + + // Create 1k "invoked" calls on the common thread pool, to stress test + // the method + val futures: List[Future[Unit]] = testIds.map { i: Int => + Future { + Invoker.invoked(i, measurementDir.toString) + } + }(breakOut) + + futures.foreach(Await.result(_, 1.second)) + + // Now verify that the measurement file is not corrupted by loading it + val measurementFiles = IOUtils.findMeasurementFiles(measurementDir) + val idsFromFile = IOUtils.invoked(measurementFiles).toSet + + assertTrue(idsFromFile === testIds) + } + + @After def cleanup(): Unit = { + deleteMeasurementFiles() + measurementDir.delete() + } + + private def deleteMeasurementFiles(): Unit = { + if (measurementDir.isDirectory) + measurementDir.listFiles().foreach(_.delete()) + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala new file mode 100644 index 0000000..b530f12 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala @@ -0,0 +1,57 @@ +package scoverage + +import java.io.File +import java.util.UUID + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.{After, Before, Test} +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Verify that the runtime can handle a multi-module project + */ +@RunWith(classOf[JUnit4]) +class InvokerMultiModuleTest { + + val measurementDir = Array( + new File(IOUtils.getTempDirectory, UUID.randomUUID.toString), + new File(IOUtils.getTempDirectory, UUID.randomUUID.toString)) + + @Before def setup(): Unit = { + deleteMeasurementFiles() + measurementDir.foreach(_.mkdirs()) + } + + @Test + def callingInvokerInvokedOnWithDifferentDirectoriesPutsMeasurementsInDifferentDirectories() = { + + val testIds: Set[Int] = (1 to 10).toSet + + testIds.map { i: Int => Invoker.invoked(i, measurementDir(i % 2).toString) } + + // Verify measurements went to correct directory + val measurementFiles0 = IOUtils.findMeasurementFiles(measurementDir(0)) + val idsFromFile0 = IOUtils.invoked(measurementFiles0).toSet + + assertTrue(idsFromFile0 === testIds.filter { i: Int => i % 2 == 0 }) + + val measurementFiles1 = IOUtils.findMeasurementFiles(measurementDir(1)) + val idsFromFile1 = IOUtils.invoked(measurementFiles1).toSet + + assertTrue(idsFromFile1 === testIds.filter { i: Int => i % 2 == 1 }) + } + + @After def cleanup(): Unit = { + deleteMeasurementFiles() + measurementDir.foreach(_.delete) + } + + private def deleteMeasurementFiles(): Unit = { + measurementDir.foreach((md) => { + if (md.isDirectory) + md.listFiles().foreach(_.delete()) + }) + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala new file mode 100644 index 0000000..5010e9c --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala @@ -0,0 +1,10 @@ +package scoverage + +object RuntimeInfo { + def runtimePath: String = ??? + def name: String = ??? +} + +object Invoker { + def invoked(id: Int, dataDir: String): Unit = ??? +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala new file mode 100644 index 0000000..b0cda99 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala @@ -0,0 +1,52 @@ +package scoverage + +import java.io.File + +import scala.tools.nsc.Global +import scala.tools.nsc.plugins.PluginComponent +import scala.tools.nsc.transform.{Transform, TypingTransformers} + +class LocationCompiler(settings: scala.tools.nsc.Settings, reporter: scala.tools.nsc.reporters.Reporter) + extends scala.tools.nsc.Global(settings, reporter) { + + val locations = List.newBuilder[(String, Location)] + private val locationSetter = new LocationSetter(this) + + def compile(code: String): Unit = { + val files = writeCodeSnippetToTempFile(code) + val command = new scala.tools.nsc.CompilerCommand(List(files.getAbsolutePath), settings) + new Run().compile(command.files) + } + + def writeCodeSnippetToTempFile(code: String): File = { + val file = File.createTempFile("code_snippet", ".scala") + IOUtils.writeToFile(file, code) + file.deleteOnExit() + file + } + + class LocationSetter(val global: Global) extends PluginComponent with TypingTransformers with Transform { + + override val phaseName = "location-setter" + override val runsAfter = List("typer") + override val runsBefore = List("patmat") + + override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit) + + class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) { + + override def transform(tree: global.Tree) = { + for (location <- Location(global)(tree)) { + locations += (tree.getClass.getSimpleName -> location) + } + super.transform(tree) + } + } + + } + + override def computeInternalPhases() { + super.computeInternalPhases() + addToPhasesSet(locationSetter, "sets locations") + } +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala new file mode 100644 index 0000000..7563aaa --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala @@ -0,0 +1,258 @@ +package scoverage + +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(classOf[JUnit4]) +class LocationTest { + + @Test + def processTopLevelTypesForClasses() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.test\nclass Sammy") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.test") + assertEquals(loc.className, "Sammy") + assertEquals(loc.fullClassName, "com.test.Sammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def processTopLevelTypesForObjects() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.test\nobject Bammy { def foo = 'boo } ") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.test") + assertEquals(loc.className, "Bammy") + assertEquals(loc.fullClassName, "com.test.Bammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Object) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def processTopLevelTypesForTraits() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.test\ntrait Gammy { def goo = 'hoo } ") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.test") + assertEquals(loc.className, "Gammy") + assertEquals(loc.fullClassName, "com.test.Gammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Trait) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldCorrectlyProcessMethods() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Hammy { def foo = 'boo } ") + val loc = compiler.locations.result().find(_._2.method == "foo").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Hammy") + assertEquals(loc.fullClassName, "com.methodtest.Hammy") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldCorrectlyProcessNestedMethods() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Hammy { def foo = { def goo = { getClass; 3 }; goo } } ") + val loc = compiler.locations.result().find(_._2.method == "goo").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Hammy") + assertEquals(loc.fullClassName, "com.methodtest.Hammy") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldProcessAnonFunctionsAsInsideTheEnclosingMethod() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Jammy { def moo = { Option(\"bat\").map(_.length) } } ") + val loc = compiler.locations.result().find(_._1 == "Function").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Jammy") + assertEquals(loc.fullClassName, "com.methodtest.Jammy") + assertEquals(loc.method, "moo") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldUseOuterPackageForNestedClasses() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Jammy { class Pammy } ") + val loc = compiler.locations.result().find(_._2.className == "Pammy").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Pammy") + assertEquals(loc.fullClassName, "com.methodtest.Jammy.Pammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + + } + + @Test + def shouldUseOuterPackageForNestedObjects() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Jammy { object Zammy } ") + val loc = compiler.locations.result().find(_._2.className == "Zammy").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Zammy") + assertEquals(loc.fullClassName, "com.methodtest.Jammy.Zammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Object) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldUseOuterPackageForNestedTraits() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.methodtest \n class Jammy { trait Mammy } ") + val loc = compiler.locations.result().find(_._2.className == "Mammy").get._2 + assertEquals(loc.packageName, "com.methodtest") + assertEquals(loc.className, "Mammy") + assertEquals(loc.fullClassName, "com.methodtest.Jammy.Mammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Trait) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + + @Test + def shouldSupportNestedPackagesForClasses() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.a \n " + + "package b \n" + + "class Kammy ") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.a.b") + assertEquals(loc.className, "Kammy") + assertEquals(loc.fullClassName, "com.a.b.Kammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldSupportNestedPackagesForObjects() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.a \n " + + "package b \n" + + "object Kammy ") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.a.b") + assertEquals(loc.className, "Kammy") + assertEquals(loc.fullClassName, "com.a.b.Kammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Object) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldSupportNestedPackagesForTraits() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.a \n " + + "package b \n" + + "trait Kammy ") + val loc = compiler.locations.result().find(_._1 == "Template").get._2 + assertEquals(loc.packageName, "com.a.b") + assertEquals(loc.className, "Kammy") + assertEquals(loc.fullClassName, "com.a.b.Kammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Trait) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + + @Test + def shouldUseNoneMethodNameForClassConstructorBody() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.b \n class Tammy { val name = 'sam } ") + val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 + assertEquals(loc.packageName, "com.b") + assertEquals(loc.className, "Tammy") + assertEquals(loc.fullClassName, "com.b.Tammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldUseNoneMethodNameForObjectConstructorBody() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.b \n object Yammy { val name = 'sam } ") + val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 + assertEquals(loc.packageName, "com.b") + assertEquals(loc.className, "Yammy") + assertEquals(loc.fullClassName, "com.b.Yammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Object) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def shouldUseNoneMethodNameForTraitConstructorBody() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.b \n trait Wammy { val name = 'sam } ") + val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 + assertEquals(loc.packageName, "com.b") + assertEquals(loc.className, "Wammy") + assertEquals(loc.fullClassName, "com.b.Wammy") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Trait) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def anonClassShouldReportEnclosingClass() = { + val compiler = ScoverageCompiler.locationCompiler + compiler + .compile( + "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }") + println() + println(compiler.locations.result().mkString("\n")) + val loc = compiler.locations.result().filter(_._1 == "Template").last._2 + assertEquals(loc.packageName, "com.a") + assertEquals(loc.className, "C") + assertEquals(loc.fullClassName, "com.a.C") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def anonClassImplementedMethodShouldReportEnclosingMethod() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile( + "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }") + val loc = compiler.locations.result().filter(_._1 == "DefDef").last._2 + assertEquals(loc.packageName, "com.a") + assertEquals(loc.className, "C") + assertEquals(loc.fullClassName, "com.a.C") + assertEquals(loc.method, "invoke") + assertEquals(loc.classType, ClassType.Class) + assertTrue(loc.sourcePath.endsWith(".scala")) + } + + @Test + def doublyNestedClassesShouldReportCorrectFullClassName() = { + val compiler = ScoverageCompiler.locationCompiler + compiler.compile("package com.a \n object Foo { object Boo { object Moo { val name = 'sam } } }") + val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 + assertEquals(loc.packageName, "com.a") + assertEquals(loc.className, "Moo") + assertEquals(loc.fullClassName, "com.a.Foo.Boo.Moo") + assertEquals(loc.method, "") + assertEquals(loc.classType, ClassType.Object) + assertTrue(loc.sourcePath.endsWith(".scala")) + } +} + + diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala new file mode 100644 index 0000000..d5d521e --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala @@ -0,0 +1,105 @@ +package scoverage + +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** @author Stephen Samuel */ +@RunWith(classOf[JUnit4]) +class PluginASTSupportTest { + + @Test + def scoverageComponentShouldIgnoreBasicMacros() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( """ + | object MyMacro { + | import scala.language.experimental.macros + | import scala.reflect.macros.Context + | def test = macro testImpl + | def testImpl(c: Context): c.Expr[Unit] = { + | import c.universe._ + | reify { + | println("macro test") + | } + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + } + + @Test + def scoverageComponentShouldIgnoreComplexMacros11() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( """ object ComplexMacro { + | + | import scala.language.experimental.macros + | import scala.reflect.macros.Context + | + | def debug(params: Any*) = macro debugImpl + | + | def debugImpl(c: Context)(params: c.Expr[Any]*) = { + | import c.universe._ + | + | val trees = params map {param => (param.tree match { + | case Literal(Constant(_)) => reify { print(param.splice) } + | case _ => reify { + | val variable = c.Expr[String](Literal(Constant(show(param.tree)))).splice + | print(s"$variable = ${param.splice}") + | } + | }).tree + | } + | + | val separators = (1 until trees.size).map(_ => (reify { print(", ") }).tree) :+ (reify { println() }).tree + | val treesWithSeparators = trees zip separators flatMap {p => List(p._1, p._2)} + | + | c.Expr[Unit](Block(treesWithSeparators.toList, Literal(Constant(())))) + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + } + + // https://github.com/scoverage/scalac-scoverage-plugin/issues/32 + @Test + def exhaustiveWarningsShouldNotBeGeneratedForUnchecked() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( """object PartialMatchObject { + | def partialMatchExample(s: Option[String]): Unit = { + | (s: @unchecked) match { + | case Some(str) => println(str) + | } + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + } + + + + // https://github.com/scoverage/scalac-scoverage-plugin/issues/45 + @Test + def compileFinalValsInAnnotations() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( """object Foo { + | final val foo = 1L + |} + |@SerialVersionUID(Foo.foo) + |class Bar + |""".stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + } + + @Test + def typeParamWithDefaultArgSupported() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( """class TypeTreeObjects { + | class Container { + | def typeParamAndDefaultArg[C](name: String = "sammy"): String = name + | } + | new Container().typeParamAndDefaultArg[Any]() + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + } +} + diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala new file mode 100644 index 0000000..20b776e --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala @@ -0,0 +1,359 @@ +package scoverage + +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** @author Stephen Samuel */ +@RunWith(classOf[JUnit4]) +class PluginCoverageTest { + + @Test + def scoverageShouldInstrumentDefaultArgumentsWithMethods() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object DefaultArgumentsObject { + | val defaultName = "world" + | def makeGreeting(name: String = defaultName): String = { + | s"Hello, $name" + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + // we expect: + // instrumenting the default-param which becomes a method call invocation + // the method makeGreeting is entered. + compiler.assertNMeasuredStatements(2) + } + + @Test + def scoverageShouldSkipMacros() = { + val compiler = ScoverageCompiler.default + val code = if (ScoverageCompiler.ShortScalaVersion == "2.10") + """ + import scala.language.experimental.macros + import scala.reflect.macros.Context + object Impl { + def poly[T: c.WeakTypeTag](c: Context) = c.literal(c.weakTypeOf[T].toString) + } + + object Macros { + def poly[T] = macro Impl.poly[T] + }""" + else + """ + import scala.language.experimental.macros + import scala.reflect.macros.Context + class Impl(val c: Context) { + import c.universe._ + def poly[T: c.WeakTypeTag] = c.literal(c.weakTypeOf[T].toString) + } + object Macros { + def poly[T] = macro Impl.poly[T] + }""" + compiler.compileCodeSnippet(code) + assertTrue(!compiler.reporter.hasErrors) + compiler.assertNMeasuredStatements(0) + } + + @Test + def scoverageShouldInstrumentFinalVals() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object FinalVals { + | final val name = { + | val name = "sammy" + | if (System.currentTimeMillis() > 0) { + | println(name) + | } + | } + | println(name) + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + // we should have 3 statements - initialising the val, executing println, and executing the parameter + compiler.assertNMeasuredStatements(8) + } + + @Test + def scoverageShouldNotInstrumentTheMatchAsAStatement() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object A { + | System.currentTimeMillis() match { + | case x => println(x) + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + + /** should have the following statements instrumented: + * the selector, clause 1 + */ + compiler.assertNMeasuredStatements(2) + } + + @Test + def scoverageShouldInstrumentMatchGuards() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object A { + | System.currentTimeMillis() match { + | case l if l < 1000 => println("a") + | case l if l > 1000 => println("b") + | case _ => println("c") + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + + /** should have the following statements instrumented: + * the selector, guard 1, clause 1, guard 2, clause 2, clause 3 + */ + compiler.assertNMeasuredStatements(6) + } + + @Test + def scoverageShouldInstrumentNonBasicSelector() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ trait A { + | def someValue = "sammy" + | def foo(a:String) = someValue match { + | case any => "yes" + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + // should instrument: + // the someValue method entry + // the selector call + // case block "yes" literal + compiler.assertNMeasuredStatements(3) + } + + @Test + def scoverageShouldInstrumentConditionalSelectorsInAMatch() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ trait A { + | def foo(a:String) = (if (a == "hello") 1 else 2) match { + | case any => "yes" + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + // should instrument: + // the if clause, + // thenp block, + // thenp literal "1", + // elsep block, + // elsep literal "2", + // case block "yes" literal + compiler.assertNMeasuredStatements(6) + } + + // https://github.com/scoverage/sbt-scoverage/issues/16 + @Test + def scoverageShouldInstrumentForLoopsButNotTheGeneratedScaffolding() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ trait A { + | def print1(list: List[String]) = for (string: String <- list) println(string) + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should instrument: + // the def method entry + // foreach body + // specifically we want to avoid the withFilter partial function added by the compiler + compiler.assertNMeasuredStatements(2) + } + + @Test + def scoverageShouldInstrumentForLoopGuards() = { + val compiler = ScoverageCompiler.default + + compiler.compileCodeSnippet( + """object A { + | def foo(list: List[String]) = for (string: String <- list if string.length > 5) + | println(string) + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should instrument: + // foreach body + // the guard + // but we want to avoid the withFilter partial function added by the compiler + compiler.assertNMeasuredStatements(3) + } + + @Test + def scoverageShouldCorrectlyHandleNewWithArgsApplyWithListOfArgs() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object A { + | new String(new String(new String)) + | } """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should have 3 statements, one for each of the nested strings + compiler.assertNMeasuredStatements(3) + } + + @Test + def scoverageShouldCorrectlyHandleNoArgsNewApplyEmptyListOfArgs() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ object A { + | new String + | } """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should have 1. the apply that wraps the select. + compiler.assertNMeasuredStatements(1) + } + + @Test + def scoverageShouldCorrectlyHandleNewThatInvokesNestedStatements() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ + | object A { + | val value = new java.util.concurrent.CountDownLatch(if (System.currentTimeMillis > 1) 5 else 10) + | } """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should have 6 statements - the apply/new statement, two literals, the if cond, if elsep, if thenp + compiler.assertNMeasuredStatements(6) + } + + @Test + def scoverageShouldInstrumentValRHS() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """object A { + | val name = BigDecimal(50.0) + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + compiler.assertNMeasuredStatements(1) + } + + @Test + def scoverageShouldNotInstrumentFunctionTupleWrapping() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ + | sealed trait Foo + | case class Bar(s: String) extends Foo + | case object Baz extends Foo + | + | object Foo { + | implicit val fooOrdering: Ordering[Foo] = Ordering.fromLessThan { + | case (Bar(_), Baz) => true + | case (Bar(a), Bar(b)) => a < b + | case (_, _) => false + | } + | } + """.stripMargin) + + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should have 4 profiled statements: the outer apply, the true, the a < b, the false + // we are testing that we don't instrument the tuple2 call used here + compiler.assertNMeasuredStatements(4) + } + + @Test + def scoverageShouldInstrumentAllCaseStatementsInAnExplicitMatch() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ trait A { + | def foo(name: Any) = name match { + | case i : Int => 1 + | case b : Boolean => println("boo") + | case _ => 3 + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + assertTrue(!compiler.reporter.hasWarnings) + // should have one statement for each case body + // selector is a constant so would be ignored. + compiler.assertNMeasuredStatements(3) + } + + @Test + def pluginShouldSupportYields() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ + | object Yielder { + | val holidays = for ( name <- Seq("sammy", "clint", "lee"); + | place <- Seq("london", "philly", "iowa") ) yield { + | name + " has been to " + place + | } + | }""".stripMargin) + assertTrue(!compiler.reporter.hasErrors) + // 2 statements for the two applies in Seq, one for each literal which is 6, one for the flat map, + // one for the map, one for the yield op. + compiler.assertNMeasuredStatements(11) + } + + @Test + def pluginShouldNotInstrumentLocalMacroImplementation() = { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ + | object MyMacro { + | import scala.language.experimental.macros + | import scala.reflect.macros.Context + | def test = macro testImpl + | def testImpl(c: Context): c.Expr[Unit] = { + | import c.universe._ + | reify { + | println("macro test") + | } + | } + |} """.stripMargin) + assertTrue(!compiler.reporter.hasErrors) + compiler.assertNoCoverage() + } + + /* Make sure this is covered in another repo, then delete + + test("plugin should not instrument expanded macro code github.com/skinny-framework/skinny-framework/issues/97") { + val compiler = ScoverageCompiler.default + scalaLoggingDeps.foreach(compiler.addToClassPath(_)) + compiler.compileCodeSnippet( s"""import ${scalaLoggingPackageName}.StrictLogging + |class MacroTest extends StrictLogging { + | logger.info("will break") + |} """.stripMargin) + assert(!compiler.reporter.hasErrors) + assert(!compiler.reporter.hasWarnings) + compiler.assertNoCoverage() + } + + ignore("plugin should handle return inside catch github.com/scoverage/scalac-scoverage-plugin/issues/93") { + val compiler = ScoverageCompiler.default + compiler.compileCodeSnippet( + """ + | object bob { + | def fail(): Boolean = { + | try { + | true + | } catch { + | case _: Throwable => + | Option(true) match { + | case Some(bool) => return recover(bool) // comment this return and instrumentation succeeds + | case _ => + | } + | false + | } + | } + | def recover(it: Boolean): Boolean = it + | } + """.stripMargin) + assert(!compiler.reporter.hasErrors) + assert(!compiler.reporter.hasWarnings) + compiler.assertNMeasuredStatements(11) + } + */ +} diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala new file mode 100644 index 0000000..69a5e95 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala @@ -0,0 +1,226 @@ +package scoverage + +import org.mockito.Mockito +import org.mockito.Mockito._ + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import scala.reflect.internal.util.{BatchSourceFile, SourceFile, NoFile} +import scala.reflect.io.AbstractFile + +@RunWith(classOf[JUnit4]) +class RegexCoverageFilterTest { + + @Test + def isClassIncludedShouldReturnTrueForEmptyExcludes() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded("x")) + } + + @Test + def isClassIncludedShouldNotCrashForEmptyInput() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded("")) + } + + @Test + def isClassIncludedShouldExcludeScoverageArrowScoverage() = { + assertTrue(!new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverage")) + } + + @Test + def isClassIncludedShouldIncludeScoverageArrowScoverageeee() = { + assertTrue(new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverageeee")) + } + + @Test + def isClassIncludedShouldExcludeScoverageStarArrowScoverageeee() = { + assertTrue(!new RegexCoverageFilter(Seq("scoverage*"), Nil, Nil).isClassIncluded("scoverageeee")) + } + + @Test + def isClassIncludedShouldIncludeEeeArrowScoverageeee() = { + assertTrue(new RegexCoverageFilter(Seq("eee"), Nil, Nil).isClassIncluded("scoverageeee")) + } + + @Test + def isClassIncludedShouldExcludeDotStarEeeArrowScoverageeee() = { + assertTrue(!new RegexCoverageFilter(Seq(".*eee"), Nil, Nil).isClassIncluded("scoverageeee")) + } + + val abstractFile = mock(classOf[AbstractFile]) + Mockito.when(abstractFile.path).thenReturn("sammy.scala") + + @Test + def isFileIncludedShouldReturnTrueForEmptyExcludes() = { + val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) + assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isFileIncluded(file)) + } + + @Test + def isFileIncludedShouldExcludeByFilename() = { + val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) + assertFalse(new RegexCoverageFilter(Nil, Seq("sammy"), Nil).isFileIncluded(file)) + } + + @Test + def isFileIncludedShouldExcludeByRegexWildcard() = { + val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) + assertFalse(new RegexCoverageFilter(Nil, Seq("sam.*"), Nil).isFileIncluded(file)) + } + + @Test + def isFileIncludedShouldNotExcludeNonMatchingRegex() = { + val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) + assertTrue(new RegexCoverageFilter(Nil, Seq("qweqeqwe"), Nil).isFileIncluded(file)) + } + + val options = new ScoverageOptions() + + @Test + def isSymbolIncludedShouldReturnTrueForEmptyExcludes() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("x")) + } + + @Test + def isSymbolIncludedShouldNotCrashForEmptyInput() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("")) + } + + @Test + def isSymbolIncludedShouldExcludeScoverageArrowScoverage() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverage")) + } + + @Test + def isSymbolIncludedShouldIncludeScoverageArrowScoverageeee() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverageeee")) + } + + @Test + def isSymbolIncludedShouldExcludeScoverageStarArrowScoverageeee() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage*")).isSymbolIncluded("scoverageeee")) + } + + @Test + def isSymbolIncludedShouldIncludeEeeArrowScoverageeee() = { + assertTrue(new RegexCoverageFilter(Nil, Nil, Seq("eee")).isSymbolIncluded("scoverageeee")) + } + + @Test + def isSymbolIncludedShouldExcludeDotStarEeeArrowScoverageeee() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq(".*eee")).isSymbolIncluded("scoverageeee")) + } + + @Test + def isSymbolIncludedShouldExcludeScalaReflectApiExprsExpr() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Exprs.Expr")) + } + + @Test + def isSymbolIncludedShouldExcludeScalaReflectMacrosUniverseTree() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.macros.Universe.Tree")) + } + + @Test + def isSymbolIncludedShouldExcludeScalaReflectApiTreesTree() = { + assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Trees.Tree")) + } + + @Test + def getExcludedLineNumbersShouldExcludeNoLinesIfNoMagicCommentsAreFound() = { + val file = + """1 + |2 + |3 + |4 + |5 + |6 + |7 + |8 + """.stripMargin + + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + assertTrue(numbers === List.empty) + } + + @Test + def getExcludedLineNumbersShouldExcludeLinesBetweenMagicComments() = { + val file = + """1 + |2 + |3 + | // $COVERAGE-OFF$ + |5 + |6 + |7 + |8 + | // $COVERAGE-ON$ + |10 + |11 + | // $COVERAGE-OFF$ + |13 + | // $COVERAGE-ON$ + |15 + |16 + """.stripMargin + + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + assertTrue(numbers === List(Range(4, 9), Range(12, 14))) + } + + @Test + def getExcludedLineNumbersShouldExcludeAllLinesAfterAnUnpairedMagicComment() = { + val file = + """1 + |2 + |3 + | // $COVERAGE-OFF$ + |5 + |6 + |7 + |8 + | // $COVERAGE-ON$ + |10 + |11 + | // $COVERAGE-OFF$ + |13 + |14 + |15 + """.stripMargin + + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + assertTrue(numbers === List(Range(4, 9), Range(12, 16))) + } + + @Test + def getExcludedLineNumbersShouldAllowTextCommentsOnTheSameLineAsTheMarkers() = { + val file = + """1 + |2 + |3 + | // $COVERAGE-OFF$ because the next lines are boring + |5 + |6 + |7 + |8 + | // $COVERAGE-ON$ resume coverage here + |10 + |11 + | // $COVERAGE-OFF$ but ignore this bit + |13 + |14 + |15 + """.stripMargin + + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + assertTrue(numbers === List(Range(4, 9), Range(12, 16))) + } + + private def mockSourceFile(contents: String): SourceFile = { + new BatchSourceFile(NoFile, contents.toCharArray) + } +} + diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala new file mode 100644 index 0000000..f33d7d5 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala @@ -0,0 +1,161 @@ +package scoverage + +import java.io.{File, FileNotFoundException} +import java.net.URL + +import scala.collection.mutable.ListBuffer +import scala.tools.nsc.{Settings, Global} +import scala.tools.nsc.plugins.PluginComponent +import scala.tools.nsc.transform.{Transform, TypingTransformers} + +/** @author Stephen Samuel */ +object ScoverageCompiler { + + val ScalaVersion = scala.util.Properties.versionNumberString + + val ShortScalaVersion = (ScalaVersion split "[.]").toList match { + case init :+ last if last forall (_.isDigit) => init mkString "." + case _ => ScalaVersion + } + + + def classPath = getScalaJars.map(_.getAbsolutePath) :+ sbtCompileDir.getAbsolutePath :+ runtimeClasses.getAbsolutePath + + def settings: Settings = { + val s = new scala.tools.nsc.Settings + s.Xprint.value = List("all") + s.Yrangepos.value = true + s.Yposdebug.value = true + s.classpath.value = classPath.mkString(File.pathSeparator) + + val path = s"./scalac-scoverage-plugin/target/scala-$ShortScalaVersion/test-generated-classes" + new File(path).mkdirs() + s.d.value = path + s + } + + def default: ScoverageCompiler = { + val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) + new ScoverageCompiler(settings, reporter) + } + + def locationCompiler: LocationCompiler = { + val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) + new LocationCompiler(settings, reporter) + } + + private def getScalaJars: List[File] = { + val scalaJars = List("scala-compiler", "scala-library", "scala-reflect") + scalaJars.map(findScalaJar) + } + + private def sbtCompileDir: File = { + val dir = new File(s"./scalac-scoverage-plugin/target/scala-$ShortScalaVersion/classes") + if (!dir.exists) + throw new FileNotFoundException(s"Could not locate SBT compile directory for plugin files [$dir]") + dir + } + + private def runtimeClasses: File = new File(s"${RuntimeInfo.runtimePath}/target/scala-$ShortScalaVersion/classes") + + private def findScalaJar(artifactId: String): File = findIvyJar("org.scala-lang", artifactId, ScalaVersion) + + private def findIvyJar(groupId: String, artifactId: String, version: String, packaging: String = "jar"): File = { + val userHome = System.getProperty("user.home") + val jarPath = s"$userHome/.ivy2/cache/$groupId/$artifactId/${packaging}s/${artifactId}-${version}.jar" + val file = new File(jarPath) + if (!file.exists) + throw new FileNotFoundException(s"Could not locate [$jarPath].") + file + } +} + +class ScoverageCompiler(settings: scala.tools.nsc.Settings, reporter: scala.tools.nsc.reporters.Reporter) + extends scala.tools.nsc.Global(settings, reporter) { + + def addToClassPath(file: File): Unit = { + settings.classpath.value = settings.classpath.value + File.pathSeparator + file.getAbsolutePath + } + + val instrumentationComponent = new ScoverageInstrumentationComponent(this, None, None) + instrumentationComponent.setOptions(new ScoverageOptions()) + val testStore = new ScoverageTestStoreComponent(this) + val validator = new PositionValidator(this) + + def compileSourceFiles(files: File*): ScoverageCompiler = { + val command = new scala.tools.nsc.CompilerCommand(files.map(_.getAbsolutePath).toList, settings) + new Run().compile(command.files) + this + } + + def writeCodeSnippetToTempFile(code: String): File = { + val file = File.createTempFile("scoverage_snippet", ".scala") + IOUtils.writeToFile(file, code) + file.deleteOnExit() + file + } + + def compileCodeSnippet(code: String): ScoverageCompiler = compileSourceFiles(writeCodeSnippetToTempFile(code)) + + def compileSourceResources(urls: URL*): ScoverageCompiler = { + compileSourceFiles(urls.map(_.getFile).map(new File(_)): _*) + } + + def assertNoCoverage() = assert(!testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked")) + + def assertNMeasuredStatements(n: Int): Unit = { + for (k <- 1 to n) { + assert(testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked($k,"), + s"Should be $n invoked statements but missing #$k") + } + assert(!testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked(${n + 1},"), + s"Found statement ${n + 1} but only expected $n") + } + + class PositionValidator(val global: Global) extends PluginComponent with TypingTransformers with Transform { + + override val phaseName = "scoverage-validator" + override val runsAfter = List("typer") + override val runsBefore = List("scoverage-instrumentation") + + override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit) + + class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) { + + override def transform(tree: global.Tree) = { + global.validatePositions(tree) + tree + } + } + + } + + class ScoverageTestStoreComponent(val global: Global) extends PluginComponent with TypingTransformers with Transform { + + val sources = new ListBuffer[String] + + override val phaseName = "scoverage-teststore" + override val runsAfter = List("jvm") + override val runsBefore = List("terminal") + + override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit) + + class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) { + + override def transform(tree: global.Tree) = { + sources append tree.toString + tree + } + } + + } + + override def computeInternalPhases() { + super.computeInternalPhases() + addToPhasesSet(validator, "scoverage validator") + addToPhasesSet(instrumentationComponent, "scoverage instrumentationComponent") + addToPhasesSet(testStore, "scoverage teststore") + } +} + + diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala new file mode 100644 index 0000000..7995fd7 --- /dev/null +++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala @@ -0,0 +1,35 @@ +package scoverage + +import java.io.StringWriter + +import AssertUtil._ +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import scala.xml.Utility + +@RunWith(classOf[JUnit4]) +class SerializerTest { + + @Test + def coverageShouldBeSerializableIntoXml() = { + val coverage = Coverage() + coverage.add( + Statement( + "mysource", + Location("org.scoverage", "test", "org.scoverage.test", ClassType.Trait, "mymethod", "mypath"), + 14, 100, 200, 4, "def test : String", "test", "DefDef", true, 32 + ) + ) + val expected = + + mysource org.scoverage test Trait org.scoverage.test mymethod mypath 14 100 200 4 def test : String test DefDef true 32 false + + + val writer = new StringWriter() + val actual = Serializer.serialize(coverage, writer) + assertTrue(Utility.trim(expected) === Utility.trim(xml.XML.loadString(writer.toString))) + } +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala index f88f5f2..a3bec6b 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala @@ -2,7 +2,6 @@ package scoverage import java.io._ -import scala.io.Source import scala.xml.Utility object Serializer { @@ -88,7 +87,7 @@ object Serializer { * This method ensures that the output String has only * valid XML unicode characters as specified by the * XML 1.0 standard. For reference, please see - * "the + * the * standard. This method will return an empty * String if the input is null or empty. * diff --git a/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java index 970ec31..d08ce4e 100644 --- a/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java +++ b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java @@ -1,7 +1,6 @@ package scoverage; import java.io.File; -import java.io.FileFilter; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; diff --git a/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala b/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala new file mode 100644 index 0000000..eefc213 --- /dev/null +++ b/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala @@ -0,0 +1,16 @@ +package scoverage + +object RuntimeInfo { + def runtimePath: String = "./scalac-scoverage-runtime-java" + def name: String = "java" +} + +class ThisCoverageTest extends CoverageTest +class ThisInvokerConcurrencyTest extends InvokerConcurrencyTest +class ThisInvokerMultiModuleTest extends InvokerMultiModuleTest +class ThisIOUtilsTest extends IOUtilsTest +class ThisLocationTest extends LocationTest +class ThisPluginASTSupportTest extends PluginASTSupportTest +class ThisPluginCoverageTest extends PluginCoverageTest +class ThisRegexCoverageFilterTest extends RegexCoverageFilterTest +class ThisSerializerTest extends SerializerTest diff --git a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala index 5ee0c5a..ac43c51 100644 --- a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala +++ b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala @@ -1,14 +1,14 @@ package scoverage -import scala.collection.{mutable, Set} -import scoverage.Platform._ +import java.io.{File, FileWriter} +import scala.collection.concurrent.TrieMap /** @author Stephen Samuel */ object Invoker { private val MeasurementsPrefix = "scoverage.measurements." - private val threadFiles = new ThreadLocal[ThreadSafeMap[String, FileWriter]] - private val ids = ThreadSafeMap.empty[(String, Int), Any] + private val threadFiles = new ThreadLocal[TrieMap[String, FileWriter]] + private val ids = TrieMap.empty[(String, Int), Any] /** * We record that the given id has been invoked by appending its id to the coverage @@ -36,7 +36,7 @@ object Invoker { // and because file appends via FileWriter are not atomic on Windows. var files = threadFiles.get() if (files == null) { - files = ThreadSafeMap.empty[String, FileWriter] + files = TrieMap.empty[String, FileWriter] threadFiles.set(files) } val writer = files.getOrElseUpdate(dataDir, new FileWriter(measurementFile(dataDir), true)) @@ -48,24 +48,4 @@ object Invoker { def measurementFile(dataDir: File): File = measurementFile(dataDir.getAbsolutePath) def measurementFile(dataDir: String): File = new File(dataDir, MeasurementsPrefix + Thread.currentThread.getId) - - def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles(new File(dataDir)) - def findMeasurementFiles(dataDir: File): Array[File] = dataDir.listFiles(new FileFilter { - override def accept(pathname: File): Boolean = pathname.getName.startsWith(MeasurementsPrefix) - }) - - // loads all the invoked statement ids from the given files - def invoked(files: Seq[File]): Set[Int] = { - val acc = mutable.Set[Int]() - files.foreach { file => - val reader = Source.fromFile(file) - for (line <- reader.getLines()) { - if (!line.isEmpty) { - acc += line.toInt - } - } - reader.close() - } - acc - } } diff --git a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala deleted file mode 100644 index 19f445e..0000000 --- a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Platform.scala +++ /dev/null @@ -1,21 +0,0 @@ -package scoverage - -import scala.collection.concurrent.TrieMap -import scala.collection.generic.{ CanBuildFrom, MutableMapFactory } -import java.io.{ - File => SupportFile, - FileWriter => SupportFileWriter, - FileFilter => SupportFileFilter -} -import scala.io.{ Source => SupportSource } - -object Platform { - type ThreadSafeMap[A, B] = TrieMap[A, B] - lazy val ThreadSafeMap = TrieMap - - type File = SupportFile - type FileWriter = SupportFileWriter - type FileFilter = SupportFileFilter - - lazy val Source = SupportSource -} diff --git a/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala b/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala new file mode 100644 index 0000000..8f5064a --- /dev/null +++ b/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala @@ -0,0 +1,16 @@ +package scoverage + +object RuntimeInfo { + def runtimePath: String = "./scalac-scoverage-runtime-scala" + def name: String = "scala" +} + +class ThisCoverageTest extends CoverageTest +class ThisInvokerConcurrencyTest extends InvokerConcurrencyTest +class ThisInvokerMultiModuleTest extends InvokerMultiModuleTest +class ThisIOUtilsTest extends IOUtilsTest +class ThisLocationTest extends LocationTest +class ThisPluginASTSupportTest extends PluginASTSupportTest +class ThisPluginCoverageTest extends PluginCoverageTest +class ThisRegexCoverageFilterTest extends RegexCoverageFilterTest +class ThisSerializerTest extends SerializerTest