diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/StreamApiTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/StreamApiTest.java index 447888c72b..e9b4443b4e 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/StreamApiTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/StreamApiTest.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.List; import java.util.function.BinaryOperator; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -59,6 +60,56 @@ public boolean test(String value) { return value.length() == 1; } })); + + List distinctSorted = Stream.of("delta", "beta", "alpha", "beta", "gamma", "alpha") + .distinct() + .sorted() + .skip(1) + .limit(2) + .collect(Collectors.toList()); + assertEqual(Arrays.asList(new String[]{"beta", "delta"}), distinctSorted, "distinct/sorted/skip/limit pipeline failed"); + + Object[] arrayResult = Stream.of("x", "y", "z").skip(2).toArray(); + assertEqual(1, arrayResult.length, "Unexpected toArray length after skip"); + assertEqual("z", arrayResult[0], "Unexpected toArray value after skip"); + + long emptyCount = Stream.empty().count(); + assertEqual(0L, emptyCount, "Stream.empty() should produce a stream with zero elements"); + + long zeroLimitedCount = Stream.of(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)).limit(0).count(); + assertEqual(0L, zeroLimitedCount, "limit(0) should produce zero elements"); + + long skippedPastEnd = Stream.of(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)).skip(99).count(); + assertEqual(0L, skippedPastEnd, "Skipping past the end should produce an empty stream"); + + StringBuffer forEachOrder = new StringBuffer(); + Stream.of(Integer.valueOf(3), Integer.valueOf(1), Integer.valueOf(2)).sorted().forEach(new Consumer() { + public void accept(Integer value) { + forEachOrder.append(value.intValue()); + } + }); + assertEqual("123", forEachOrder.toString(), "forEach should preserve sorted encounter order"); + + assertTrue(!Stream.empty().anyMatch(new Predicate() { + public boolean test(String value) { + return true; + } + }), "anyMatch on empty stream should be false"); + + assertTrue(Stream.empty().allMatch(new Predicate() { + public boolean test(String value) { + return false; + } + }), "allMatch on empty stream should be true"); + + assertTrue(Stream.empty().noneMatch(new Predicate() { + public boolean test(String value) { + return true; + } + }), "noneMatch on empty stream should be true"); + + long distinctWithNullCount = Stream.of("x", "x", null, null, "y").distinct().count(); + assertEqual(3L, distinctWithNullCount, "distinct should keep one null and unique non-null values"); } catch (Throwable t) { fail("Stream API test failed: " + t); return false; diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StreamApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StreamApiIntegrationTest.java new file mode 100644 index 0000000000..d6c6fae585 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StreamApiIntegrationTest.java @@ -0,0 +1,151 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class StreamApiIntegrationTest { + + @Test + void streamEdgeCasesMatchBetweenJavaSEAndParparVM() throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("stream-integration-sources"); + Path classesDir = Files.createTempDirectory("stream-integration-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-classes"); + + Path source = sourceDir.resolve("StreamEdgeApp.java"); + Files.write(source, loadAppSource().getBytes(StandardCharsets.UTF_8)); + + CompilerHelper.CompilerConfig config = selectCompiler(); + if (config == null) { + fail("No compatible compiler available for stream integration test"); + } + + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + + CompilerHelper.compileJavaAPI(javaApiDir, config); + + List compileArgs = new ArrayList<>(); + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + if (CompilerHelper.useClasspath(config)) { + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.add(source.toString()); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "StreamEdgeApp should compile"); + + String javaOutput = runJavaMain(config, classesDir, javaApiDir); + String javaResult = extractResultLine(javaOutput); + assertTrue(javaResult.startsWith("RESULT="), "JavaSE should produce a RESULT line. Output: " + javaOutput); + + CompilerHelper.copyDirectory(javaApiDir, classesDir); + + Path outputDir = Files.createTempDirectory("stream-integration-output"); + CleanTargetIntegrationTest.runTranslator(classesDir, outputDir, "StreamEdgeApp"); + + Path distDir = outputDir.resolve("dist"); + Path cmakeLists = distDir.resolve("CMakeLists.txt"); + assertTrue(Files.exists(cmakeLists), "Translator should emit a CMake project"); + + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(cmakeLists, "StreamEdgeApp-src"); + + Path buildDir = distDir.resolve("build"); + Files.createDirectories(buildDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList( + "cmake", + "-S", distDir.toString(), + "-B", buildDir.toString(), + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang" + ), distDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + + Path executable = buildDir.resolve("StreamEdgeApp"); + String parparOutput = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); + String parparResult = extractResultLine(parparOutput); + assertTrue(parparResult.startsWith("RESULT="), "ParparVM execution should produce a RESULT line. Output: " + parparOutput); + + assertEquals(javaResult, parparResult, + "JavaSE and ParparVM should emit identical result lines for stream edge cases"); + } + + private String loadAppSource() throws Exception { + java.io.InputStream in = StreamApiIntegrationTest.class.getResourceAsStream("/com/codename1/tools/translator/StreamEdgeApp.java"); + assertNotNull(in, "StreamEdgeApp.java test resource should exist"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")) + "\n"; + } + } + + private String runJavaMain(CompilerHelper.CompilerConfig config, Path classesDir, Path javaApiDir) throws Exception { + String javaExe = config.jdkHome.resolve("bin").resolve("java").toString(); + if (System.getProperty("os.name").toLowerCase().contains("win")) { + javaExe += ".exe"; + } + + ProcessBuilder pb = new ProcessBuilder( + javaExe, + "-cp", + classesDir + System.getProperty("path.separator") + javaApiDir, + "StreamEdgeApp" + ); + pb.redirectErrorStream(true); + + Process process = pb.start(); + String output; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + output = reader.lines().collect(Collectors.joining("\n")); + } + + int exitCode = process.waitFor(); + assertEquals(0, exitCode, "JVM run should exit cleanly. Output: " + output); + return output; + } + + private String extractResultLine(String output) { + for (String line : output.split("\\R")) { + if (line.startsWith("RESULT=")) { + return line.trim(); + } + } + return ""; + } + + private CompilerHelper.CompilerConfig selectCompiler() { + String[] preferredTargets = {"11", "17", "21", "25", "1.8"}; + for (String target : preferredTargets) { + List configs = CompilerHelper.getAvailableCompilers(target); + for (CompilerHelper.CompilerConfig config : configs) { + if (CompilerHelper.isJavaApiCompatible(config)) { + return config; + } + } + } + return null; + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/StreamEdgeApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/StreamEdgeApp.java new file mode 100644 index 0000000000..99233a5335 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/StreamEdgeApp.java @@ -0,0 +1,52 @@ +import java.util.stream.Stream; + +public class StreamEdgeApp { + private static int calculate() { + Object[] transformed = Stream.of(4, 2, 9, 2, 7, 4) + .distinct() + .sorted() + .skip(1) + .limit(3) + .toArray(); + int transformedSum = ((Integer) transformed[0]).intValue() + + ((Integer) transformed[1]).intValue() + + ((Integer) transformed[2]).intValue(); + + int reduce = Stream.of(1, 2, 3, 4).reduce(10, (a, b) -> a + b); + + final int[] forEachCode = new int[] { 0 }; + Stream.of(3, 1, 2).sorted().forEach(i -> forEachCode[0] = forEachCode[0] * 10 + i); + + Object[] arr = Stream.of(5, 6).skip(1).toArray(); + long emptyCount = Stream.empty().count(); + + int matchScore = 0; + if (!Stream.empty().anyMatch(v -> true)) { + matchScore += 1; + } + if (Stream.empty().allMatch(v -> false)) { + matchScore += 10; + } + if (Stream.empty().noneMatch(v -> true)) { + matchScore += 100; + } + + long clampCount = Stream.of(1, 2, 3).skip(5).limit(2).count(); + long distinctCount = Stream.of(1, 1, 2, 3, 3).distinct().count(); + + int checksum = 0; + checksum += transformedSum * 2; + checksum += reduce; + checksum += forEachCode[0]; + checksum += ((Integer) arr[0]).intValue() * 7; + checksum += (int) emptyCount * 11; + checksum += matchScore; + checksum += (int) clampCount; + checksum += (int) distinctCount * 13; + return checksum; + } + + public static void main(String[] args) { + System.out.println("RESULT=" + calculate()); + } +}