konsoletyper / teavm

Compiles Java bytecode to JavaScript, WebAssembly and C
https://teavm.org
Apache License 2.0
2.55k stars 260 forks source link

Generated JS code never exits #913

Closed pcoperatr closed 2 months ago

pcoperatr commented 2 months ago

I encountered an issue with JUnit4 test method in my project that gets stuck and never exits when executed by TeaVMTestRunner on JS platform. I stripped off all project-specific code and came up TeaVMNoExitTest test case:

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.PrimitiveIterator;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.teavm.junit.TeaVMTestRunner;

@RunWith(TeaVMTestRunner.class)
public class TeaVMNoExitTest {

    @Test
    public void testStuck() throws IOException {
        new Testee().stuck();
        System.out.println("testStuck: about to exit");
    }

    @Test
    public void testOk() throws IOException {
        new Testee().ok();
        System.out.println("testOk: about to exit");
    }

    private static class Testee {

        void stuck() throws IOException {
            Stream.of("03", "1").map(line -> {
                if (line.codePointCount(0, line.length()) < 10) {
                    return List.of(getPaddedCodePoints(line, 10));
                } else {
                    StringBuilder sb = new StringBuilder(line);
                    List<IntStream> rows = new ArrayList<>();

                    for (;;) {
                        try {
                            int rowCharCount = line.codePointCount(0, 20);
                            rows.add(IntStream.of(1, 2, 3));
                            sb.delete(0, rowCharCount);
                        } catch (IndexOutOfBoundsException ex) {
                            if (!sb.isEmpty()) {
                                rows.add(getPaddedCodePoints(sb, 10));
                            }
                            break;
                        }
                    }
                    return rows;
                }
            }).flatMap(List::stream).map(rowCodePoints -> {
                return rowCodePoints.boxed();
            }).flatMap(Function.identity()).toArray();
        }

        void ok() throws IOException {
            Stream.of("03", "1").map(line -> {
                if (line.codePointCount(0, line.length()) < 10) {
                    return List.of(getPaddedCodePoints(line, 10));
                } else {
                    StringBuilder sb = new StringBuilder(line);
                    List<IntStream> rows = new ArrayList<>();

                    for (;;) {
                        try {
                            int rowCharCount = line.codePointCount(0, 20);
                            rows.add(IntStream.of(1, 2, 3));
                            sb.delete(0, rowCharCount);
                        } catch (IndexOutOfBoundsException ex) {
                            if (!sb.isEmpty()) {
                                rows.add(getPaddedCodePoints(sb, 10));
                            }
                            break;
                        }
                    }
                    return rows;
                }
            }).flatMap(List::stream).map(rowCodePoints -> {
                return rowCodePoints.boxed();
            }).toArray();
        }

        private static IntStream getPaddedCodePoints(CharSequence row, int limit) {
            return IntStream.concat(codePoints(row), IntStream.generate(() -> {
                return ' ';
            })).limit(limit);
        }

        private static IntStream codePoints(CharSequence str) {
            class CodePointIterator implements PrimitiveIterator.OfInt {
                @Override
                public boolean hasNext() {
                    return cur < str.length();
                }

                @Override
                public int nextInt() {
                    char c1 = str.charAt(cur++);
                    if (Character.isHighSurrogate(c1) && cur < str.length()) {
                        char c2 = str.charAt(cur);
                        if (Character.isLowSurrogate(c2)) {
                            cur++;
                            return Character.toCodePoint(c1, c2);
                        }
                    }
                    return c1;
                }

                int cur;
            }

            var iterator = new CodePointIterator();
            return IntStream.generate(() -> 0).
                    takeWhile(x -> iterator.hasNext()).
                    map(n -> iterator.next());
        }
    }
}

