Skip to content

Commit 6678f5b

Browse files
authored
Merge pull request #4261 from Gedochao/feature/test-dot-java
Support for `.test.java`
2 parents 9167f53 + 865857f commit 6678f5b

9 files changed

Lines changed: 193 additions & 48 deletions

File tree

modules/build/src/main/scala/scala/build/CrossSources.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ final case class CrossSources(
135135
}
136136

137137
object CrossSources {
138+
private val testScopeFileSuffixes: Set[String] = Set(".test.scala", ".test.java")
139+
140+
private def hasTestScopeSuffix(fileName: String): Boolean =
141+
testScopeFileSuffixes.exists(fileName.endsWith)
138142

139143
private def withinTestSubDirectory(p: ScopePath, inputs: Inputs): Boolean =
140144
p.root.exists { path =>
@@ -248,12 +252,14 @@ object CrossSources {
248252
.flatMap(_.valueFor(path).toSeq)
249253
.foldLeft(BuildRequirements())(_.orElse(_))
250254

251-
// Scala CLI treats all `.test.scala` files tests as well as
252-
// files from within `test` subdirectory from provided input directories
253-
// If file has `using target <scope>` directive this take precendeces.
255+
// Scala CLI treats all source files whose names end with one of the
256+
// testScopeFileSuffixes (e.g. `.test.scala`, `.test.java`) as tests, as well as
257+
// files from within `test` subdirectory from provided input directories.
258+
// If file has `using target <scope>` directive this takes precedence.
254259
if (
255260
fromDirectives.scope.isEmpty &&
256-
(path.subPath.last.endsWith(".test.scala") || withinTestSubDirectory(path, allInputs))
261+
(CrossSources.hasTestScopeSuffix(path.subPath.last) ||
262+
withinTestSubDirectory(path, allInputs))
257263
)
258264
fromDirectives.copy(scope = Some(BuildRequirements.ScopeRequirement(Scope.Test)))
259265
else fromDirectives

modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import scala.build.Logger
1010
import scala.build.errors.BuildException
1111
import scala.build.input.{JavaFile, ScalaCliInvokeData, SingleElement, VirtualJavaFile}
1212
import scala.build.internal.JavaParserProxyMaker
13-
import scala.build.options.{BuildRequirements, SuppressWarningOptions}
13+
import scala.build.options.SuppressWarningOptions
1414
import scala.build.preprocessing.directives.PreprocessedDirectives
1515

1616
/** Java source preprocessor.
@@ -52,15 +52,44 @@ final case class JavaPreprocessor(
5252
)
5353
.preprocess(content)
5454
}
55-
Seq(PreprocessedSource.OnDisk(
56-
path = j.path,
57-
options = Some(preprocessedDirectives.globalUsings),
58-
optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs,
59-
requirements = Some(BuildRequirements()),
60-
scopedRequirements = Nil,
61-
mainClassOpt = None,
62-
directivesPositions = preprocessedDirectives.directivesPositions
63-
))
55+
// Java's source-file rule requires that a public class be declared in a file named
56+
// exactly `<ClassName>.java`, so emit `*.test.java` as an in-memory source whose
57+
// generated path strips the `.test` infix. This lets users follow the same naming
58+
// convention as `.test.scala` for routing while still compiling under `javac`.
59+
val testJavaSuffix = ".test.java"
60+
val preprocessedJavaSources =
61+
if j.subPath.last.endsWith(testJavaSuffix) then {
62+
val strippedFileName =
63+
s"${j.subPath.last.stripSuffix(testJavaSuffix)}.java"
64+
val strippedSubPath: os.SubPath =
65+
if j.subPath.segments.sizeIs > 1 then
66+
os.sub / j.subPath.segments.init / strippedFileName
67+
else os.sub / strippedFileName
68+
Seq(PreprocessedSource.InMemory(
69+
originalPath = Right((j.subPath, j.path)),
70+
relPath = os.rel / strippedSubPath,
71+
content = content.getBytes(StandardCharsets.UTF_8),
72+
wrapperParamsOpt = None,
73+
options = Some(preprocessedDirectives.globalUsings),
74+
optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs,
75+
requirements = Some(preprocessedDirectives.globalReqs),
76+
scopedRequirements = preprocessedDirectives.scopedReqs,
77+
mainClassOpt = None,
78+
scopePath = scopePath,
79+
directivesPositions = preprocessedDirectives.directivesPositions
80+
))
81+
}
82+
else
83+
Seq(PreprocessedSource.OnDisk(
84+
path = j.path,
85+
options = Some(preprocessedDirectives.globalUsings),
86+
optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs,
87+
requirements = Some(preprocessedDirectives.globalReqs),
88+
scopedRequirements = preprocessedDirectives.scopedReqs,
89+
mainClassOpt = None,
90+
directivesPositions = preprocessedDirectives.directivesPositions
91+
))
92+
preprocessedJavaSources
6493
})
6594
case v: VirtualJavaFile =>
6695
val res = either {
@@ -102,8 +131,8 @@ final case class JavaPreprocessor(
102131
wrapperParamsOpt = None,
103132
options = Some(preprocessedDirectives.globalUsings),
104133
optionsWithTargetRequirements = preprocessedDirectives.usingsWithReqs,
105-
requirements = Some(BuildRequirements()),
106-
scopedRequirements = Nil,
134+
requirements = Some(preprocessedDirectives.globalReqs),
135+
scopedRequirements = preprocessedDirectives.scopedReqs,
107136
mainClassOpt = None,
108137
scopePath = v.scopePath,
109138
directivesPositions = preprocessedDirectives.directivesPositions

modules/build/src/test/scala/scala/build/tests/SourcesTests.scala

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,103 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite {
334334
}
335335
}
336336

337+
/** Asserts that the only Java source produced by `testInputs` is routed to the test scope and is
338+
* reachable on disk at `expectedOnDiskPath`.
339+
*/
340+
private def expectJavaFileRoutedToTestScope(
341+
testInputs: TestInputs,
342+
expectedOnDiskPath: os.RelPath
343+
): Unit =
344+
testInputs.withInputs { (root, inputs) =>
345+
val (crossSources, _) =
346+
CrossSources.forInputs(
347+
inputs,
348+
preprocessors,
349+
TestLogger(),
350+
SuppressWarningOptions()
351+
).orThrow
352+
353+
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
354+
val mainSources =
355+
scopedSources.sources(
356+
Scope.Main,
357+
crossSources.sharedOptions(BuildOptions()),
358+
root,
359+
TestLogger()
360+
).orThrow
361+
val testSources =
362+
scopedSources.sources(
363+
Scope.Test,
364+
crossSources.sharedOptions(BuildOptions()),
365+
root,
366+
TestLogger()
367+
).orThrow
368+
369+
expect(mainSources.paths.isEmpty)
370+
expect(mainSources.inMemory.isEmpty)
371+
val onDiskPaths = testSources.paths.map(_._2)
372+
val inMemoryOriginalPaths =
373+
testSources.inMemory.flatMap(_.originalPath.toOption.map(_._1: os.SubPath))
374+
expect((onDiskPaths ++ inMemoryOriginalPaths.map(os.rel / _)) == Seq(expectedOnDiskPath))
375+
}
376+
377+
private val javaTestPublicClassName: String = "Something"
378+
private val javaTestSourceContent: String =
379+
s"""public class $javaTestPublicClassName {
380+
| public int a = 1;
381+
|}
382+
|""".stripMargin
383+
384+
test("a .test.java file should be routed to the test scope") {
385+
val onDiskPath = os.rel / s"$javaTestPublicClassName.test.java"
386+
val testInputs = TestInputs(onDiskPath -> javaTestSourceContent)
387+
expectJavaFileRoutedToTestScope(testInputs, onDiskPath)
388+
}
389+
390+
test("a .test.java file's generated path strips the .test infix for javac") {
391+
val onDiskPath = os.rel / s"$javaTestPublicClassName.test.java"
392+
TestInputs(onDiskPath -> javaTestSourceContent).withInputs { (root, inputs) =>
393+
val (crossSources, _) =
394+
CrossSources.forInputs(
395+
inputs,
396+
preprocessors,
397+
TestLogger(),
398+
SuppressWarningOptions()
399+
).orThrow
400+
401+
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
402+
val testSources =
403+
scopedSources.sources(
404+
Scope.Test,
405+
crossSources.sharedOptions(BuildOptions()),
406+
root,
407+
TestLogger()
408+
).orThrow
409+
410+
val expectedGeneratedRelPath = os.rel / s"$javaTestPublicClassName.java"
411+
expect(testSources.paths.isEmpty)
412+
expect(testSources.inMemory.length == 1)
413+
expect(testSources.inMemory.head.generatedRelPath == expectedGeneratedRelPath)
414+
}
415+
}
416+
417+
test("a .java file under a test/ directory should be routed to the test scope") {
418+
val onDiskPath = os.rel / "test" / s"$javaTestPublicClassName.java"
419+
val testInputs = TestInputs(Seq(onDiskPath -> javaTestSourceContent), Seq("."))
420+
expectJavaFileRoutedToTestScope(testInputs, onDiskPath)
421+
}
422+
423+
test("a .java file with //> using target.scope test should be routed to the test scope") {
424+
val onDiskPath = os.rel / s"$javaTestPublicClassName.java"
425+
val testInputs = TestInputs(
426+
onDiskPath ->
427+
s"""//> using target.scope test
428+
|
429+
|$javaTestSourceContent""".stripMargin
430+
)
431+
expectJavaFileRoutedToTestScope(testInputs, onDiskPath)
432+
}
433+
337434
test("should skip SheBang in .sc and .scala") {
338435
val testInputs = TestInputs(
339436
os.rel / "something1.sc" ->

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ object TestOptions {
4343
implicit lazy val help: Help[TestOptions] = Help.derive
4444

4545
val cmdName = "test"
46-
private val helpHeader = "Compile and test Scala code."
46+
private val helpHeader = "Compile and test Scala (or Java) code."
4747
val helpMessage: String = HelpMessages.shortHelpMessage(cmdName, helpHeader)
4848
val detailedHelpMessage: String =
4949
s"""$helpHeader
5050
|
5151
|Test sources are compiled separately (after the 'main' sources), and may use different dependencies, compiler options, and other configurations.
5252
|A source file is treated as a test source if:
53-
| - the file name ends with `.test.scala`
53+
| - the file name ends with `.test.scala` or `.test.java`
5454
| - the file comes from a directory that is provided as input, and the relative path from that file to its original directory contains a `test` directory
5555
| - it contains the `//> using target.scope test` directive (Experimental)
5656
|

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -855,28 +855,41 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr
855855
}
856856
}
857857

858-
test("successful pure Java test with JUnit") {
859-
val expectedMessage = "Hello from JUnit"
860-
TestInputs(
861-
os.rel / "test" / "MyTests.java" ->
862-
s"""//> using test.dependencies junit:junit:4.13.2
863-
|//> using test.dependencies com.novocode:junit-interface:0.11
864-
|import org.junit.Test;
865-
|import static org.junit.Assert.assertEquals;
866-
|
867-
|public class MyTests {
868-
| @Test
869-
| public void foo() {
870-
| assertEquals(4, 2 + 2);
871-
| System.out.println("$expectedMessage");
872-
| }
873-
|}
874-
|""".stripMargin
875-
).fromRoot { root =>
876-
val res = os.proc(TestUtil.cli, "test", extraOptions, ".").call(cwd = root)
877-
expect(res.out.text().contains(expectedMessage))
878-
}
858+
for {
859+
(placementName, fileRelPath, extraDirectives, extraCliFlags) <- Seq(
860+
(".test.java suffix", os.rel / "MyTests.test.java", "", Seq.empty[String]),
861+
("test/ subdirectory", os.rel / "test" / "MyTests.java", "", Seq.empty[String]),
862+
(
863+
"//> using target.scope test",
864+
os.rel / "MyTests.java",
865+
"//> using target.scope test\n",
866+
Seq("--power")
867+
)
868+
)
869+
expectedMessage = "Hello from JUnit"
879870
}
871+
test(s"successful pure Java test with JUnit ($placementName)") {
872+
TestInputs(
873+
fileRelPath ->
874+
s"""$extraDirectives//> using test.dep junit:junit:4.13.2
875+
|//> using test.dep com.novocode:junit-interface:0.11
876+
|import org.junit.Test;
877+
|import static org.junit.Assert.assertEquals;
878+
|
879+
|public class MyTests {
880+
| @Test
881+
| public void foo() {
882+
| assertEquals(4, 2 + 2);
883+
| System.out.println("$expectedMessage");
884+
| }
885+
|}
886+
|""".stripMargin
887+
).fromRoot { root =>
888+
val res =
889+
os.proc(TestUtil.cli, extraCliFlags, "test", extraOptions, ".").call(cwd = root)
890+
expect(res.out.text().contains(expectedMessage))
891+
}
892+
}
880893

881894
test(s"zio-test warning when zio-test-sbt was not passed") {
882895
TestUtil.retryOnCi() {

website/docs/commands/test.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ so [using directives](../guides/introduction/using-directives.md) can be used to
1616

1717
A source file is treated as test source if:
1818

19-
- the file name ends with `.test.scala`, or
19+
- the file name ends with `.test.scala` or `.test.java`, or
2020
- the file comes from a directory that is provided as input, and the relative path from that file to its original
2121
directory contains a `test` directory, or
2222
- it contains the `//> using target.scope test` directive
@@ -55,7 +55,7 @@ Given that directory structure, let's analyze what file(s) will be treated as te
5555

5656
`scala-cli example` results in the following files being treated as test sources:
5757

58-
- `a.test.scala`, since it ends with `.test.scala`
58+
- `a.test.scala`, since it ends with `.test.scala` (the same applies to `.test.java` for Java sources)
5959
- `src/test/scala/b.scala`, since the path to that directory contains a directory named `test`
6060

6161
Note that `e.scala` is not treated as a test source since it lacks a parent directory in its relative path that is
@@ -71,7 +71,7 @@ contain `test` (the fact that the directory provided as input is named `test` do
7171
Directives take precedence over file or path names, so `using target.scope main` can be used to force `test/a.scala`
7272
or `a.test.scala` to not be treated as tests.
7373

74-
As a rule of thumb, we recommend naming all of your test files with the `.test.scala` suffix.
74+
As a rule of thumb, we recommend naming all of your test files with the `.test.scala` (or `.test.java` for Java sources) suffix.
7575

7676
## Test directives
7777

website/docs/reference/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,11 @@ Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [c
391391

392392
## test
393393

394-
Compile and test Scala code.
394+
Compile and test Scala (or Java) code.
395395

396396
Test sources are compiled separately (after the 'main' sources), and may use different dependencies, compiler options, and other configurations.
397397
A source file is treated as a test source if:
398-
- the file name ends with `.test.scala`
398+
- the file name ends with `.test.scala` or `.test.java`
399399
- the file comes from a directory that is provided as input, and the relative path from that file to its original directory contains a `test` directory
400400
- it contains the `//> using target.scope test` directive (Experimental)
401401

website/docs/reference/scala-command/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [c
192192

193193
### test
194194

195-
Compile and test Scala code.
195+
Compile and test Scala (or Java) code.
196196

197197
Test sources are compiled separately (after the 'main' sources), and may use different dependencies, compiler options, and other configurations.
198198
A source file is treated as a test source if:
199-
- the file name ends with `.test.scala`
199+
- the file name ends with `.test.scala` or `.test.java`
200200
- the file comes from a directory that is provided as input, and the relative path from that file to its original directory contains a `test` directory
201201
- it contains the `//> using target.scope test` directive (Experimental)
202202

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4106,11 +4106,11 @@ Aliases: `--fmt-version`
41064106
## `test` command
41074107
**SHOULD have for Scala Runner specification.**
41084108

4109-
Compile and test Scala code.
4109+
Compile and test Scala (or Java) code.
41104110

41114111
Test sources are compiled separately (after the 'main' sources), and may use different dependencies, compiler options, and other configurations.
41124112
A source file is treated as a test source if:
4113-
- the file name ends with `.test.scala`
4113+
- the file name ends with `.test.scala` or `.test.java`
41144114
- the file comes from a directory that is provided as input, and the relative path from that file to its original directory contains a `test` directory
41154115
- it contains the `//> using target.scope test` directive (Experimental)
41164116

0 commit comments

Comments
 (0)