Skip to content

Commit 9f03d60

Browse files
authored
Merge pull request #741 from olafurpg/sbt-testkit
Add ScalafixTestkitPlugin to simplify g8 template.
2 parents 9b331e6 + 20a7fad commit 9f03d60

File tree

8 files changed

+265
-126
lines changed

8 files changed

+265
-126
lines changed

build.sbt

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,10 @@ val testsInput = TestProject(
271271
project.settings(
272272
noPublish,
273273
semanticdbSettings,
274-
scalacOptions ++= List(
275-
{
276-
val sourceroot = baseDirectory.in(ThisBuild).value / srcMain
277-
s"-P:semanticdb:sourceroot:$sourceroot"
278-
}
279-
),
274+
scalacOptions += {
275+
val sourceroot = baseDirectory.in(ThisBuild).value / srcMain / "scala"
276+
s"-P:semanticdb:sourceroot:$sourceroot"
277+
},
280278
scalacOptions ~= (_.filterNot(_ == "-Yno-adapted-args")),
281279
scalacOptions += "-Ywarn-adapted-args", // For NoAutoTupling
282280
scalacOptions += "-Ywarn-unused-import", // For RemoveUnusedImports
@@ -291,15 +289,20 @@ lazy val testsInput212 = testsInput(scala212, _.dependsOn(testsShared212))
291289

292290
val testsOutput = TestProject(
293291
"output",
294-
_.settings(
295-
noPublish,
296-
semanticdbSettings,
297-
scalacOptions --= List(
298-
warnUnusedImports,
299-
"-Xlint"
300-
),
301-
testsInputOutputSetting
302-
))
292+
(project, srcMain) =>
293+
project.settings(
294+
noPublish,
295+
semanticdbSettings,
296+
unmanagedSourceDirectories.in(Compile) +=
297+
baseDirectory.in(ThisBuild).value / srcMain /
298+
s"scala-${scalaBinaryVersion.value}",
299+
scalacOptions --= List(
300+
warnUnusedImports,
301+
"-Xlint"
302+
),
303+
testsInputOutputSetting
304+
)
305+
)
303306

304307
val testsOutput211 = testsOutput(scala211, _.dependsOn(testsShared211))
305308
val testsOutput212 = testsOutput(scala212, _.dependsOn(testsShared212))
@@ -359,6 +362,37 @@ def unit(
359362
)
360363
.value
361364
},
365+
resourceGenerators.in(Test) += Def.task {
366+
// copy-pasted code from ScalafixTestkitPlugin to avoid cyclic dependencies between build and sbt-scalafix.
367+
val props = new java.util.Properties()
368+
369+
def put(key: String, files: Seq[File]): Unit =
370+
props.put(
371+
key,
372+
files.iterator
373+
.filter(_.exists())
374+
.mkString(java.io.File.pathSeparator)
375+
)
376+
377+
put(
378+
"inputClasspath",
379+
fullClasspath.in(testsInput, Compile).value.map(_.data)
380+
)
381+
put(
382+
"inputSourceDirectories",
383+
sourceDirectories.in(testsInput, Compile).value
384+
)
385+
put(
386+
"outputSourceDirectories",
387+
sourceDirectories.in(testsOutput, Compile).value ++
388+
sourceDirectories.in(testsOutputDotty, Compile).value
389+
)
390+
val out =
391+
managedResourceDirectories.in(Test).value.head /
392+
"scalafix-testkit.properties"
393+
IO.write(props, "Input data for scalafix testkit", out)
394+
List(out)
395+
},
362396
buildInfoKeys := Seq[BuildInfoKey](
363397
"baseDirectory" ->
364398
baseDirectory.in(ThisBuild).value,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package scalafix.sbt
2+
3+
import sbt.Def
4+
import sbt._
5+
import sbt.Keys._
6+
import java.io.File.pathSeparator
7+
import sbt.plugins.JvmPlugin
8+
9+
object ScalafixTestkitPlugin extends AutoPlugin {
10+
override def trigger: PluginTrigger = noTrigger
11+
override def requires: Plugins = JvmPlugin
12+
object autoImport {
13+
val scalafixTestkitInputClasspath =
14+
taskKey[Classpath]("Classpath of input project")
15+
val scalafixTestkitInputSourceDirectories =
16+
taskKey[Seq[File]]("Source directory of output projects")
17+
val scalafixTestkitOutputSourceDirectories =
18+
taskKey[Seq[File]]("Source directories of output projects")
19+
}
20+
import autoImport._
21+
22+
override def projectSettings: Seq[Def.Setting[_]] = List(
23+
resourceGenerators.in(Test) += Def.task {
24+
val props = new java.util.Properties()
25+
val values = Map[String, Seq[File]](
26+
"inputClasspath" ->
27+
scalafixTestkitInputClasspath.value.map(_.data),
28+
"inputSourceDirectories" ->
29+
scalafixTestkitInputSourceDirectories.value,
30+
"outputSourceDirectories" ->
31+
scalafixTestkitOutputSourceDirectories.value
32+
)
33+
values.foreach {
34+
case (key, files) =>
35+
props.put(
36+
key,
37+
files.iterator.filter(_.exists()).mkString(pathSeparator)
38+
)
39+
}
40+
val out =
41+
managedResourceDirectories.in(Test).value.head /
42+
"scalafix-testkit.properties"
43+
IO.write(props, "Input data for scalafix testkit", out)
44+
List(out)
45+
}
46+
)
47+
}

scalafix-testkit/src/main/scala/scalafix/testkit/RuleTest.scala

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import metaconfig.Conf
55
import metaconfig.Configured
66
import metaconfig.internal.ConfGet
77
import scala.meta.internal.io.FileIO
8-
import scala.meta.internal.io.PathIO
98
import scalafix.v1
109
import scala.meta._
1110
import scalafix.internal.util.SymbolTable
@@ -26,40 +25,34 @@ object RuleTest {
2625
dir: AbsolutePath,
2726
classpath: Classpath,
2827
symtab: SymbolTable): Seq[RuleTest] = {
29-
FileIO
30-
.listAllFilesRecursively(dir)
31-
.files
32-
.filter(p =>
33-
p.toNIO.startsWith("scala") &&
34-
PathIO.extension(p.toNIO) == "scala")
35-
.map { rel =>
36-
new RuleTest(
37-
rel, { () =>
38-
val input = Input.VirtualFile(
39-
rel.toString(),
40-
FileIO.slurp(dir.resolve(rel), StandardCharsets.UTF_8))
41-
val tree = input.parse[Source].get
42-
val doc = v1.Doc.fromTree(tree)
43-
val sdoc = v1.SemanticDoc.fromPath(doc, rel, classpath, symtab)
44-
val comment = SemanticRuleSuite.findTestkitComment(tree.tokens)
45-
val syntax = comment.syntax.stripPrefix("/*").stripSuffix("*/")
46-
val conf = Conf.parseString(rel.toString(), syntax).get
47-
val config = conf.as[ScalafixConfig]
48-
config.andThen(scalafixConfig => {
49-
val decoderSettings =
50-
RuleDecoder.Settings().withConfig(scalafixConfig)
51-
val decoder = RuleDecoder.decoder(decoderSettings)
52-
val rulesConf =
53-
ConfGet
54-
.getKey(conf, "rules" :: "rule" :: Nil)
55-
.getOrElse(Conf.Lst(Nil))
56-
decoder
57-
.read(rulesConf)
58-
.andThen(_.withConfig(conf))
59-
.map(_ -> sdoc)
60-
})
61-
}
62-
)
63-
}
28+
FileIO.listAllFilesRecursively(dir).files.map { rel =>
29+
new RuleTest(
30+
rel, { () =>
31+
val input = Input.VirtualFile(
32+
rel.toString(),
33+
FileIO.slurp(dir.resolve(rel), StandardCharsets.UTF_8))
34+
val tree = input.parse[Source].get
35+
val doc = v1.Doc.fromTree(tree)
36+
val sdoc = v1.SemanticDoc.fromPath(doc, rel, classpath, symtab)
37+
val comment = SemanticRuleSuite.findTestkitComment(tree.tokens)
38+
val syntax = comment.syntax.stripPrefix("/*").stripSuffix("*/")
39+
val conf = Conf.parseString(rel.toString(), syntax).get
40+
val config = conf.as[ScalafixConfig]
41+
config.andThen(scalafixConfig => {
42+
val decoderSettings =
43+
RuleDecoder.Settings().withConfig(scalafixConfig)
44+
val decoder = RuleDecoder.decoder(decoderSettings)
45+
val rulesConf =
46+
ConfGet
47+
.getKey(conf, "rules" :: "rule" :: Nil)
48+
.getOrElse(Conf.Lst(Nil))
49+
decoder
50+
.read(rulesConf)
51+
.andThen(_.withConfig(conf))
52+
.map(_ -> sdoc)
53+
})
54+
}
55+
)
56+
}
6457
}
6558
}

