Skip to content

Commit 865857f

Browse files
committed
Process .test.java as just .java in-memory for javac to accept the public class inside
1 parent a6c5516 commit 865857f

3 files changed

Lines changed: 123 additions & 49 deletions

File tree

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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(preprocessedDirectives.globalReqs),
60-
scopedRequirements = preprocessedDirectives.scopedReqs,
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 {

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

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,12 @@ 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
338-
* appears at `expectedPath`.
337+
/** Asserts that the only Java source produced by `testInputs` is routed to the test scope and is
338+
* reachable on disk at `expectedOnDiskPath`.
339339
*/
340340
private def expectJavaFileRoutedToTestScope(
341341
testInputs: TestInputs,
342-
expectedPath: os.RelPath
342+
expectedOnDiskPath: os.RelPath
343343
): Unit =
344344
testInputs.withInputs { (root, inputs) =>
345345
val (crossSources, _) =
@@ -367,36 +367,68 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite {
367367
).orThrow
368368

369369
expect(mainSources.paths.isEmpty)
370-
expect(testSources.paths.map(_._2) == Seq(expectedPath))
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))
371375
}
372376

373-
private val javaTestSourceContent: String =
374-
"""public class Something {
375-
| public int a = 1;
376-
|}
377-
|""".stripMargin
377+
private val javaTestPublicClassName: String = "Something"
378+
private val javaTestSourceContent: String =
379+
s"""public class $javaTestPublicClassName {
380+
| public int a = 1;
381+
|}
382+
|""".stripMargin
378383

379384
test("a .test.java file should be routed to the test scope") {
380-
val expectedPath = os.rel / "Something.test.java"
381-
val testInputs = TestInputs(expectedPath -> javaTestSourceContent)
382-
expectJavaFileRoutedToTestScope(testInputs, expectedPath)
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+
}
383415
}
384416

385417
test("a .java file under a test/ directory should be routed to the test scope") {
386-
val expectedPath = os.rel / "test" / "Something.java"
387-
val testInputs = TestInputs(Seq(expectedPath -> javaTestSourceContent), Seq("."))
388-
expectJavaFileRoutedToTestScope(testInputs, expectedPath)
418+
val onDiskPath = os.rel / "test" / s"$javaTestPublicClassName.java"
419+
val testInputs = TestInputs(Seq(onDiskPath -> javaTestSourceContent), Seq("."))
420+
expectJavaFileRoutedToTestScope(testInputs, onDiskPath)
389421
}
390422

391423
test("a .java file with //> using target.scope test should be routed to the test scope") {
392-
val expectedPath = os.rel / "Something.java"
393-
val testInputs = TestInputs(
394-
expectedPath ->
424+
val onDiskPath = os.rel / s"$javaTestPublicClassName.java"
425+
val testInputs = TestInputs(
426+
onDiskPath ->
395427
s"""//> using target.scope test
396428
|
397429
|$javaTestSourceContent""".stripMargin
398430
)
399-
expectJavaFileRoutedToTestScope(testInputs, expectedPath)
431+
expectJavaFileRoutedToTestScope(testInputs, onDiskPath)
400432
}
401433

402434
test("should skip SheBang in .sc and .scala") {

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -834,28 +834,41 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr
834834
}
835835
}
836836

837-
test("successful pure Java test with JUnit") {
838-
val expectedMessage = "Hello from JUnit"
839-
TestInputs(
840-
os.rel / "test" / "MyTests.java" ->
841-
s"""//> using test.dependencies junit:junit:4.13.2
842-
|//> using test.dependencies com.novocode:junit-interface:0.11
843-
|import org.junit.Test;
844-
|import static org.junit.Assert.assertEquals;
845-
|
846-
|public class MyTests {
847-
| @Test
848-
| public void foo() {
849-
| assertEquals(4, 2 + 2);
850-
| System.out.println("$expectedMessage");
851-
| }
852-
|}
853-
|""".stripMargin
854-
).fromRoot { root =>
855-
val res = os.proc(TestUtil.cli, "test", extraOptions, ".").call(cwd = root)
856-
expect(res.out.text().contains(expectedMessage))
857-
}
837+
for {
838+
(placementName, fileRelPath, extraDirectives, extraCliFlags) <- Seq(
839+
(".test.java suffix", os.rel / "MyTests.test.java", "", Seq.empty[String]),
840+
("test/ subdirectory", os.rel / "test" / "MyTests.java", "", Seq.empty[String]),
841+
(
842+
"//> using target.scope test",
843+
os.rel / "MyTests.java",
844+
"//> using target.scope test\n",
845+
Seq("--power")
846+
)
847+
)
848+
expectedMessage = "Hello from JUnit"
858849
}
850+
test(s"successful pure Java test with JUnit ($placementName)") {
851+
TestInputs(
852+
fileRelPath ->
853+
s"""$extraDirectives//> using test.dep junit:junit:4.13.2
854+
|//> using test.dep com.novocode:junit-interface:0.11
855+
|import org.junit.Test;
856+
|import static org.junit.Assert.assertEquals;
857+
|
858+
|public class MyTests {
859+
| @Test
860+
| public void foo() {
861+
| assertEquals(4, 2 + 2);
862+
| System.out.println("$expectedMessage");
863+
| }
864+
|}
865+
|""".stripMargin
866+
).fromRoot { root =>
867+
val res =
868+
os.proc(TestUtil.cli, extraCliFlags, "test", extraOptions, ".").call(cwd = root)
869+
expect(res.out.text().contains(expectedMessage))
870+
}
871+
}
859872

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

0 commit comments

Comments
 (0)