Skip to content

Commit bdfb303

Browse files
authored
Merge pull request #1604 from lwronski/test-only
Add `testOnly` to filter running test suites
2 parents fb43f8c + c9ec90b commit bdfb303

File tree

10 files changed

+223
-11
lines changed

10 files changed

+223
-11
lines changed

modules/cli/src/main/scala/scala/cli/commands/test/Test.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ object Test extends ScalaCommand[TestOptions] {
3838
sharedJava.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine)
3939
),
4040
testOptions = baseOptions.testOptions.copy(
41-
frameworkOpt = testFramework.map(_.trim).filter(_.nonEmpty)
41+
frameworkOpt = testFramework.map(_.trim).filter(_.nonEmpty),
42+
testOnly = testOnly.map(_.trim).filter(_.nonEmpty)
4243
),
4344
internalDependencies = baseOptions.internalDependencies.copy(
4445
addTestRunnerDependencyOpt = Some(true)
@@ -228,11 +229,13 @@ object Test extends ScalaCommand[TestOptions] {
228229
val testFrameworkOpt0 = testFrameworkOpt.orElse {
229230
findTestFramework(classPath.map(_.toNIO), logger)
230231
}
232+
val testOnly = build.options.testOptions.testOnly
231233

232234
val extraArgs =
233235
(if (requireTests) Seq("--require-tests") else Nil) ++
234236
build.options.internal.verbosity.map(v => s"--verbosity=$v") ++
235237
testFrameworkOpt0.map(fw => s"--test-framework=$fw").toSeq ++
238+
testOnly.map(to => s"--test-only=$to").toSeq ++
236239
Seq("--") ++ args
237240

238241
Runner.runJvm(

modules/cli/src/main/scala/scala/cli/commands/test/TestOptions.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ final case class TestOptions(
3333
@Group("Test")
3434
@Tag(tags.should)
3535
@HelpMessage("Fail if no test suites were run")
36-
requireTests: Boolean = false
36+
requireTests: Boolean = false,
37+
@Group("Test")
38+
@Tag(tags.should)
39+
@HelpMessage("Specify a glob pattern to filter the tests suite to be run.")
40+
testOnly: Option[String] = None
41+
3742
) extends HasSharedOptions
3843
// format: on
3944

modules/docs-tests/src/main/scala/sclicheck/sclicheck.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def mkBashScript(content: Seq[String]) =
145145
|${content.mkString("\n")}
146146
|""".stripMargin
147147

148+
def removeAnsiColors(str: String) = str.replaceAll("\\e\\[[0-9]+m", "")
149+
148150
private lazy val baseTmpDir = {
149151
val random = new SecureRandom
150152
val dirName = s"run-${math.abs(random.nextInt.toLong)}"
@@ -257,7 +259,7 @@ def checkFile(file: os.Path, options: Options): Unit =
257259

258260
case Commands.Check(patterns, regex, _) =>
259261
check(lastOutput != null, "No output stored from previous commands")
260-
val lines = lastOutput.linesIterator.toList
262+
val lines = lastOutput.linesIterator.toList.map(removeAnsiColors)
261263

262264
if regex then
263265
patterns.foreach { pattern =>

modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,85 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String])
258258
}
259259
}
260260

261+
test("run only one test from munit") {
262+
val inputs: TestInputs = TestInputs(
263+
os.rel / "MyTests.scala" ->
264+
"""//> using lib "org.scalameta::munit::0.7.29"
265+
|package test
266+
|
267+
|class MyTests extends munit.FunSuite {
268+
| test("foo") {
269+
| assert(2 + 2 == 5, "foo")
270+
| }
271+
| test("bar") {
272+
| assert(2 + 3 == 5)
273+
| println("Hello from bar")
274+
| }
275+
|}
276+
|""".stripMargin
277+
)
278+
inputs.fromRoot { root =>
279+
val res =
280+
os.proc(
281+
TestUtil.cli,
282+
"test",
283+
".",
284+
extraOptions,
285+
"--test-only",
286+
"test.MyTests",
287+
"--",
288+
"*bar*"
289+
).call(cwd = root)
290+
val output = res.out.text()
291+
expect(res.exitCode == 0)
292+
expect(output.contains("Hello from bar"))
293+
}
294+
}
295+
test("run only one test from utest") {
296+
val inputs: TestInputs = TestInputs(
297+
os.rel / "FooTests.scala" ->
298+
"""//> using lib "com.lihaoyi::utest::0.7.10"
299+
|package tests.foo
300+
|import utest._
301+
|
302+
|object FooTests extends TestSuite {
303+
| val tests = Tests {
304+
| test("foo") {
305+
| assert(2 + 2 == 5)
306+
| }
307+
| }
308+
|}
309+
|""".stripMargin,
310+
os.rel / "BarTests.scala" ->
311+
"""//> using lib "com.lihaoyi::utest::0.7.10"
312+
|package tests.bar
313+
|import utest._
314+
|
315+
|object BarTests extends TestSuite {
316+
| val tests = Tests {
317+
| test("bar") {
318+
| assert(2 + 2 == 4)
319+
| println("Hello from bar")
320+
| }
321+
| }
322+
|}
323+
|""".stripMargin
324+
)
325+
inputs.fromRoot { root =>
326+
val res =
327+
os.proc(
328+
TestUtil.cli,
329+
"test",
330+
".",
331+
extraOptions,
332+
"--test-only",
333+
"tests.b*"
334+
).call(cwd = root)
335+
val output = res.out.text()
336+
expect(res.exitCode == 0)
337+
expect(output.contains("Hello from bar"))
338+
}
339+
}
261340
def successfulNativeTest(): Unit =
262341
successfulTestInputs.fromRoot { root =>
263342
val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--native")

modules/options/src/main/scala/scala/build/options/TestOptions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package scala.build.options
22

33
final case class TestOptions(
4-
frameworkOpt: Option[String] = None
4+
frameworkOpt: Option[String] = None,
5+
testOnly: Option[String] = None
56
)
67

78
object TestOptions {

modules/test-runner/src/main/scala/scala/build/testrunner/DynamicTestRunner.scala

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.lang.annotation.Annotation
66
import java.lang.reflect.Modifier
77
import java.nio.file.{Files, Path}
88
import java.util.ServiceLoader
9+
import java.util.regex.Pattern
910

1011
import scala.annotation.tailrec
1112
import scala.jdk.CollectionConverters._
@@ -148,27 +149,55 @@ object DynamicTestRunner {
148149
.headOption
149150
}
150151

152+
/** Based on junit-interface [GlobFilter.
153+
* compileGlobPattern](https://github.com/sbt/junit-interface/blob/f8c6372ed01ce86f15393b890323d96afbe6d594/src/main/java/com/novocode/junit/GlobFilter.java#L37)
154+
*
155+
* @return
156+
* Pattern allows to regex input which contains only *, for example `*foo*` match to
157+
* `MyTests.foo`
158+
*/
159+
private def globPattern(expr: String): Pattern = {
160+
val a = expr.split("\\*", -1)
161+
val b = new StringBuilder()
162+
for (i <- 0 until a.length) {
163+
if (i != 0) b.append(".*")
164+
if (a(i).nonEmpty) b.append(Pattern.quote(a(i).replaceAll("\n", "\\n")))
165+
}
166+
Pattern.compile(b.toString)
167+
}
168+
151169
def main(args: Array[String]): Unit = {
152170

153-
val (testFrameworkOpt, requireTests, verbosity, args0) = {
171+
val (testFrameworkOpt, requireTests, verbosity, testOnly, args0) = {
154172
@tailrec
155173
def parse(
156174
testFrameworkOpt: Option[String],
157175
reverseTestArgs: List[String],
158176
requireTests: Boolean,
159177
verbosity: Int,
178+
testOnly: Option[String],
160179
args: List[String]
161-
): (Option[String], Boolean, Int, List[String]) =
180+
): (Option[String], Boolean, Int, Option[String], List[String]) =
162181
args match {
163-
case Nil => (testFrameworkOpt, requireTests, verbosity, reverseTestArgs.reverse)
182+
case Nil => (testFrameworkOpt, requireTests, verbosity, testOnly, reverseTestArgs.reverse)
164183
case "--" :: t =>
165-
(testFrameworkOpt, requireTests, verbosity, reverseTestArgs.reverse ::: t)
184+
(testFrameworkOpt, requireTests, verbosity, testOnly, reverseTestArgs.reverse ::: t)
166185
case h :: t if h.startsWith("--test-framework=") =>
167186
parse(
168187
Some(h.stripPrefix("--test-framework=")),
169188
reverseTestArgs,
170189
requireTests,
171190
verbosity,
191+
testOnly,
192+
t
193+
)
194+
case h :: t if h.startsWith("--test-only=") =>
195+
parse(
196+
testFrameworkOpt,
197+
reverseTestArgs,
198+
requireTests,
199+
verbosity,
200+
Some(h.stripPrefix("--test-only=")),
172201
t
173202
)
174203
case h :: t if h.startsWith("--verbosity=") =>
@@ -177,14 +206,16 @@ object DynamicTestRunner {
177206
reverseTestArgs,
178207
requireTests,
179208
h.stripPrefix("--verbosity=").toInt,
209+
testOnly,
180210
t
181211
)
182212
case "--require-tests" :: t =>
183-
parse(testFrameworkOpt, reverseTestArgs, true, verbosity, t)
184-
case h :: t => parse(testFrameworkOpt, h :: reverseTestArgs, requireTests, verbosity, t)
213+
parse(testFrameworkOpt, reverseTestArgs, true, verbosity, testOnly, t)
214+
case h :: t =>
215+
parse(testFrameworkOpt, h :: reverseTestArgs, requireTests, verbosity, testOnly, t)
185216
}
186217

187-
parse(None, Nil, false, 0, args.toList)
218+
parse(None, Nil, false, 0, None, args.toList)
188219
}
189220

190221
val classLoader = Thread.currentThread().getContextClassLoader
@@ -214,6 +245,12 @@ object DynamicTestRunner {
214245
.iterator
215246
}
216247
val taskDefs = clsFingerprints
248+
.filter {
249+
case (cls, _) =>
250+
testOnly.forall(pattern =>
251+
globPattern(pattern).matcher(cls.getName.stripSuffix("$")).matches()
252+
)
253+
}
217254
.map {
218255
case (cls, fp) =>
219256
new TaskDef(cls.getName.stripSuffix("$"), fp, false, Array(new SuiteSelector))

website/docs/commands/test.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,77 @@ MyTests
102102
foo
103103
-->
104104

105+
## Filter test suite
106+
107+
Passing the `--test-only` option to the `test` sub-command filters the test suites to be run:
108+
109+
```scala title=BarTests.scala
110+
//> using lib "org.scalameta::munit::0.7.29"
111+
package tests.only
112+
113+
class BarTests extends munit.FunSuite {
114+
test("bar") {
115+
assert(2 + 3 == 5)
116+
}
117+
}
118+
```
119+
```scala title=HelloTests.scala
120+
package tests
121+
122+
class HelloTests extends munit.FunSuite {
123+
test("hello") {
124+
assert(2 + 2 == 4)
125+
}
126+
}
127+
```
128+
129+
```bash
130+
scala-cli test . --test-only 'tests.only*'
131+
# tests.only.BarTests:
132+
# + bar 0.045s
133+
```
134+
135+
<!-- Expected:
136+
tests.only.BarTests:
137+
+ bar
138+
-->
139+
140+
## Filter test case
141+
142+
### Munit
143+
144+
To run a specific test case inside the unit test suite pass `*exact-test-name*` as an argument to scala-cli:
145+
146+
```scala title=BarTests.scala
147+
//> using lib "org.scalameta::munit::0.7.29"
148+
package tests.only
149+
150+
class Tests extends munit.FunSuite {
151+
test("bar") {
152+
assert(2 + 2 == 5)
153+
}
154+
test("foo") {
155+
assert(2 + 3 == 5)
156+
}
157+
test("foo-again") {
158+
assert(2 + 3 == 5)
159+
}
160+
}
161+
```
162+
```bash
163+
scala-cli test . --test-only 'tests.only*' -- '*foo*'
164+
# tests.only.Tests:
165+
# + foo 0.045s
166+
# + foo-again 0.001s
167+
```
168+
169+
<!-- Expected:
170+
tests.only.Tests:
171+
+ foo
172+
+ foo-again
173+
-->
174+
175+
105176
## Test arguments
106177

107178
You can pass test arguments to your test framework by passing them after `--`:

website/docs/reference/cli-options.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,10 @@ Name of the test framework's runner class to use while running tests
14591459

14601460
Fail if no test suites were run
14611461

1462+
### `--test-only`
1463+
1464+
Specify a glob pattern to filter the tests suite to be run.
1465+
14621466
## Uninstall options
14631467

14641468
Available in commands:

website/docs/reference/scala-command/cli-options.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,12 @@ Name of the test framework's runner class to use while running tests
10341034

10351035
Fail if no test suites were run
10361036

1037+
### `--test-only`
1038+
1039+
`SHOULD have` per Scala Runner specification
1040+
1041+
Specify a glob pattern to filter the tests suite to be run.
1042+
10371043
## Uninstall options
10381044

10391045
Available in commands:

website/docs/reference/scala-command/runner-specification.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3448,6 +3448,10 @@ Name of the test framework's runner class to use while running tests
34483448
34493449
Fail if no test suites were run
34503450
3451+
**--test-only**
3452+
3453+
Specify a glob pattern to filter the tests suite to be run.
3454+
34513455
<details><summary>
34523456
34533457
### Implementantation specific options

0 commit comments

Comments
 (0)