Running TeaVMNoExitTest#testOK produces the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
2024-04-26 10:50:10.782:INFO::main: Logging initialized @1392ms to org.eclipse.jetty.util.log.StdErrLog
2024-04-26 10:50:11.030:INFO:oejs.Server:main: jetty-9.4.50.v20221201; built: 2022-12-01T22:07:03.915Z; git: da9a0b30691a45daf90a9f17b5defa2f1434f882; jvm 16.0.2+7-67
2024-04-26 10:50:11.137:INFO:oejs.session:main: DefaultSessionIdManager workerName=node0
2024-04-26 10:50:11.140:INFO:oejs.session:main: No SessionScavenger set, using defaults
2024-04-26 10:50:11.144:INFO:oejs.session:main: node0 Scavenging every 660000ms
2024-04-26 10:50:11.247:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@46cdf8bd{/,null,AVAILABLE}
2024-04-26 10:50:11.583:INFO:oejs.AbstractConnector:main: Started ServerConnector@5e0e82ae{HTTP/1.1, (http/1.1)}{0.0.0.0:54054}
2024-04-26 10:50:11.587:INFO:oejs.Server:main: Started @2200ms
Running chrome with user data dir: C:\Users\asemenyu\AppData\Local\Temp\teavm17340942034458119319teavm
Running TeaVMNoExitTest
chrome stderr: [0426/105013.052:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: [0426/105013.053:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: 
chrome stderr: DevTools listening on ws://127.0.0.1:9222/devtools/browser/ed088f37-55dc-4e11-a708-332c1412b3dd
testOk: about to exit
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.485 sec - in TeaVMNoExitTest
2024-04-26 10:50:23.439:INFO:oejs.AbstractConnector:Thread-12: Stopped ServerConnector@5e0e82ae{HTTP/1.1, (http/1.1)}{0.0.0.0:0}
2024-04-26 10:50:23.440:INFO:oejs.session:Thread-12: node0 Stopped scavenging
2024-04-26 10:50:23.443:INFO:oejsh.ContextHandler:Thread-12: Stopped o.e.j.s.ServletContextHandler@46cdf8bd{/,null,STOPPED}

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  19.270 s
[INFO] Finished at: 2024-04-26T10:50:24-04:00
[INFO] ------------------------------------------------------------------------

testOk: about to exit is printed and the test exits as expected.

Running TeaVMNoExitTest#testStuck produces the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
2024-04-26 10:52:47.312:INFO::main: Logging initialized @1383ms to org.eclipse.jetty.util.log.StdErrLog
2024-04-26 10:52:47.717:INFO:oejs.Server:main: jetty-9.4.50.v20221201; built: 2022-12-01T22:07:03.915Z; git: da9a0b30691a45daf90a9f17b5defa2f1434f882; jvm 16.0.2+7-67
2024-04-26 10:52:48.002:INFO:oejs.session:main: DefaultSessionIdManager workerName=node0
2024-04-26 10:52:48.002:INFO:oejs.session:main: No SessionScavenger set, using defaults
2024-04-26 10:52:48.006:INFO:oejs.session:main: node0 Scavenging every 660000ms
2024-04-26 10:52:48.163:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@46cdf8bd{/,null,AVAILABLE}
2024-04-26 10:52:48.702:INFO:oejs.AbstractConnector:main: Started ServerConnector@5e0e82ae{HTTP/1.1, (http/1.1)}{0.0.0.0:57254}
2024-04-26 10:52:48.705:INFO:oejs.Server:main: Started @2783ms
Running chrome with user data dir: C:\Users\asemenyu\AppData\Local\Temp\teavm13030094901025833324teavm
Running TeaVMNoExitTest
chrome stderr: [0426/105249.913:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: [0426/105249.914:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: 
chrome stderr: DevTools listening on ws://127.0.0.1:9222/devtools/browser/4be3e413-347c-4960-91b4-01677285e0eb
testStuck: about to exit

testStuck: about to exit is printed but the test doesn't exit, the execution gets stuck at this point.

Testee#ok and Testee#stuck don't differ much: The first one ends with .flatMap(Function.identity()).toArray() and the second one with .toArray(), the rest is the same. Additional Stream#flatMap(Function.identity()) call makes a huge difference.

My Maven surefire plugin config:

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.19.1</version>
  <configuration>
    <parallel>none</parallel>
    <systemProperties>
      <teavm.junit.target>${project.build.directory}/js-tests</teavm.junit.target>
      <teavm.junit.js.runner>browser-chrome</teavm.junit.js.runner>
    </systemProperties>
    <environmentVariables>
      <!--
      TeaVM runs Chrome through "cmd.exe" without the full path specified,
      so need to add the location to chrome.exe to the PATH to make it work
      -->
      <PATH>${PATH};C:/Program Files (x86)/Google/Chrome/Application</PATH>
    </environmentVariables>
  </configuration>
</plugin>

Can reproduce the issue with TeaVM version 0.10.0-SNAPSHOT (master branch) and also with 0.9.2 release.

pcoperatr commented 2 months ago

If I change Testee#codePoints() to:

        private static IntStream codePoints(CharSequence str) {
            return IntStream.range(0, str.length());
        }

All test cases pass and exit:

Running chrome with user data dir: C:\Users\asemenyu\AppData\Local\Temp\teavm18155043029957928249teavm
Running TeaVMNoExitTest
chrome stderr: [0426/111047.172:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: [0426/111047.174:WARNING:registry_dict.cc(360)] Can't convert registry key to schema type string
chrome stderr: 
chrome stderr: DevTools listening on ws://127.0.0.1:9222/devtools/browser/523f8da0-a056-4a99-b083-24513d2ea899
testOk: about to exit
testStuck: about to exit
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 14.189 sec - in TeaVMNoExitTest
konsoletyper commented 2 months ago

I'll take a look. And also I'd like to remind you that you can debug tests right in the browser. One quick tip: when test falls into infinite loop, you can't open dev tools. The solution is to edit HTML file and update script to run test on, say, clicking a button. In this case you open HTML file, then open dev tools and click button.

konsoletyper commented 2 months ago

BTW, if your intention was to implement missing CharacterSequence.codePoints method, than this is the wrong way. Please, take a look at how String.characters implemented.

pcoperatr commented 2 months ago

No, the intention was different. I had to emulate CharacterSequence.codePoints to make my code run with TeaVM.

So the problem is not with CharacterSequence.codePoints.

Please reopen this issue as it is about generated JS code getting stuck.

konsoletyper commented 2 months ago

I closed this issue because I fixed it