scalafix-testkit/src/main/scala/scalafix/testkit/SemanticRuleSuite.scala

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,35 @@
11
package scalafix.testkit
22

3-
import java.io.File
43
import java.nio.charset.StandardCharsets
5-
import scala.meta.internal.io.FileIO
64
import org.scalatest.BeforeAndAfterAll
75
import org.scalatest.FunSuite
86
import org.scalatest.exceptions.TestFailedException
97
import scala.meta._
8+
import scala.meta.internal.io.FileIO
109
import scalafix.internal.reflect.ClasspathOps
1110
import scalafix.internal.reflect.RuleCompiler
1211
import scalafix.internal.testkit.AssertDiff
1312
import scalafix.internal.testkit.CommentAssertion
1413
import scalafix.internal.testkit.EndOfLineAssertExtractor
1514
import scalafix.internal.testkit.MultiLineAssertExtractor
15+
import scalafix.v0.SemanticdbIndex
1616

17-
/**
18-
* Construct a test suite for running semantic Scalafix rules.
19-
*
20-
* @param inputClassDirectory The class directory of the input sources. This directory should contain a
21-
* META-INF/semanticd sub-directory with SemanticDB files.
22-
* @param inputSourceroot The source directory of the input sources. This directory should contain Scala code to
23-
* be fixed by Scalafix.
24-
* @param outputSourceroot The source directories of the expected output sources. These directories should contain
25-
* Scala source files with the expected output after running Scalafix. When multiple directories
26-
* are provided, the first directory that contains a source files with a matching relative path
27-
* in inputSourceroot is used.
28-
*/
29-
abstract class SemanticRuleSuite(
30-
inputClassDirectory: File,
31-
inputSourceroot: File,
32-
outputSourceroot: Seq[File]
33-
) extends FunSuite
17+
/** Construct a test suite for running semantic Scalafix rules. */
18+
abstract class SemanticRuleSuite
19+
extends FunSuite
3420
with DiffAssertions
3521
with BeforeAndAfterAll { self =>
3622

37-
private val sourceroot: AbsolutePath =
38-
AbsolutePath(inputSourceroot)
39-
private val classpath: Classpath =
40-
SemanticRuleSuite.defaultClasspath(AbsolutePath(inputClassDirectory))
41-
private val expectedOutputSourceroot: Seq[AbsolutePath] =
42-
outputSourceroot.map(AbsolutePath(_))
23+
@deprecated(
24+
"Use empty constructor instead. Arguments are passed as resource 'scalafix-testkit.properties'",
25+
"0.6.0")
26+
def this(
27+
index: SemanticdbIndex,
28+
inputSourceroot: AbsolutePath,
29+
expectedOutputSourceroot: Seq[AbsolutePath]
30+
) = this()
4331

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

5544
val tokens = fixed.tokenize.get
5645
val obtained = SemanticRuleSuite.stripTestkitComments(tokens)
57-
val candidateOutputFiles = expectedOutputSourceroot.flatMap { root =>
46+
val candidateOutputFiles = props.outputSourceDirectories.flatMap { root =>
5847
val scalaSpecificFilename = scalaVersionDirectory.toList.map { path =>
5948
root.resolve(
6049
RelativePath(
@@ -100,9 +89,11 @@ abstract class SemanticRuleSuite(
10089

10190
lazy val testsToRun = {
10291
val symtab = ClasspathOps
103-
.newSymbolTable(classpath)
92+
.newSymbolTable(props.inputClasspath)
10493
.getOrElse { sys.error("Failed to load symbol table") }
105-
RuleTest.fromDirectory(sourceroot, classpath, symtab)
94+
props.inputSourceDirectories.flatMap { dir =>
95+
RuleTest.fromDirectory(dir, props.inputClasspath, symtab)
96+
}
10697
}
10798
def runAllTests(): Unit = {
10899
testsToRun.foreach(runOn)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package scalafix.testkit
2+
3+
import scala.meta.AbsolutePath
4+
import scala.meta.Classpath
5+
6+
/**
7+
* Input arguments to run scalafix testkit rules.
8+
*
9+
* @param inputClasspath
10+
* The class directory of the input sources. This directory should contain a
11+
* META-INF/semanticd sub-directory with SemanticDB files.
12+
* @param inputSourceDirectories
13+
* The source directory of the input sources. This directory should contain Scala code to
14+
* be fixed by Scalafix.
15+
* @param outputSourceDirectories
16+
* The source directories of the expected output sources. These directories should contain
17+
* Scala source files with the expected output after running Scalafix. When multiple directories
18+
* are provided, the first directory that contains a source files with a matching relative path
19+
* in inputSourceroot is used.
20+
*/
21+
final class TestkitProperties(
22+
val inputClasspath: Classpath,
23+
val inputSourceDirectories: List[AbsolutePath],
24+
val outputSourceDirectories: List[AbsolutePath]
25+
) {
26+
def inputSourceDirectory: AbsolutePath =
27+
inputSourceDirectories.head
28+
def outputSourceDirectory: AbsolutePath =
29+
outputSourceDirectories.head
30+
override def toString: String = {
31+
val map = Map(
32+
"inputSourceDirectories" -> inputSourceDirectories,
33+
"outputSourceDirectories" -> outputSourceDirectories,
34+
"inputClasspath" -> inputClasspath.syntax
35+
)
36+
pprint.PPrinter.BlackWhite.tokenize(map).mkString
37+
}
38+
}
39+
40+
object TestkitProperties {
41+
42+
/** Loads TestkitProperties from resource "scalafix-testkit.properties" of this classloader. */
43+
def loadFromResources(): TestkitProperties = {
44+
import scala.collection.JavaConverters._
45+
val props = new java.util.Properties()
46+
val path = "scalafix-testkit.properties"
47+
val in = this.getClass.getClassLoader.getResourceAsStream(path)
48+
if (in == null) {
49+
sys.error(s"Failed to load resource $path")
50+
} else {
51+
val sprops = props.asScala
52+
try props.load(in)
53+
finally in.close()
54+
new TestkitProperties(
55+
Classpath(sprops("inputClasspath")),
56+
Classpath(sprops("inputSourceDirectories")).entries,
57+
Classpath(sprops("outputSourceDirectories")).entries
58+
)
59+
}
60+
}
61+
62+
}

0 commit comments

Comments
 (0)