Skip to content

Commit

Permalink
Merge pull request #741 from olafurpg/sbt-testkit
Browse files Browse the repository at this point in the history
Add ScalafixTestkitPlugin to simplify g8 template.
  • Loading branch information
olafurpg authored May 31, 2018
2 parents 9b331e6 + 20a7fad commit 9f03d60
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 126 deletions.
64 changes: 49 additions & 15 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,10 @@ val testsInput = TestProject(
project.settings(
noPublish,
semanticdbSettings,
scalacOptions ++= List(
{
val sourceroot = baseDirectory.in(ThisBuild).value / srcMain
s"-P:semanticdb:sourceroot:$sourceroot"
}
),
scalacOptions += {
val sourceroot = baseDirectory.in(ThisBuild).value / srcMain / "scala"
s"-P:semanticdb:sourceroot:$sourceroot"
},
scalacOptions ~= (_.filterNot(_ == "-Yno-adapted-args")),
scalacOptions += "-Ywarn-adapted-args", // For NoAutoTupling
scalacOptions += "-Ywarn-unused-import", // For RemoveUnusedImports
Expand All @@ -291,15 +289,20 @@ lazy val testsInput212 = testsInput(scala212, _.dependsOn(testsShared212))

val testsOutput = TestProject(
"output",
_.settings(
noPublish,
semanticdbSettings,
scalacOptions --= List(
warnUnusedImports,
"-Xlint"
),
testsInputOutputSetting
))
(project, srcMain) =>
project.settings(
noPublish,
semanticdbSettings,
unmanagedSourceDirectories.in(Compile) +=
baseDirectory.in(ThisBuild).value / srcMain /
s"scala-${scalaBinaryVersion.value}",
scalacOptions --= List(
warnUnusedImports,
"-Xlint"
),
testsInputOutputSetting
)
)

val testsOutput211 = testsOutput(scala211, _.dependsOn(testsShared211))
val testsOutput212 = testsOutput(scala212, _.dependsOn(testsShared212))
Expand Down Expand Up @@ -359,6 +362,37 @@ def unit(
)
.value
},
resourceGenerators.in(Test) += Def.task {
// copy-pasted code from ScalafixTestkitPlugin to avoid cyclic dependencies between build and sbt-scalafix.
val props = new java.util.Properties()

def put(key: String, files: Seq[File]): Unit =
props.put(
key,
files.iterator
.filter(_.exists())
.mkString(java.io.File.pathSeparator)
)

put(
"inputClasspath",
fullClasspath.in(testsInput, Compile).value.map(_.data)
)
put(
"inputSourceDirectories",
sourceDirectories.in(testsInput, Compile).value
)
put(
"outputSourceDirectories",
sourceDirectories.in(testsOutput, Compile).value ++
sourceDirectories.in(testsOutputDotty, Compile).value
)
val out =
managedResourceDirectories.in(Test).value.head /
"scalafix-testkit.properties"
IO.write(props, "Input data for scalafix testkit", out)
List(out)
},
buildInfoKeys := Seq[BuildInfoKey](
"baseDirectory" ->
baseDirectory.in(ThisBuild).value,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package scalafix.sbt

import sbt.Def
import sbt._
import sbt.Keys._
import java.io.File.pathSeparator
import sbt.plugins.JvmPlugin

object ScalafixTestkitPlugin extends AutoPlugin {
override def trigger: PluginTrigger = noTrigger
override def requires: Plugins = JvmPlugin
object autoImport {
val scalafixTestkitInputClasspath =
taskKey[Classpath]("Classpath of input project")
val scalafixTestkitInputSourceDirectories =
taskKey[Seq[File]]("Source directory of output projects")
val scalafixTestkitOutputSourceDirectories =
taskKey[Seq[File]]("Source directories of output projects")
}
import autoImport._

override def projectSettings: Seq[Def.Setting[_]] = List(
resourceGenerators.in(Test) += Def.task {
val props = new java.util.Properties()
val values = Map[String, Seq[File]](
"inputClasspath" ->
scalafixTestkitInputClasspath.value.map(_.data),
"inputSourceDirectories" ->
scalafixTestkitInputSourceDirectories.value,
"outputSourceDirectories" ->
scalafixTestkitOutputSourceDirectories.value
)
values.foreach {
case (key, files) =>
props.put(
key,
files.iterator.filter(_.exists()).mkString(pathSeparator)
)
}
val out =
managedResourceDirectories.in(Test).value.head /
"scalafix-testkit.properties"
IO.write(props, "Input data for scalafix testkit", out)
List(out)
}
)
}
65 changes: 29 additions & 36 deletions scalafix-testkit/src/main/scala/scalafix/testkit/RuleTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import metaconfig.Conf
import metaconfig.Configured
import metaconfig.internal.ConfGet
import scala.meta.internal.io.FileIO
import scala.meta.internal.io.PathIO
import scalafix.v1
import scala.meta._
import scalafix.internal.util.SymbolTable
Expand All @@ -26,40 +25,34 @@ object RuleTest {
dir: AbsolutePath,
classpath: Classpath,
symtab: SymbolTable): Seq[RuleTest] = {
FileIO
.listAllFilesRecursively(dir)
.files
.filter(p =>
p.toNIO.startsWith("scala") &&
PathIO.extension(p.toNIO) == "scala")
.map { rel =>
new RuleTest(
rel, { () =>
val input = Input.VirtualFile(
rel.toString(),
FileIO.slurp(dir.resolve(rel), StandardCharsets.UTF_8))
val tree = input.parse[Source].get
val doc = v1.Doc.fromTree(tree)
val sdoc = v1.SemanticDoc.fromPath(doc, rel, classpath, symtab)
val comment = SemanticRuleSuite.findTestkitComment(tree.tokens)
val syntax = comment.syntax.stripPrefix("/*").stripSuffix("*/")
val conf = Conf.parseString(rel.toString(), syntax).get
val config = conf.as[ScalafixConfig]
config.andThen(scalafixConfig => {
val decoderSettings =
RuleDecoder.Settings().withConfig(scalafixConfig)
val decoder = RuleDecoder.decoder(decoderSettings)
val rulesConf =
ConfGet
.getKey(conf, "rules" :: "rule" :: Nil)
.getOrElse(Conf.Lst(Nil))
decoder
.read(rulesConf)
.andThen(_.withConfig(conf))
.map(_ -> sdoc)
})
}
)
}
FileIO.listAllFilesRecursively(dir).files.map { rel =>
new RuleTest(
rel, { () =>
val input = Input.VirtualFile(
rel.toString(),
FileIO.slurp(dir.resolve(rel), StandardCharsets.UTF_8))
val tree = input.parse[Source].get
val doc = v1.Doc.fromTree(tree)
val sdoc = v1.SemanticDoc.fromPath(doc, rel, classpath, symtab)
val comment = SemanticRuleSuite.findTestkitComment(tree.tokens)
val syntax = comment.syntax.stripPrefix("/*").stripSuffix("*/")
val conf = Conf.parseString(rel.toString(), syntax).get
val config = conf.as[ScalafixConfig]
config.andThen(scalafixConfig => {
val decoderSettings =
RuleDecoder.Settings().withConfig(scalafixConfig)
val decoder = RuleDecoder.decoder(decoderSettings)
val rulesConf =
ConfGet
.getKey(conf, "rules" :: "rule" :: Nil)
.getOrElse(Conf.Lst(Nil))
decoder
.read(rulesConf)
.andThen(_.withConfig(conf))
.map(_ -> sdoc)
})
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,35 @@
package scalafix.testkit

import java.io.File
import java.nio.charset.StandardCharsets
import scala.meta.internal.io.FileIO
import org.scalatest.BeforeAndAfterAll
import org.scalatest.FunSuite
import org.scalatest.exceptions.TestFailedException
import scala.meta._
import scala.meta.internal.io.FileIO
import scalafix.internal.reflect.ClasspathOps
import scalafix.internal.reflect.RuleCompiler
import scalafix.internal.testkit.AssertDiff
import scalafix.internal.testkit.CommentAssertion
import scalafix.internal.testkit.EndOfLineAssertExtractor
import scalafix.internal.testkit.MultiLineAssertExtractor
import scalafix.v0.SemanticdbIndex

/**
* Construct a test suite for running semantic Scalafix rules.
*
* @param inputClassDirectory The class directory of the input sources. This directory should contain a
* META-INF/semanticd sub-directory with SemanticDB files.
* @param inputSourceroot The source directory of the input sources. This directory should contain Scala code to
* be fixed by Scalafix.
* @param outputSourceroot The source directories of the expected output sources. These directories should contain
* Scala source files with the expected output after running Scalafix. When multiple directories
* are provided, the first directory that contains a source files with a matching relative path
* in inputSourceroot is used.
*/
abstract class SemanticRuleSuite(
inputClassDirectory: File,
inputSourceroot: File,
outputSourceroot: Seq[File]
) extends FunSuite
/** Construct a test suite for running semantic Scalafix rules. */
abstract class SemanticRuleSuite
extends FunSuite
with DiffAssertions
with BeforeAndAfterAll { self =>

private val sourceroot: AbsolutePath =
AbsolutePath(inputSourceroot)
private val classpath: Classpath =
SemanticRuleSuite.defaultClasspath(AbsolutePath(inputClassDirectory))
private val expectedOutputSourceroot: Seq[AbsolutePath] =
outputSourceroot.map(AbsolutePath(_))
@deprecated(
"Use empty constructor instead. Arguments are passed as resource 'scalafix-testkit.properties'",
"0.6.0")
def this(
index: SemanticdbIndex,
inputSourceroot: AbsolutePath,
expectedOutputSourceroot: Seq[AbsolutePath]
) = this()

val props: TestkitProperties = TestkitProperties.loadFromResources()
private def scalaVersion: String = scala.util.Properties.versionNumberString
private def scalaVersionDirectory: Option[String] =
if (scalaVersion.startsWith("2.11")) Some("scala-2.11")
Expand All @@ -54,7 +43,7 @@ abstract class SemanticRuleSuite(

val tokens = fixed.tokenize.get
val obtained = SemanticRuleSuite.stripTestkitComments(tokens)
val candidateOutputFiles = expectedOutputSourceroot.flatMap { root =>
val candidateOutputFiles = props.outputSourceDirectories.flatMap { root =>
val scalaSpecificFilename = scalaVersionDirectory.toList.map { path =>
root.resolve(
RelativePath(
Expand Down Expand Up @@ -100,9 +89,11 @@ abstract class SemanticRuleSuite(

lazy val testsToRun = {
val symtab = ClasspathOps
.newSymbolTable(classpath)
.newSymbolTable(props.inputClasspath)
.getOrElse { sys.error("Failed to load symbol table") }
RuleTest.fromDirectory(sourceroot, classpath, symtab)
props.inputSourceDirectories.flatMap { dir =>
RuleTest.fromDirectory(dir, props.inputClasspath, symtab)
}
}
def runAllTests(): Unit = {
testsToRun.foreach(runOn)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package scalafix.testkit

import scala.meta.AbsolutePath
import scala.meta.Classpath

/**
* Input arguments to run scalafix testkit rules.
*
* @param inputClasspath
* The class directory of the input sources. This directory should contain a
* META-INF/semanticd sub-directory with SemanticDB files.
* @param inputSourceDirectories
* The source directory of the input sources. This directory should contain Scala code to
* be fixed by Scalafix.
* @param outputSourceDirectories
* The source directories of the expected output sources. These directories should contain
* Scala source files with the expected output after running Scalafix. When multiple directories
* are provided, the first directory that contains a source files with a matching relative path
* in inputSourceroot is used.
*/
final class TestkitProperties(
val inputClasspath: Classpath,
val inputSourceDirectories: List[AbsolutePath],
val outputSourceDirectories: List[AbsolutePath]
) {
def inputSourceDirectory: AbsolutePath =
inputSourceDirectories.head
def outputSourceDirectory: AbsolutePath =
outputSourceDirectories.head
override def toString: String = {
val map = Map(
"inputSourceDirectories" -> inputSourceDirectories,
"outputSourceDirectories" -> outputSourceDirectories,
"inputClasspath" -> inputClasspath.syntax
)
pprint.PPrinter.BlackWhite.tokenize(map).mkString
}
}

object TestkitProperties {

/** Loads TestkitProperties from resource "scalafix-testkit.properties" of this classloader. */
def loadFromResources(): TestkitProperties = {
import scala.collection.JavaConverters._
val props = new java.util.Properties()
val path = "scalafix-testkit.properties"
val in = this.getClass.getClassLoader.getResourceAsStream(path)
if (in == null) {
sys.error(s"Failed to load resource $path")
} else {
val sprops = props.asScala
try props.load(in)
finally in.close()
new TestkitProperties(
Classpath(sprops("inputClasspath")),
Classpath(sprops("inputSourceDirectories")).entries,
Classpath(sprops("outputSourceDirectories")).entries
)
}
}

}
Loading

0 comments on commit 9f03d60

Please sign in to comment.