|
| 1 | +package dotty.tools |
| 2 | +package repl |
| 3 | + |
| 4 | +import scala.language.unsafeNulls |
| 5 | + |
| 6 | +import java.io.{File, FileOutputStream} |
| 7 | +import java.nio.file.{Files, Path} |
| 8 | +import java.util.Comparator |
| 9 | +import java.util.jar.{JarEntry, JarOutputStream, Manifest} |
| 10 | +import javax.tools.ToolProvider |
| 11 | + |
| 12 | +import org.junit.{Test, Assume} |
| 13 | +import org.junit.Assert.{assertEquals, assertTrue, assertFalse} |
| 14 | + |
| 15 | +/** Tests for loading JARs that contain classes in scala.* packages. |
| 16 | + * This tests the fix for https://github.com/scala/scala3/issues/25058 |
| 17 | + */ |
| 18 | +class ScalaPackageJarTests extends ReplTest: |
| 19 | + import ScalaPackageJarTests.* |
| 20 | + |
| 21 | + @Test def `i25058 load JAR with scala collection parallel subpackage` = |
| 22 | + // Skip if javac is not available |
| 23 | + Assume.assumeTrue("javac not available", ToolProvider.getSystemJavaCompiler != null) |
| 24 | + |
| 25 | + // Create a JAR that simulates scala-parallel-collections: |
| 26 | + // - Classes in scala.collection (e.g., ParIterable) |
| 27 | + // - Classes in scala.collection.parallel subpackage (e.g., CollectionConverters) |
| 28 | + // The bug was that when a package has BOTH classes AND subpackages, |
| 29 | + // only the classes were processed and subpackages were skipped. |
| 30 | + val jarPath = createScalaCollectionParallelJar() |
| 31 | + try |
| 32 | + initially { |
| 33 | + // First, access scala.collection to ensure it's loaded in the symbol table |
| 34 | + val state = run("scala.collection.mutable.ArrayBuffer.empty[Int]") |
| 35 | + storedOutput() // discard output |
| 36 | + state |
| 37 | + } andThen { |
| 38 | + // Load the JAR with classes in scala.collection AND scala.collection.parallel |
| 39 | + val state = run(s":jar $jarPath") |
| 40 | + val output = storedOutput() |
| 41 | + assertTrue(s"Expected success message, got: $output", output.contains("Added") && output.contains("to classpath")) |
| 42 | + state |
| 43 | + } andThen { |
| 44 | + // Import from scala.collection.parallel - this is the key test for #25058 |
| 45 | + val state = run("import scala.collection.parallel.TestParallel") |
| 46 | + val importOutput = storedOutput() |
| 47 | + // Should not have an error |
| 48 | + assertFalse(s"Import should succeed, got: $importOutput", |
| 49 | + importOutput.contains("Not Found Error") || importOutput.contains("not a member")) |
| 50 | + state |
| 51 | + } andThen { |
| 52 | + // Use the imported class |
| 53 | + run("TestParallel.getValue()") |
| 54 | + val output = storedOutput() |
| 55 | + assertTrue(s"Expected value 42, got: $output", output.contains("42")) |
| 56 | + } |
| 57 | + finally |
| 58 | + Files.deleteIfExists(Path.of(jarPath)) |
| 59 | + |
| 60 | +object ScalaPackageJarTests: |
| 61 | + |
| 62 | + /** Creates a JAR file simulating scala-parallel-collections structure: |
| 63 | + * - A class in scala.collection (TestParIterable) |
| 64 | + * - A class in scala.collection.parallel (TestParallel) |
| 65 | + * |
| 66 | + * This is critical for testing #25058: the bug only manifests when |
| 67 | + * a JAR adds BOTH classes to an existing package (scala.collection) |
| 68 | + * AND a new subpackage (scala.collection.parallel). |
| 69 | + */ |
| 70 | + def createScalaCollectionParallelJar(): String = |
| 71 | + val tempDir = Files.createTempDirectory("scala-pkg-test") |
| 72 | + |
| 73 | + // Create package directory structures |
| 74 | + val collectionDir = tempDir.resolve("scala/collection") |
| 75 | + val parallelDir = tempDir.resolve("scala/collection/parallel") |
| 76 | + Files.createDirectories(parallelDir) |
| 77 | + |
| 78 | + // Write Java source file in scala.collection (simulates ParIterable etc.) |
| 79 | + val collectionSource = collectionDir.resolve("TestParIterable.java") |
| 80 | + Files.writeString(collectionSource, |
| 81 | + """|package scala.collection; |
| 82 | + |public class TestParIterable { |
| 83 | + | public static int getCount() { return 100; } |
| 84 | + |} |
| 85 | + |""".stripMargin) |
| 86 | + |
| 87 | + // Write Java source file in scala.collection.parallel (simulates CollectionConverters etc.) |
| 88 | + val parallelSource = parallelDir.resolve("TestParallel.java") |
| 89 | + Files.writeString(parallelSource, |
| 90 | + """|package scala.collection.parallel; |
| 91 | + |public class TestParallel { |
| 92 | + | public static int getValue() { return 42; } |
| 93 | + |} |
| 94 | + |""".stripMargin) |
| 95 | + |
| 96 | + // Compile with javac |
| 97 | + val compiler = ToolProvider.getSystemJavaCompiler |
| 98 | + val fileManager = compiler.getStandardFileManager(null, null, null) |
| 99 | + val compilationUnits = fileManager.getJavaFileObjects(collectionSource.toFile, parallelSource.toFile) |
| 100 | + val task = compiler.getTask(null, fileManager, null, |
| 101 | + java.util.Arrays.asList("-d", tempDir.toString), null, compilationUnits) |
| 102 | + val success = task.call() |
| 103 | + fileManager.close() |
| 104 | + |
| 105 | + if !success then |
| 106 | + throw new RuntimeException("Failed to compile test classes") |
| 107 | + |
| 108 | + // Create JAR file |
| 109 | + val jarFile = tempDir.resolve("scala-collection-parallel.jar").toFile |
| 110 | + val manifest = new Manifest() |
| 111 | + manifest.getMainAttributes.putValue("Manifest-Version", "1.0") |
| 112 | + |
| 113 | + val jos = new JarOutputStream(new FileOutputStream(jarFile), manifest) |
| 114 | + try |
| 115 | + // Add class in scala.collection |
| 116 | + val collectionClass = collectionDir.resolve("TestParIterable.class") |
| 117 | + jos.putNextEntry(new JarEntry("scala/collection/TestParIterable.class")) |
| 118 | + jos.write(Files.readAllBytes(collectionClass)) |
| 119 | + jos.closeEntry() |
| 120 | + |
| 121 | + // Add class in scala.collection.parallel |
| 122 | + val parallelClass = parallelDir.resolve("TestParallel.class") |
| 123 | + jos.putNextEntry(new JarEntry("scala/collection/parallel/TestParallel.class")) |
| 124 | + jos.write(Files.readAllBytes(parallelClass)) |
| 125 | + jos.closeEntry() |
| 126 | + finally |
| 127 | + jos.close() |
| 128 | + |
| 129 | + // Clean up source and class files (keep only the JAR) |
| 130 | + Files.walk(tempDir) |
| 131 | + .sorted(Comparator.reverseOrder[Path]()) |
| 132 | + .filter(p => !p.equals(jarFile.toPath) && !p.equals(tempDir)) |
| 133 | + .forEach(Files.delete) |
| 134 | + |
| 135 | + jarFile.getAbsolutePath |
0 commit comments