Skip to content

Commit fcaa3ab

Browse files
committed
Add tab completion to the partest command
We can complete partest options (I've excluded some that aren't relevant in SBT), as well as test file names. If `--srcpath scaladoc` is included, completion of test paths will be based on `test/scaladoc` rather than the default `test/files`. Note that the `--srcpath` option is currently broken via scala partest interface, this change to scala-partest is needed to make it work: scala/scala-partest#49 I've also hijacked the `--grep` option with logic in the SBT command itself, rather than passing this to `partest`. Just like `./bin/partest-ack`, this looks for either test file names or regex matches within the contents of test, check, or flag files. I tried for some time to make the tab completion of thousands of filenames more user friendly, but wasn't able to get something working. Ideally, it should only suggest to `test/files/{pos, neg, ...}` on the first <TAB>, and then offer files on another TAB. Files should also be offered if a full directory has been entered. Hopefully a SBT parser guru will step in and add some polish here.
1 parent b51fe3d commit fcaa3ab

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

build.sbt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,4 +836,8 @@ addCommandAlias("scalac", "compiler/compile:runMain scala.tools.nsc
836836
addCommandAlias("scala", "repl-jline-embedded/compile:runMain scala.tools.nsc.MainGenericRunner -usejavacp")
837837
addCommandAlias("scaladoc", "scaladoc/compile:runMain scala.tools.nsc.ScalaDoc -usejavacp")
838838
addCommandAlias("scalap", "scalap/compile:runMain scala.tools.scalap.Main -usejavacp")
839-
addCommandAlias("partest", "test/it:testOnly --")
839+
840+
// Add tab completion to partest
841+
commands += Command("partest")(_ => PartestUtil.partestParser((baseDirectory in ThisBuild).value, (baseDirectory in ThisBuild).value / "test")) { (state, parsed) =>
842+
("test/it:testOnly -- " + parsed):: state
843+
}

project/PartestUtil.scala

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import sbt._
2+
import sbt.complete._, Parser._, Parsers._
3+
4+
object PartestUtil {
5+
private case class TestFiles(srcPath: String, globalBase: File, testBase: File) {
6+
private val testCaseDir = new SimpleFileFilter(f => f.isDirectory && f.listFiles.nonEmpty && !(f.getParentFile / (f.name + ".res")).exists)
7+
private def testCaseFinder = (testBase / srcPath).*(AllPassFilter).*(GlobFilter("*.scala") | GlobFilter("*.java") | GlobFilter("*.res") || testCaseDir)
8+
private val basePaths = allTestCases.map(_._2.split('/').take(3).mkString("/") + "/").distinct
9+
10+
def allTestCases = testCaseFinder.pair(relativeTo(globalBase))
11+
def basePathExamples = new FixedSetExamples(basePaths)
12+
}
13+
/** A parser for the custom `partest` command */
14+
def partestParser(globalBase: File, testBase: File): Parser[String] = {
15+
val knownUnaryOptions = List(
16+
"--pos", "--neg", "--run", "--jvm", "--res", "--ant", "--scalap", "--specialized",
17+
"--scalacheck", "--instrumented", "--presentation", "--failed", "--update-check",
18+
"--show-diff", "--verbose", "--terse", "--debug", "--version", "--self-test", "--help")
19+
val srcPathOption = "--srcpath"
20+
val grepOption = "--grep"
21+
22+
// HACK: if we parse `--srpath scaladoc`, we overwrite this var. The parser for test file paths
23+
// then lazily creates the examples based on the current value.
24+
// TODO is there a cleaner way to do this with SBT's parser infrastructure?
25+
var srcPath = "files"
26+
var _testFiles: TestFiles = null
27+
def testFiles = {
28+
if (_testFiles == null || _testFiles.srcPath != srcPath) _testFiles = new TestFiles(srcPath, globalBase, testBase)
29+
_testFiles
30+
}
31+
val TestPathParser = token(NotSpace & not('-' ~> any.*, "File name cannot start with '-'."), TokenCompletions.fixed({
32+
(seen, level) =>
33+
val suggestions = testFiles.allTestCases.map(_._2).filter(_.startsWith(seen)).map(_.stripPrefix(seen))
34+
Completions.strict(suggestions.map(s => Completion.token(seen, s)).toSet)
35+
}))
36+
37+
// allow `--grep "is unchecked" | --grep *t123*, in the spirit of ./bin/partest-ack
38+
// superset of the --grep built into partest itself.
39+
val Grep = {
40+
def expandGrep(x: String): Seq[String] = {
41+
val matchingFileContent = try {
42+
val Pattern = ("(?i)" + x).r
43+
testFiles.allTestCases.filter {
44+
case (testFile, testPath) =>
45+
val assocFiles = List(".check", ".flags").map(testFile.getParentFile / _)
46+
val sourceFiles = if (testFile.isFile) List(testFile) else testFile.**(AllPassFilter).get.toList
47+
val allFiles = testFile :: assocFiles ::: sourceFiles
48+
allFiles.exists { f => f.exists && f.isFile && Pattern.findFirstIn(IO.read(f)).isDefined }
49+
}
50+
} catch {
51+
case _: Throwable => Nil
52+
}
53+
val matchingFileName = try {
54+
val filter = GlobFilter("*" + x + "*")
55+
testFiles.allTestCases.filter(x => filter.accept(x._1.name))
56+
} catch {
57+
case t: Throwable => Nil
58+
}
59+
(matchingFileContent ++ matchingFileName).map(_._2).distinct.sorted
60+
}
61+
62+
val completion = Completions.strict(Set("<filename glob>", "<regex> (for source, flags or checkfile contents)").map(s => Completion.displayOnly(s)))
63+
val tokenCompletion = TokenCompletions.fixed((seen, level) => completion)
64+
65+
val globOrPattern = StringBasic.map(expandGrep).flatMap {
66+
case Seq() => failure("no tests match pattern / glob")
67+
case x => success(x.mkString(" "))
68+
}
69+
((token(grepOption <~ Space)) ~> token(globOrPattern, tokenCompletion))
70+
}
71+
72+
val SrcPath = ((token(srcPathOption) <~ Space) ~ token(StringBasic.examples(Set("files", "pending", "scaladoc")))) map {
73+
case opt ~ path =>
74+
srcPath = path
75+
opt + " " + path
76+
}
77+
// allow the user to additional abitrary arguments, in case our parser is incomplete.
78+
val WhatEver = token(NotSpace, _ => true).filter(x => !knownUnaryOptions.contains(x) && !Set(grepOption, srcPathOption).contains(x), x => x)
79+
val P = oneOf(knownUnaryOptions.map(x => token(x))) | SrcPath | TestPathParser | Grep //| WhatEver
80+
(Space ~> repsep(P, oneOrMore(Space))).map(_.mkString(" "))
81+
}
82+
}

0 commit comments

Comments
 (0)