From 8db406c6035d68ef2b7c26d5f2841f89ed41edb0 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 9 Jan 2024 20:07:08 +0100 Subject: [PATCH] jso: implement exporting Java methods to JS Fix #785 --- .../java/org/teavm/backend/c/CTarget.java | 21 +- .../javascript/ExportedDeclaration.java | 32 ++ .../backend/javascript/JavaScriptTarget.java | 52 +- .../javascript/rendering/Renderer.java | 30 +- .../rendering/RenderingManager.java | 9 + .../org/teavm/backend/wasm/WasmTarget.java | 40 +- .../org/teavm/dependency/DependencyAgent.java | 9 + .../teavm/dependency/DependencyAnalyzer.java | 19 +- .../dependency/DependencyClassSource.java | 14 +- .../model/ClassHolderTransformerContext.java | 2 + .../analysis/ClassInitializerAnalysis.java | 10 +- core/src/main/java/org/teavm/vm/TeaVM.java | 106 ++-- .../org/teavm/vm/TeaVMTargetController.java | 5 +- .../src/main/java/org/teavm/jso/JSClass.java | 27 + .../src/main/java/org/teavm/jso/JSExport.java | 26 + .../java/org/teavm/jso/JSExportClasses.java | 27 + .../org/teavm/jso/impl/JSAliasRenderer.java | 267 ++++++--- .../teavm/jso/impl/JSDependencyListener.java | 21 +- .../jso/impl/JSObjectClassTransformer.java | 225 +++++--- .../org/teavm/jso/impl/JSValueMarshaller.java | 20 + samples/module-test/build.gradle.kts | 36 ++ .../teavm/samples/modules/SimpleModule.java | 34 ++ samples/settings.gradle.kts | 1 + settings.gradle.kts | 1 + tests/build.gradle.kts | 1 + .../org/teavm/dependency/ClassValueTest.java | 2 +- .../org/teavm/dependency/DependencyTest.java | 2 +- .../teavm/incremental/IncrementalTest.java | 2 +- .../java/org/teavm/jso/export/ExportTest.java | 120 ++++ .../jso/export/ModuleWithConsumedObject.java | 30 +- .../ModuleWithExportedClassMembers.java | 64 +++ .../jso/export/ModuleWithExportedClasses.java | 51 ++ .../jso/export/ModuleWithInitializer.java | 57 ++ .../jso/export/ModuleWithPrimitiveTypes.java | 128 +++++ .../org/teavm/jso/export/SimpleModule.java | 28 + .../test/java/org/teavm/tests/JSOTest.java | 2 +- .../resources/org/teavm/jso/export/assert.js | 39 ++ .../teavm/jso/export/exportClassMembers.js | 25 + .../org/teavm/jso/export/exportClasses.js | 24 + .../teavm/jso/export/importClassMembers.js | 23 + .../org/teavm/jso/export/initializer.js | 25 + .../org/teavm/jso/export/primitives.js | 52 ++ .../resources/org/teavm/jso/export/simple.js | 20 + tools/browser-runner/build.gradle.kts | 53 ++ .../browserrunner/BrowserRunDescriptor.java | 55 ++ .../teavm/browserrunner/BrowserRunner.java | 526 ++++++++++++++++++ .../src/main/resources/test-server/client.js | 0 .../src/main/resources/test-server/frame.html | 0 .../src/main/resources/test-server/frame.js | 2 +- .../src/main/resources/test-server/index.html | 0 .../c/incremental/IncrementalCBuilder.java | 5 +- .../java/org/teavm/tooling/TeaVMTool.java | 5 +- .../java/org/teavm/devserver/CodeServlet.java | 2 +- tools/junit/build.gradle.kts | 24 +- .../org/teavm/junit/BrowserRunStrategy.java | 504 +---------------- .../org/teavm/junit/JSPlatformSupport.java | 21 +- .../org/teavm/junit/TestPlatformSupport.java | 2 +- .../junit/WebAssemblyPlatformSupport.java | 18 +- 58 files changed, 2115 insertions(+), 831 deletions(-) create mode 100644 core/src/main/java/org/teavm/backend/javascript/ExportedDeclaration.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSClass.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSExport.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSExportClasses.java create mode 100644 samples/module-test/build.gradle.kts create mode 100644 samples/module-test/src/main/java/org/teavm/samples/modules/SimpleModule.java create mode 100644 tests/src/test/java/org/teavm/jso/export/ExportTest.java rename core/src/main/java/org/teavm/vm/TeaVMEntryPoint.java => tests/src/test/java/org/teavm/jso/export/ModuleWithConsumedObject.java (53%) create mode 100644 tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java create mode 100644 tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java create mode 100644 tests/src/test/java/org/teavm/jso/export/ModuleWithInitializer.java create mode 100644 tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java create mode 100644 tests/src/test/java/org/teavm/jso/export/SimpleModule.java create mode 100644 tests/src/test/resources/org/teavm/jso/export/assert.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/exportClasses.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/importClassMembers.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/initializer.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/primitives.js create mode 100644 tests/src/test/resources/org/teavm/jso/export/simple.js create mode 100644 tools/browser-runner/build.gradle.kts create mode 100644 tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunDescriptor.java create mode 100644 tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java rename tools/{junit => browser-runner}/src/main/resources/test-server/client.js (100%) rename tools/{junit => browser-runner}/src/main/resources/test-server/frame.html (100%) rename tools/{junit => browser-runner}/src/main/resources/test-server/frame.js (98%) rename tools/{junit => browser-runner}/src/main/resources/test-server/index.html (100%) diff --git a/core/src/main/java/org/teavm/backend/c/CTarget.java b/core/src/main/java/org/teavm/backend/c/CTarget.java index 2e0298c71..04b98b967 100644 --- a/core/src/main/java/org/teavm/backend/c/CTarget.java +++ b/core/src/main/java/org/teavm/backend/c/CTarget.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -139,7 +138,6 @@ import org.teavm.runtime.RuntimeArray; import org.teavm.runtime.RuntimeClass; import org.teavm.runtime.RuntimeObject; import org.teavm.vm.BuildTarget; -import org.teavm.vm.TeaVMEntryPoint; import org.teavm.vm.TeaVMTarget; import org.teavm.vm.TeaVMTargetController; import org.teavm.vm.spi.TeaVMHostExtension; @@ -817,8 +815,7 @@ public class CTarget implements TeaVMTarget, TeaVMCHost { private void generateMain(GenerationContext context, CodeWriter writer, IncludeManager includes, ListableClassHolderSource classes, List types) { - Iterator entryPointIter = controller.getEntryPoints().values().iterator(); - String mainFunctionName = entryPointIter.hasNext() ? entryPointIter.next().getPublicName() : null; + var mainFunctionName = controller.getEntryPointName(); if (mainFunctionName == null) { mainFunctionName = "main"; } @@ -936,15 +933,13 @@ public class CTarget implements TeaVMTarget, TeaVMCHost { private void generateCallToMainMethod(IntrinsicContext context, InvocationExpr invocation) { NameProvider names = context.names(); - Iterator entryPointIter = controller.getEntryPoints().values().iterator(); - if (entryPointIter.hasNext()) { - TeaVMEntryPoint entryPoint = entryPointIter.next(); - context.importMethod(entryPoint.getMethod(), true); - String mainMethod = names.forMethod(entryPoint.getMethod()); - context.writer().print(mainMethod + "("); - context.emit(invocation.getArguments().get(0)); - context.writer().print(")"); - } + var method = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class), + ValueType.parse(void.class)); + context.importMethod(method, true); + String mainMethod = names.forMethod(method); + context.writer().print(mainMethod + "("); + context.emit(invocation.getArguments().get(0)); + context.writer().print(")"); } @Override diff --git a/core/src/main/java/org/teavm/backend/javascript/ExportedDeclaration.java b/core/src/main/java/org/teavm/backend/javascript/ExportedDeclaration.java new file mode 100644 index 000000000..81772f0aa --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/ExportedDeclaration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.javascript; + +import java.util.function.Consumer; +import org.teavm.backend.javascript.codegen.NamingStrategy; +import org.teavm.backend.javascript.codegen.SourceWriter; + +public class ExportedDeclaration { + final Consumer name; + final Consumer nameFreq; + final String alias; + + public ExportedDeclaration(Consumer name, Consumer nameFreq, String alias) { + this.name = name; + this.nameFreq = nameFreq; + this.alias = alias; + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java index cd5b37444..fbf89a94f 100644 --- a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java +++ b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java @@ -129,6 +129,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private final Map importedModules = new LinkedHashMap<>(); private JavaScriptTemplateFactory templateFactory; private JSModuleType moduleType = JSModuleType.UMD; + private List exports = new ArrayList<>(); private int maxTopLevelNames = 80_000; @Override @@ -398,7 +399,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { var rememberingWriter = new RememberingSourceWriter(debugEmitter != null); var renderer = new Renderer(rememberingWriter, asyncMethods, renderingContext, controller.getDiagnostics(), - methodGenerators, astCache, controller.getCacheStatus(), templateFactory); + methodGenerators, astCache, controller.getCacheStatus(), templateFactory, exports, + controller.getEntryPoint()); renderer.setProperties(controller.getProperties()); renderer.setProgressConsumer(controller::reportProgress); @@ -414,15 +416,19 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { renderer.renderStringPool(); renderer.renderStringConstants(); renderer.renderCompatibilityStubs(); - for (var entry : controller.getEntryPoints().entrySet()) { - var alias = "$rt_export_" + entry.getKey(); - var ref = entry.getValue().getMethod(); + + var alias = "$rt_export_main"; + var ref = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class), + ValueType.parse(void.class)); + if (classes.resolve(ref) != null) { rememberingWriter.startVariableDeclaration().appendFunction(alias) .appendFunction("$rt_mainStarter").append("(").appendMethod(ref); rememberingWriter.append(")").endDeclaration(); rememberingWriter.appendFunction(alias).append(".") .append("javaException").ws().append("=").ws().appendFunction("$rt_javaException") .append(";").newLine(); + exports.add(new ExportedDeclaration(w -> w.appendFunction(alias), + n -> n.functionName(alias), controller.getEntryPointName())); } for (var listener : rendererListeners) { @@ -448,8 +454,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { for (var module : importedModules.values()) { naming.functionName(module); } - for (var exportedName : controller.getEntryPoints().keySet()) { - naming.functionName("$rt_export_" + exportedName); + for (var export : exports) { + export.nameFreq.accept(naming); } var frequencyEstimator = new NameFrequencyEstimator(); runtime.replay(frequencyEstimator, RememberedSource.FILTER_REF); @@ -553,8 +559,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { } private void printIIFStart(SourceWriter writer) { - for (var exportedName : controller.getEntryPoints().keySet()) { - writer.append("var ").appendGlobal(exportedName).append(";").softNewLine(); + for (var export : exports) { + writer.append("var ").appendGlobal(export.alias).append(";").softNewLine(); } writer.append("(function()").appendBlockStart(); @@ -603,40 +609,42 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { } private void printUmdEnd(SourceWriter writer) { - for (var export : controller.getEntryPoints().keySet()) { - writer.appendFunction("$rt_exports").append(".").append(export).ws().append("=").ws() - .appendFunction("$rt_export_" + export).append(";").softNewLine(); + for (var export : exports) { + writer.appendFunction("$rt_exports").append(".").append(export.alias).ws() + .append("=").ws(); + export.name.accept(writer); + writer.append(";").softNewLine(); } writer.outdent().append("}));").newLine(); } private void printCommonJsEnd(SourceWriter writer) { - for (var export : controller.getEntryPoints().keySet()) { - writer.appendFunction("exports.").append(export).ws().append("=").ws() - .appendFunction("$rt_export_" + export).append(";").softNewLine(); + for (var export : exports) { + writer.append("exports.").append(export.alias).ws().append("=").ws(); + export.name.accept(writer); + writer.append(";").softNewLine(); } } private void printIFFEnd(SourceWriter writer) { - for (var exportedName : controller.getEntryPoints().keySet()) { - writer.appendGlobal(exportedName).ws().append("=").ws().appendFunction("$rt_export_" + exportedName) - .append(";").softNewLine(); + for (var export : exports) { + writer.appendGlobal(export.alias).ws().append("=").ws(); + export.name.accept(writer); + writer.append(";").softNewLine(); } writer.outdent().append("})();"); } private void printES2015End(SourceWriter writer) { - if (controller.getEntryPoints().isEmpty()) { - return; - } writer.append("export").ws().append("{").ws(); var first = true; - for (var exportedName : controller.getEntryPoints().keySet()) { + for (var export : exports) { if (!first) { writer.append(",").ws(); } first = false; - writer.appendFunction("$rt_export_" + exportedName).append(" as ").append(exportedName); + export.name.accept(writer); + writer.append(" as ").append(export.alias); } writer.ws().append("};").softNewLine(); } diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java index bd60225bc..d6e339133 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java @@ -35,6 +35,7 @@ import org.teavm.ast.RegularMethodNode; import org.teavm.ast.analysis.LocationGraphBuilder; import org.teavm.ast.decompilation.DecompilationException; import org.teavm.ast.decompilation.Decompiler; +import org.teavm.backend.javascript.ExportedDeclaration; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.spi.GeneratedBy; import org.teavm.backend.javascript.spi.Generator; @@ -89,11 +90,15 @@ public class Renderer implements RenderingManager { private JavaScriptTemplateFactory templateFactory; private boolean threadLibraryUsed; private AstDependencyExtractor dependencyExtractor = new AstDependencyExtractor(); + private List exports; + private String entryPoint; + public static final MethodDescriptor CLINIT_METHOD = new MethodDescriptor("", ValueType.VOID); public Renderer(SourceWriter writer, Set asyncMethods, RenderingContext context, Diagnostics diagnostics, Map generators, - MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory) { + MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory, + List exports, String entryPoint) { this.writer = writer; this.classSource = context.getClassSource(); this.classLoader = context.getClassLoader(); @@ -106,6 +111,8 @@ public class Renderer implements RenderingManager { this.astCache = astCache; this.cacheStatus = cacheStatus; this.templateFactory = templateFactory; + this.exports = exports; + this.entryPoint = entryPoint; } @Override @@ -113,6 +120,27 @@ public class Renderer implements RenderingManager { return writer; } + @Override + public String getEntryPoint() { + return entryPoint; + } + + @Override + public void exportMethod(MethodReference method, String alias) { + exports.add(new ExportedDeclaration(w -> w.appendMethod(method), n -> n.methodName(method), alias)); + } + + @Override + public void exportClass(String className, String alias) { + exports.add(new ExportedDeclaration(w -> w.appendClass(className), n -> n.className(className), alias)); + } + + @Override + public void exportFunction(String functionName, String alias) { + exports.add(new ExportedDeclaration(w -> w.appendFunction(functionName), + n -> n.functionName(functionName), alias)); + } + public boolean isThreadLibraryUsed() { return threadLibraryUsed; } diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingManager.java b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingManager.java index e72c0cbba..54f166dfe 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingManager.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingManager.java @@ -19,13 +19,22 @@ import java.util.Properties; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.common.ServiceRepository; import org.teavm.model.ListableClassReaderSource; +import org.teavm.model.MethodReference; public interface RenderingManager extends ServiceRepository { SourceWriter getWriter(); + void exportMethod(MethodReference method, String alias); + + void exportClass(String className, String alias); + + void exportFunction(String functionName, String alias); + ListableClassReaderSource getClassSource(); ClassLoader getClassLoader(); Properties getProperties(); + + String getEntryPoint(); } diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java index 6d8ac9a81..b808ab950 100644 --- a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java +++ b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java @@ -105,7 +105,6 @@ import org.teavm.backend.wasm.model.expression.WasmLoadInt32; import org.teavm.backend.wasm.model.expression.WasmReturn; import org.teavm.backend.wasm.model.expression.WasmSetLocal; import org.teavm.backend.wasm.model.expression.WasmStoreInt32; -import org.teavm.backend.wasm.model.expression.WasmUnreachable; import org.teavm.backend.wasm.optimization.UnusedFunctionElimination; import org.teavm.backend.wasm.render.ReportingWasmBinaryStatsCollector; import org.teavm.backend.wasm.render.WasmBinaryRenderer; @@ -176,7 +175,6 @@ import org.teavm.runtime.RuntimeClass; import org.teavm.runtime.RuntimeObject; import org.teavm.runtime.ShadowStack; import org.teavm.vm.BuildTarget; -import org.teavm.vm.TeaVMEntryPoint; import org.teavm.vm.TeaVMTarget; import org.teavm.vm.TeaVMTargetController; import org.teavm.vm.spi.TeaVMHostExtension; @@ -1197,29 +1195,23 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost { public WasmExpression apply(InvocationExpr invocation, WasmIntrinsicManager manager) { switch (invocation.getMethod().getName()) { case "runMain": { - var entryPointIter = controller.getEntryPoints().values().iterator(); - if (entryPointIter.hasNext()) { - TeaVMEntryPoint entryPoint = entryPointIter.next(); - String name = manager.getNames().forMethod(entryPoint.getMethod()); - WasmCall call = new WasmCall(name); - var arg = manager.generate(invocation.getArguments().get(0)); - if (manager.isManagedMethodCall(entryPoint.getMethod())) { - var block = new WasmBlock(false); - block.setType(WasmType.INT32); - var callSiteId = manager.generateCallSiteId(invocation.getLocation()); - block.getBody().add(manager.generateRegisterCallSite(callSiteId, - invocation.getLocation())); - block.getBody().add(arg); - arg = block; - } - call.getArguments().add(arg); - call.setLocation(invocation.getLocation()); - return call; - } else { - var unreachable = new WasmUnreachable(); - unreachable.setLocation(invocation.getLocation()); - return unreachable; + var entryPoint = new MethodReference(controller.getEntryPoint(), + "main", ValueType.parse(String[].class), ValueType.parse(void.class)); + String name = manager.getNames().forMethod(entryPoint); + WasmCall call = new WasmCall(name); + var arg = manager.generate(invocation.getArguments().get(0)); + if (manager.isManagedMethodCall(entryPoint)) { + var block = new WasmBlock(false); + block.setType(WasmType.INT32); + var callSiteId = manager.generateCallSiteId(invocation.getLocation()); + block.getBody().add(manager.generateRegisterCallSite(callSiteId, + invocation.getLocation())); + block.getBody().add(arg); + arg = block; } + call.getArguments().add(arg); + call.setLocation(invocation.getLocation()); + return call; } case "setCurrentThread": { String name = manager.getNames().forMethod(new MethodReference(Thread.class, diff --git a/core/src/main/java/org/teavm/dependency/DependencyAgent.java b/core/src/main/java/org/teavm/dependency/DependencyAgent.java index d2672babe..b26c747da 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyAgent.java +++ b/core/src/main/java/org/teavm/dependency/DependencyAgent.java @@ -24,11 +24,20 @@ import org.teavm.model.*; public class DependencyAgent implements DependencyInfo, ServiceRepository { private DependencyAnalyzer analyzer; + private String entryPoint; DependencyAgent(DependencyAnalyzer analyzer) { this.analyzer = analyzer; } + public String getEntryPoint() { + return entryPoint; + } + + void setEntryPoint(String entryPoint) { + this.entryPoint = entryPoint; + } + public DependencyNode createNode() { return analyzer.createNode(); } diff --git a/core/src/main/java/org/teavm/dependency/DependencyAnalyzer.java b/core/src/main/java/org/teavm/dependency/DependencyAnalyzer.java index cb3f1c0e8..2dd215208 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyAnalyzer.java +++ b/core/src/main/java/org/teavm/dependency/DependencyAnalyzer.java @@ -142,6 +142,11 @@ public abstract class DependencyAnalyzer implements DependencyInfo { classType = getType("java.lang.Class"); } + public void setEntryPoint(String entryPoint) { + classSource.setEntryPoint(entryPoint); + agent.setEntryPoint(entryPoint); + } + public void setObfuscated(boolean obfuscated) { classSource.obfuscated = obfuscated; } @@ -290,20 +295,6 @@ public abstract class DependencyAnalyzer implements DependencyInfo { classSource.addTransformer(transformer); } - public void addEntryPoint(MethodReference methodRef, String... argumentTypes) { - ValueType[] parameters = methodRef.getDescriptor().getParameterTypes(); - if (parameters.length + 1 != argumentTypes.length) { - throw new IllegalArgumentException("argumentTypes length does not match the number of method's arguments"); - } - MethodDependency method = linkMethod(methodRef); - method.use(); - DependencyNode[] varNodes = method.getVariables(); - varNodes[0].propagate(getType(methodRef.getClassName())); - for (int i = 0; i < argumentTypes.length; ++i) { - varNodes[i + 1].propagate(getType(argumentTypes[i])); - } - } - private int propagationDepth; void schedulePropagation(DependencyConsumer consumer, DependencyType type) { diff --git a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java index c459efd8c..1deb5c2b6 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java +++ b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java @@ -47,6 +47,7 @@ class DependencyClassSource implements ClassHolderSource { Map> cache = new LinkedHashMap<>(1000, 0.5f); private ReferenceResolver referenceResolver; private ClassInitInsertion classInitInsertion; + private String entryPoint; DependencyClassSource(ClassReaderSource innerSource, Diagnostics diagnostics, IncrementalDependencyRegistration dependencyRegistration, String[] platformTags) { @@ -117,10 +118,6 @@ class DependencyClassSource implements ClassHolderSource { return generatedClasses.keySet(); } - public Collection getGeneratedClasses() { - return generatedClasses.values(); - } - public boolean isGeneratedClass(String className) { return generatedClasses.containsKey(className); } @@ -129,6 +126,10 @@ class DependencyClassSource implements ClassHolderSource { transformers.add(transformer); } + void setEntryPoint(String entryPoint) { + this.entryPoint = entryPoint; + } + public void dispose() { transformers.clear(); } @@ -163,5 +164,10 @@ class DependencyClassSource implements ClassHolderSource { public void submit(ClassHolder cls) { DependencyClassSource.this.submit(cls); } + + @Override + public String getEntryPoint() { + return entryPoint; + } }; } diff --git a/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java b/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java index 936864d3a..3cbf89ddb 100644 --- a/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java +++ b/core/src/main/java/org/teavm/model/ClassHolderTransformerContext.java @@ -29,5 +29,7 @@ public interface ClassHolderTransformerContext { boolean isStrict(); + String getEntryPoint(); + void submit(ClassHolder cls); } diff --git a/core/src/main/java/org/teavm/model/analysis/ClassInitializerAnalysis.java b/core/src/main/java/org/teavm/model/analysis/ClassInitializerAnalysis.java index 210b8714b..a556180eb 100644 --- a/core/src/main/java/org/teavm/model/analysis/ClassInitializerAnalysis.java +++ b/core/src/main/java/org/teavm/model/analysis/ClassInitializerAnalysis.java @@ -50,14 +50,17 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo { private Map methodInfoMap = new HashMap<>(); private ListableClassReaderSource classes; private ClassHierarchy hierarchy; + private String entryPoint; private List order = new ArrayList<>(); private List readonlyOrder = Collections.unmodifiableList(order); private String currentAnalyzedClass; private DependencyInfo dependencyInfo; - public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy) { + public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy, + String entryPoint) { this.classes = classes; this.hierarchy = hierarchy; + this.entryPoint = entryPoint; } public void analyze(DependencyInfo dependencyInfo) { @@ -99,6 +102,11 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo { return; } + if (className.equals(entryPoint)) { + classStatuses.put(className, DYNAMIC); + return; + } + var cls = classes.get(className); if (cls == null || cls.getAnnotations().get(StaticInit.class.getName()) != null) { diff --git a/core/src/main/java/org/teavm/vm/TeaVM.java b/core/src/main/java/org/teavm/vm/TeaVM.java index 6950ecb58..1da4fa2d3 100644 --- a/core/src/main/java/org/teavm/vm/TeaVM.java +++ b/core/src/main/java/org/teavm/vm/TeaVM.java @@ -23,7 +23,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -73,6 +72,7 @@ import org.teavm.model.ProgramCache; import org.teavm.model.ValueType; import org.teavm.model.analysis.ClassInitializerAnalysis; import org.teavm.model.analysis.ClassInitializerInfo; +import org.teavm.model.instructions.ExitInstruction; import org.teavm.model.instructions.InitClassInstruction; import org.teavm.model.instructions.InvokeInstruction; import org.teavm.model.optimization.ArrayUnwrapMotion; @@ -134,12 +134,13 @@ import org.teavm.vm.spi.TeaVMPlugin; public class TeaVM implements TeaVMHost, ServiceRepository { private static final MethodDescriptor MAIN_METHOD_DESC = new MethodDescriptor("main", ValueType.arrayOf(ValueType.object("java.lang.String")), ValueType.VOID); + private static final MethodDescriptor CLINIT_DESC = new MethodDescriptor("", ValueType.VOID); private final DependencyAnalyzer dependencyAnalyzer; private final AccumulationDiagnostics diagnostics = new AccumulationDiagnostics(); private final ClassLoader classLoader; - private final Map entryPoints = new LinkedHashMap<>(); - private final Map readonlyEntryPoints = Collections.unmodifiableMap(entryPoints); + private String entryPoint; + private String entryPointName = "main"; private final Set preservedClasses = new HashSet<>(); private final Set readonlyPreservedClasses = Collections.unmodifiableSet(preservedClasses); private final Map, Object> services = new HashMap<>(); @@ -284,39 +285,50 @@ public class TeaVM implements TeaVMHost, ServiceRepository { return target.getPlatformTags(); } - public void entryPoint(String className, String name) { - if (entryPoints.containsKey(name)) { - throw new IllegalArgumentException("Entry point with public name `" + name + "' already defined " - + "for class " + className); - } - - var cls = dependencyAnalyzer.getClassSource().get(className); - if (cls == null) { - diagnostics.error(null, "There's no main class: '{{c0}}'", className); - return; - } - - if (cls.getMethod(MAIN_METHOD_DESC) == null) { - diagnostics.error(null, "Specified main class '{{c0}}' does not have method '" + MAIN_METHOD_DESC + "'", - cls.getName()); - return; - } - - var mainMethod = dependencyAnalyzer.linkMethod(new MethodReference(className, - "main", ValueType.parse(String[].class), ValueType.VOID)); - - var entryPoint = new TeaVMEntryPoint(name, mainMethod); - dependencyAnalyzer.defer(() -> { - dependencyAnalyzer.linkClass(className).initClass(null); - mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;")); - mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String")); - mainMethod.use(); - }); - entryPoints.put(name, entryPoint); + public void setEntryPoint(String entryPoint) { + this.entryPoint = entryPoint; } - public void entryPoint(String className) { - entryPoint(className, "main"); + public void setEntryPointName(String entryPointName) { + this.entryPointName = entryPointName; + } + + private void processEntryPoint() { + dependencyAnalyzer.setEntryPoint(entryPoint); + dependencyAnalyzer.addClassTransformer((c, context) -> { + if (c.getName().equals(entryPoint)) { + var clinit = c.getMethod(CLINIT_DESC); + if (clinit == null) { + clinit = new MethodHolder(CLINIT_DESC); + clinit.getModifiers().add(ElementModifier.STATIC); + var clinitProg = new Program(); + clinitProg.createVariable(); + var block = clinitProg.createBasicBlock(); + block.add(new ExitInstruction()); + clinit.setProgram(clinitProg); + c.addMethod(clinit); + } + } + }); + + var cls = dependencyAnalyzer.getClassSource().get(entryPoint); + if (cls == null) { + diagnostics.error(null, "There's no main class: '{{c0}}'", entryPoint); + return; + } + + var mainMethod = cls.getMethod(MAIN_METHOD_DESC) != null + ? dependencyAnalyzer.linkMethod(new MethodReference(entryPoint, + "main", ValueType.parse(String[].class), ValueType.VOID)) + : null; + dependencyAnalyzer.defer(() -> { + dependencyAnalyzer.linkClass(entryPoint).initClass(null); + if (mainMethod != null) { + mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;")); + mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String")); + mainMethod.use(); + } + }); } public void preserveType(String className) { @@ -368,6 +380,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository { return; } + processEntryPoint(); dependencyAnalyzer.setAsyncSupported(target.isAsyncSupported()); dependencyAnalyzer.setInterruptor(() -> { int progress = dependencyAnalyzer.getReachableClasses().size(); @@ -453,7 +466,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository { } var classInitializerAnalysis = new ClassInitializerAnalysis(classSet, - dependencyAnalyzer.getClassHierarchy()); + dependencyAnalyzer.getClassHierarchy(), entryPoint); classInitializerAnalysis.analyze(dependencyAnalyzer); classInitializerInfo = classInitializerAnalysis; insertClassInit(classSet); @@ -535,13 +548,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository { } } - var initializers = target.getInitializerMethods(); - if (initializers == null) { - initializers = entryPoints.values().stream().map(ep -> ep.getMethod()).collect(Collectors.toList()); - } - for (var initMethod : initializers) { - addInitializersToEntryPoint(classes, initMethod); - } + addInitializersToEntryPoint(classes, new MethodReference(entryPoint, CLINIT_DESC)); } private void addInitializersToEntryPoint(ClassHolderSource classes, MethodReference methodRef) { @@ -560,7 +567,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository { Instruction first = block.getFirstInstruction(); for (String className : classInitializerInfo.getInitializationOrder()) { var invoke = new InvokeInstruction(); - invoke.setMethod(new MethodReference(className, "", ValueType.VOID)); + invoke.setMethod(new MethodReference(className, CLINIT_DESC)); first.insertPrevious(invoke); } } @@ -914,8 +921,13 @@ public class TeaVM implements TeaVMHost, ServiceRepository { } @Override - public Map getEntryPoints() { - return readonlyEntryPoints; + public String getEntryPoint() { + return entryPoint; + } + + @Override + public String getEntryPointName() { + return entryPointName; } @Override @@ -1032,9 +1044,9 @@ public class TeaVM implements TeaVMHost, ServiceRepository { ListableClassReaderSourceAdapter(ClassReaderSource classSource, Set classes) { this.classSource = classSource; - this.classes = Collections.unmodifiableSet(classes.stream() + this.classes = classes.stream() .filter(className -> classSource.get(className) != null) - .collect(Collectors.toSet())); + .collect(Collectors.toUnmodifiableSet()); } @Override diff --git a/core/src/main/java/org/teavm/vm/TeaVMTargetController.java b/core/src/main/java/org/teavm/vm/TeaVMTargetController.java index e2a530aae..f9191cdfb 100644 --- a/core/src/main/java/org/teavm/vm/TeaVMTargetController.java +++ b/core/src/main/java/org/teavm/vm/TeaVMTargetController.java @@ -15,7 +15,6 @@ */ package org.teavm.vm; -import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.function.Predicate; @@ -48,7 +47,9 @@ public interface TeaVMTargetController { boolean isFriendlyToDebugger(); - Map getEntryPoints(); + String getEntryPoint(); + + String getEntryPointName(); Set getPreservedClasses(); diff --git a/jso/core/src/main/java/org/teavm/jso/JSClass.java b/jso/core/src/main/java/org/teavm/jso/JSClass.java new file mode 100644 index 000000000..bffcab2ec --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSClass.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface JSClass { + String name() default ""; +} diff --git a/jso/core/src/main/java/org/teavm/jso/JSExport.java b/jso/core/src/main/java/org/teavm/jso/JSExport.java new file mode 100644 index 000000000..25e8f74f8 --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSExport.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JSExport { +} diff --git a/jso/core/src/main/java/org/teavm/jso/JSExportClasses.java b/jso/core/src/main/java/org/teavm/jso/JSExportClasses.java new file mode 100644 index 000000000..d6f601ffd --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSExportClasses.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface JSExportClasses { + Class[] value(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java index 26f86b7ba..01c7678b0 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java @@ -16,12 +16,16 @@ package org.teavm.jso.impl; import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.rendering.RenderingManager; import org.teavm.backend.javascript.spi.VirtualMethodContributor; import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; +import org.teavm.jso.JSClass; import org.teavm.model.AnnotationReader; import org.teavm.model.ClassReader; +import org.teavm.model.ElementModifier; import org.teavm.model.FieldReader; import org.teavm.model.FieldReference; import org.teavm.model.ListableClassReaderSource; @@ -36,104 +40,209 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { private SourceWriter writer; private ListableClassReaderSource classSource; private JSTypeHelper typeHelper; + private RenderingManager context; @Override public void begin(RenderingManager context, BuildTarget buildTarget) { writer = context.getWriter(); classSource = context.getClassSource(); typeHelper = new JSTypeHelper(context.getClassSource()); + this.context = context; } @Override public void complete() { + exportClasses(); + exportModule(); + } + + private void exportClasses() { if (!hasClassesToExpose()) { return; } writer.startVariableDeclaration().appendFunction("$rt_jso_marker") .appendGlobal("Symbol").append("('jsoClass')").endDeclaration(); - writer.append("(function()").ws().append("{").softNewLine().indent(); - writer.append("var c;").softNewLine(); - for (String className : classSource.getClassNames()) { - ClassReader classReader = classSource.get(className); - var methods = new HashMap(); - var properties = new HashMap(); - for (var method : classReader.getMethods()) { - var methodAlias = getPublicAlias(method); - if (methodAlias != null) { - switch (methodAlias.kind) { - case METHOD: - methods.put(methodAlias.name, method.getDescriptor()); - break; - case GETTER: { - var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo()); - propInfo.getter = method.getDescriptor(); - break; - } - case SETTER: { - var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo()); - propInfo.setter = method.getDescriptor(); - break; - } - } + writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent(); + writer.append("let c;").softNewLine(); + for (var className : classSource.getClassNames()) { + var classReader = classSource.get(className); + var hasExportedMembers = false; + hasExportedMembers |= exportClassInstanceMembers(classReader); + if (!className.equals(context.getEntryPoint())) { + hasExportedMembers |= exportClassStaticMembers(classReader); + if (hasExportedMembers && !typeHelper.isJavaScriptClass(className) + && !typeHelper.isJavaScriptImplementation(className)) { + exportClassFromModule(classReader); } } - - var isJsClassImpl = typeHelper.isJavaScriptImplementation(className); - if (methods.isEmpty() && properties.isEmpty() && !isJsClassImpl) { - continue; - } - - writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;") - .softNewLine(); - if (isJsClassImpl) { - writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;") - .softNewLine(); - } - - for (var aliasEntry : methods.entrySet()) { - if (classReader.getMethod(aliasEntry.getValue()) == null) { - continue; - } - if (isKeyword(aliasEntry.getKey())) { - writer.append("c[\"").append(aliasEntry.getKey()).append("\"]"); - } else { - writer.append("c.").append(aliasEntry.getKey()); - } - writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue()) - .append(";").softNewLine(); - } - for (var aliasEntry : properties.entrySet()) { - var propInfo = aliasEntry.getValue(); - if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) { - continue; - } - writer.append("Object.defineProperty(c,") - .ws().append("\"").append(aliasEntry.getKey()).append("\",") - .ws().append("{").indent().softNewLine(); - writer.append("get:").ws().append("c.").appendVirtualMethod(propInfo.getter); - if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) { - writer.append(",").softNewLine(); - writer.append("set:").ws().append("c.").appendVirtualMethod(propInfo.setter); - } - writer.softNewLine().outdent().append("});").softNewLine(); - } - - FieldReader functorField = getFunctorField(classReader); - if (functorField != null) { - writeFunctor(classReader, functorField.getReference()); - } } writer.outdent().append("})();").newLine(); } + private boolean exportClassInstanceMembers(ClassReader classReader) { + var members = collectMembers(classReader, method -> !method.hasModifier(ElementModifier.STATIC)); + + var isJsClassImpl = typeHelper.isJavaScriptImplementation(classReader.getName()); + if (members.methods.isEmpty() && members.properties.isEmpty() && !isJsClassImpl) { + return false; + } + + writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(".prototype;") + .softNewLine(); + if (isJsClassImpl) { + writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;") + .softNewLine(); + } + + for (var aliasEntry : members.methods.entrySet()) { + if (classReader.getMethod(aliasEntry.getValue()) == null) { + continue; + } + appendMethodAlias(aliasEntry.getKey()); + writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue()) + .append(";").softNewLine(); + } + for (var aliasEntry : members.properties.entrySet()) { + var propInfo = aliasEntry.getValue(); + if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) { + continue; + } + appendPropertyAlias(aliasEntry.getKey()); + writer.append("get:").ws().append("c.").appendVirtualMethod(propInfo.getter); + if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) { + writer.append(",").softNewLine(); + writer.append("set:").ws().append("c.").appendVirtualMethod(propInfo.setter); + } + writer.softNewLine().outdent().append("});").softNewLine(); + } + + var functorField = getFunctorField(classReader); + if (functorField != null) { + writeFunctor(classReader, functorField.getReference()); + } + + return true; + } + + private boolean exportClassStaticMembers(ClassReader classReader) { + var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC)); + + if (members.methods.isEmpty() && members.properties.isEmpty()) { + return false; + } + + writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(";").softNewLine(); + + for (var aliasEntry : members.methods.entrySet()) { + appendMethodAlias(aliasEntry.getKey()); + var fullRef = new MethodReference(classReader.getName(), aliasEntry.getValue()); + writer.ws().append("=").ws().appendMethod(fullRef).append(";").softNewLine(); + } + for (var aliasEntry : members.properties.entrySet()) { + var propInfo = aliasEntry.getValue(); + if (propInfo.getter == null) { + continue; + } + appendPropertyAlias(aliasEntry.getKey()); + var fullGetter = new MethodReference(classReader.getName(), propInfo.getter); + writer.append("get:").ws().appendMethod(fullGetter); + if (propInfo.setter != null) { + writer.append(",").softNewLine(); + var fullSetter = new MethodReference(classReader.getName(), propInfo.setter); + writer.append("set:").ws().appendMethod(fullSetter); + } + writer.softNewLine().outdent().append("});").softNewLine(); + } + + return true; + } + + private void appendMethodAlias(String name) { + if (isKeyword(name)) { + writer.append("c[\"").append(name).append("\"]"); + } else { + writer.append("c.").append(name); + } + } + + private void appendPropertyAlias(String name) { + writer.append("Object.defineProperty(c,") + .ws().append("\"").append(name).append("\",") + .ws().append("{").indent().softNewLine(); + } + + private Members collectMembers(ClassReader classReader, Predicate filter) { + var methods = new HashMap(); + var properties = new HashMap(); + for (var method : classReader.getMethods()) { + if (!filter.test(method)) { + continue; + } + var methodAlias = getPublicAlias(method); + if (methodAlias != null) { + switch (methodAlias.kind) { + case METHOD: + methods.put(methodAlias.name, method.getDescriptor()); + break; + case GETTER: { + var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo()); + propInfo.getter = method.getDescriptor(); + break; + } + case SETTER: { + var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo()); + propInfo.setter = method.getDescriptor(); + break; + } + } + } + } + return new Members(methods, properties); + } + + private void exportModule() { + var cls = classSource.get(context.getEntryPoint()); + for (var method : cls.getMethods()) { + if (!method.hasModifier(ElementModifier.STATIC)) { + continue; + } + var methodAlias = getPublicAlias(method); + if (methodAlias != null && methodAlias.kind == AliasKind.METHOD) { + context.exportMethod(method.getReference(), methodAlias.name); + } + } + } + + private void exportClassFromModule(ClassReader cls) { + var name = cls.getSimpleName(); + if (name == null) { + name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1); + } + var jsExport = cls.getAnnotations().get(JSClass.class.getName()); + if (jsExport != null) { + var nameValue = jsExport.getValue("name"); + if (nameValue != null) { + var nameValueString = nameValue.getString(); + if (!nameValueString.isEmpty()) { + name = nameValueString; + } + } + } + context.exportClass(cls.getName(), name); + } + private boolean hasClassesToExpose() { for (String className : classSource.getClassNames()) { ClassReader cls = classSource.get(className); - if (cls.getMethods().stream().anyMatch(method -> getPublicAlias(method) != null) - || typeHelper.isJavaScriptImplementation(className)) { + if (typeHelper.isJavaScriptImplementation(className)) { return true; } + for (var method : cls.getMethods()) { + if (!method.hasModifier(ElementModifier.STATIC) && getPublicAlias(method) != null) { + return true; + } + } } return false; } @@ -242,12 +351,22 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { return methodReader != null && getPublicAlias(methodReader) != null; } - static class PropertyInfo { + private static class Members { + final Map methods; + final Map properties; + + Members(Map methods, Map properties) { + this.methods = methods; + this.properties = properties; + } + } + + private static class PropertyInfo { MethodDescriptor getter; MethodDescriptor setter; } - static class Alias { + private static class Alias { final String name; final AliasKind kind; @@ -257,7 +376,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor { } } - enum AliasKind { + private enum AliasKind { METHOD, GETTER, SETTER diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java index d1e6d9f2f..cbf8bb0e9 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java @@ -18,11 +18,14 @@ package org.teavm.jso.impl; import org.teavm.dependency.AbstractDependencyListener; import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.MethodDependency; +import org.teavm.jso.JSExportClasses; import org.teavm.model.AnnotationReader; import org.teavm.model.CallLocation; import org.teavm.model.ClassReader; +import org.teavm.model.ElementModifier; import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; +import org.teavm.model.ValueType; class JSDependencyListener extends AbstractDependencyListener { private JSBodyRepository repository; @@ -55,8 +58,22 @@ class JSDependencyListener extends AbstractDependencyListener { } if (exposeAnnot != null) { MethodDependency methodDep = agent.linkMethod(method.getReference()); - methodDep.getVariable(0).propagate(agent.getType(className)); - methodDep.use(); + if (methodDep.getMethod() != null) { + if (!methodDep.getMethod().hasModifier(ElementModifier.STATIC)) { + methodDep.getVariable(0).propagate(agent.getType(className)); + } + methodDep.use(); + } + } + } + + var exportClassesAnnot = cls.getAnnotations().get(JSExportClasses.class.getName()); + if (exportClassesAnnot != null) { + for (var classRef : exportClassesAnnot.getValue("value").getList()) { + if (classRef.getJavaClass() instanceof ValueType.Object) { + var classRefName = ((ValueType.Object) classRef.getJavaClass()).getClassName(); + agent.linkClass(classRefName); + } } } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java index 9fe1e9f57..dec9d8b6c 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java @@ -23,12 +23,12 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.teavm.diagnostics.Diagnostics; +import org.teavm.jso.JSExport; import org.teavm.jso.JSMethod; import org.teavm.jso.JSObject; import org.teavm.jso.JSProperty; import org.teavm.model.AccessLevel; import org.teavm.model.AnnotationHolder; -import org.teavm.model.AnnotationReader; import org.teavm.model.AnnotationValue; import org.teavm.model.BasicBlock; import org.teavm.model.CallLocation; @@ -99,6 +99,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer { } exposeMethods(cls, exposedClass, context.getDiagnostics(), functorMethod); + exportStaticMethods(cls, context.getDiagnostics()); } private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics, @@ -156,21 +157,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer { classHolder.addMethod(exportedMethod); var export = classToExpose.methods.get(method); - String annotationName; - switch (export.kind) { - case GETTER: - annotationName = JSGetterToExpose.class.getName(); - break; - case SETTER: - annotationName = JSSetterToExpose.class.getName(); - break; - default: - annotationName = JSMethodToExpose.class.getName(); - break; - } - AnnotationHolder annot = new AnnotationHolder(annotationName); - annot.getValues().put("name", new AnnotationValue(export.alias)); - exportedMethod.getAnnotations().add(annot); + exportedMethod.getAnnotations().add(createExportAnnotation(export)); if (methodRef.equals(functorMethod)) { addFunctorField(classHolder, exportedMethod.getReference()); @@ -178,6 +165,85 @@ class JSObjectClassTransformer implements ClassHolderTransformer { } } + private void exportStaticMethods(ClassHolder classHolder, Diagnostics diagnostics) { + int index = 0; + for (var method : classHolder.getMethods().toArray(new MethodHolder[0])) { + if (!method.hasModifier(ElementModifier.STATIC) + || method.getAnnotations().get(JSExport.class.getName()) == null) { + continue; + } + + var callLocation = new CallLocation(method.getReference()); + var exportedMethodSignature = Arrays.stream(method.getSignature()) + .map(type -> ValueType.object(JSObject.class.getName())) + .toArray(ValueType[]::new); + var exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++, + exportedMethodSignature); + var exportedMethod = new MethodHolder(exportedMethodDesc); + exportedMethod.getModifiers().add(ElementModifier.STATIC); + var program = new Program(); + program.createVariable(); + exportedMethod.setProgram(program); + + var basicBlock = program.createBasicBlock(); + var marshallInstructions = new ArrayList(); + var marshaller = new JSValueMarshaller(diagnostics, typeHelper, hierarchy.getClassSource(), + program, marshallInstructions); + + var variablesToPass = new Variable[method.parameterCount()]; + for (int i = 0; i < method.parameterCount(); ++i) { + variablesToPass[i] = program.createVariable(); + } + + for (int i = 0; i < method.parameterCount(); ++i) { + variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i], + method.parameterType(i), false, true); + } + + basicBlock.addAll(marshallInstructions); + marshallInstructions.clear(); + + var invocation = new InvokeInstruction(); + invocation.setType(InvocationType.SPECIAL); + invocation.setMethod(method.getReference()); + invocation.setArguments(variablesToPass); + basicBlock.add(invocation); + + var exit = new ExitInstruction(); + if (method.getResultType() != ValueType.VOID) { + invocation.setReceiver(program.createVariable()); + exit.setValueToReturn(marshaller.wrapArgument(callLocation, invocation.getReceiver(), + method.getResultType(), JSType.MIXED, false)); + basicBlock.addAll(marshallInstructions); + marshallInstructions.clear(); + } + basicBlock.add(exit); + + classHolder.addMethod(exportedMethod); + + var export = createMethodExport(method); + exportedMethod.getAnnotations().add(createExportAnnotation(export)); + } + } + + private AnnotationHolder createExportAnnotation(MethodExport export) { + String annotationName; + switch (export.kind) { + case GETTER: + annotationName = JSGetterToExpose.class.getName(); + break; + case SETTER: + annotationName = JSSetterToExpose.class.getName(); + break; + default: + annotationName = JSMethodToExpose.class.getName(); + break; + } + var annot = new AnnotationHolder(annotationName); + annot.getValues().put("name", new AnnotationValue(export.alias)); + return annot; + } + private ExposedClass getExposedClass(String name) { ExposedClass cls = exposedClasses.get(name); if (cls == null) { @@ -206,7 +272,9 @@ class JSObjectClassTransformer implements ClassHolderTransformer { exposedCls.inheritedMethods.addAll(parent.methods.keySet()); exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces); } - addInterfaces(exposedCls, cls); + if (!addInterfaces(exposedCls, cls)) { + addExportedMethods(exposedCls, cls); + } } private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) { @@ -226,58 +294,10 @@ class JSObjectClassTransformer implements ClassHolderTransformer { || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) { continue; } - if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) { - String name = null; - MethodKind kind = MethodKind.METHOD; - AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); - if (methodAnnot != null) { - name = method.getName(); - AnnotationValue nameVal = methodAnnot.getValue("value"); - if (nameVal != null) { - String nameStr = nameVal.getString(); - if (!nameStr.isEmpty()) { - name = nameStr; - } - } - } else { - var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); - if (propertyAnnot != null) { - AnnotationValue nameVal = propertyAnnot.getValue("value"); - if (nameVal != null) { - String nameStr = nameVal.getString(); - if (!nameStr.isEmpty()) { - name = nameStr; - } - } - String expectedPrefix; - if (method.parameterCount() == 0) { - if (method.getResultType() == ValueType.BOOLEAN) { - expectedPrefix = "is"; - } else { - expectedPrefix = "get"; - } - kind = MethodKind.GETTER; - } else { - expectedPrefix = "set"; - kind = MethodKind.SETTER; - } - - if (name == null) { - name = method.getName(); - if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length() - && Character.isUpperCase(name.charAt(expectedPrefix.length()))) { - name = Character.toLowerCase(name.charAt(expectedPrefix.length())) - + name.substring(expectedPrefix.length() + 1); - } - } - } - } - if (name == null) { - name = method.getName(); - } - exposedCls.methods.put(method.getDescriptor(), new MethodExport(name, kind)); - } + addExportedMethod(exposedCls, method); } + } else { + addExportedMethods(exposedCls, iface); } } return added; @@ -290,6 +310,75 @@ class JSObjectClassTransformer implements ClassHolderTransformer { return addInterfaces(exposedCls, cls); } + private void addExportedMethods(ExposedClass exposedCls, ClassReader cls) { + for (var method : cls.getMethods()) { + if (method.hasModifier(ElementModifier.STATIC)) { + continue; + } + if (method.getAnnotations().get(JSExport.class.getName()) != null) { + addExportedMethod(exposedCls, method); + } + } + } + + private void addExportedMethod(ExposedClass exposedCls, MethodReader method) { + if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) { + exposedCls.methods.put(method.getDescriptor(), createMethodExport(method)); + } + } + + private MethodExport createMethodExport(MethodReader method) { + String name = null; + MethodKind kind = MethodKind.METHOD; + var methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); + if (methodAnnot != null) { + name = method.getName(); + var nameVal = methodAnnot.getValue("value"); + if (nameVal != null) { + String nameStr = nameVal.getString(); + if (!nameStr.isEmpty()) { + name = nameStr; + } + } + } else { + var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName()); + if (propertyAnnot != null) { + var nameVal = propertyAnnot.getValue("value"); + if (nameVal != null) { + String nameStr = nameVal.getString(); + if (!nameStr.isEmpty()) { + name = nameStr; + } + } + String expectedPrefix; + if (method.parameterCount() == 0) { + if (method.getResultType() == ValueType.BOOLEAN) { + expectedPrefix = "is"; + } else { + expectedPrefix = "get"; + } + kind = MethodKind.GETTER; + } else { + expectedPrefix = "set"; + kind = MethodKind.SETTER; + } + + if (name == null) { + name = method.getName(); + if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length() + && Character.isUpperCase(name.charAt(expectedPrefix.length()))) { + name = Character.toLowerCase(name.charAt(expectedPrefix.length())) + + name.substring(expectedPrefix.length() + 1); + } + } + } + } + if (name == null) { + name = method.getName(); + } + return new MethodExport(name, kind); + } + private void addFunctorField(ClassHolder cls, MethodReference method) { if (cls.getAnnotations().get(FunctorImpl.class.getName()) != null) { return; diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java index dcb595d74..6c3812798 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -127,6 +127,17 @@ class JSValueMarshaller { } } if (!className.equals("java.lang.String")) { + if (!typeHelper.isJavaScriptClass(className) && !typeHelper.isJavaScriptImplementation(className)) { + var unwrapNative = new InvokeInstruction(); + unwrapNative.setLocation(location); + unwrapNative.setType(InvocationType.SPECIAL); + unwrapNative.setMethod(new MethodReference(JSWrapper.class, + "dependencyJavaToJs", Object.class, JSObject.class)); + unwrapNative.setArguments(var); + unwrapNative.setReceiver(program.createVariable()); + replacement.add(unwrapNative); + return unwrapNative.getReceiver(); + } return var; } } @@ -317,6 +328,15 @@ class JSValueMarshaller { return unwrap(var, "unwrapString", JSMethods.JS_OBJECT, stringType, location.getSourceLocation()); } else if (typeHelper.isJavaScriptClass(className)) { return var; + } else { + var wrapNative = new InvokeInstruction(); + wrapNative.setLocation(location.getSourceLocation()); + wrapNative.setType(InvocationType.SPECIAL); + wrapNative.setMethod(LIGHTWEIGHT_JS_TO_JAVA); + wrapNative.setArguments(var); + wrapNative.setReceiver(program.createVariable()); + replacement.add(wrapNative); + return wrapNative.getReceiver(); } } else if (type instanceof ValueType.Array) { return unwrapArray(location, var, (ValueType.Array) type); diff --git a/samples/module-test/build.gradle.kts b/samples/module-test/build.gradle.kts new file mode 100644 index 000000000..5ec4a1f07 --- /dev/null +++ b/samples/module-test/build.gradle.kts @@ -0,0 +1,36 @@ +import org.teavm.gradle.api.JSModuleType +import org.teavm.gradle.api.OptimizationLevel + +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + java + war + id("org.teavm") +} + +dependencies { + teavm(teavm.libs.jsoApis) +} + +teavm.js { + addedToWebApp = true + mainClass = "org.teavm.samples.modules.SimpleModule" + moduleType = JSModuleType.ES2015 + obfuscated = false + outOfProcess = true +} diff --git a/samples/module-test/src/main/java/org/teavm/samples/modules/SimpleModule.java b/samples/module-test/src/main/java/org/teavm/samples/modules/SimpleModule.java new file mode 100644 index 000000000..83676fafc --- /dev/null +++ b/samples/module-test/src/main/java/org/teavm/samples/modules/SimpleModule.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.samples.modules; + +import org.teavm.jso.JSExport; + +public class SimpleModule { + static { + System.out.println("Module initialized"); + } + + @JSExport + public static void foo() { + System.out.println("Hello, world"); + } + + @JSExport + public static String bar(int a) { + return "bar: " + a + Integer.TYPE; + } +} diff --git a/samples/settings.gradle.kts b/samples/settings.gradle.kts index d8746e099..db11e7ea6 100644 --- a/samples/settings.gradle.kts +++ b/samples/settings.gradle.kts @@ -60,6 +60,7 @@ include("kotlin") include("scala") include("web-apis") include("software3d") +include("module-test") gradle.allprojects { apply() diff --git a/settings.gradle.kts b/settings.gradle.kts index 8a2aefaf8..ad4938d45 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ include("jso:core", "jso:apis", "jso:impl") include("platform") include("classlib") include("tools:core") +include("tools:browser-runner") include("tools:deobfuscator-js") include("tools:junit") include("tools:devserver") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 89d6f6f98..2c4ebcfb6 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { testImplementation(project(":metaprogramming:impl")) testImplementation(project(":tools:core")) testImplementation(project(":tools:junit")) + testImplementation(project(":tools:browser-runner")) testImplementation(libs.hppc) testImplementation(libs.rhino) testImplementation(libs.junit) diff --git a/tests/src/test/java/org/teavm/dependency/ClassValueTest.java b/tests/src/test/java/org/teavm/dependency/ClassValueTest.java index 4f3045803..f82b4ffc4 100644 --- a/tests/src/test/java/org/teavm/dependency/ClassValueTest.java +++ b/tests/src/test/java/org/teavm/dependency/ClassValueTest.java @@ -79,7 +79,7 @@ public class ClassValueTest { TeaVM vm = new TeaVMBuilder(target).build(); vm.add(new DependencyTestPatcher(getClass().getName(), methodName)); vm.installPlugins(); - vm.entryPoint(getClass().getName()); + vm.setEntryPoint(getClass().getName()); vm.build(fileName -> new ByteArrayOutputStream(), "tmp"); if (!vm.getProblemProvider().getSevereProblems().isEmpty()) { fail("Code compiled with errors:\n" + describeProblems(vm)); diff --git a/tests/src/test/java/org/teavm/dependency/DependencyTest.java b/tests/src/test/java/org/teavm/dependency/DependencyTest.java index 3af741a7b..46c19f82c 100644 --- a/tests/src/test/java/org/teavm/dependency/DependencyTest.java +++ b/tests/src/test/java/org/teavm/dependency/DependencyTest.java @@ -136,7 +136,7 @@ public class DependencyTest { MethodReference testMethod = new MethodReference(DependencyTestData.class, testName.getMethodName(), void.class); - vm.entryPoint(DependencyTestData.class.getName()); + vm.setEntryPoint(DependencyTestData.class.getName()); vm.build(fileName -> new ByteArrayOutputStream(), "out"); List problems = vm.getProblemProvider().getSevereProblems(); diff --git a/tests/src/test/java/org/teavm/incremental/IncrementalTest.java b/tests/src/test/java/org/teavm/incremental/IncrementalTest.java index d1a177ea2..cf05f2561 100644 --- a/tests/src/test/java/org/teavm/incremental/IncrementalTest.java +++ b/tests/src/test/java/org/teavm/incremental/IncrementalTest.java @@ -204,7 +204,7 @@ public class IncrementalTest { target.setObfuscated(false); target.setStrict(true); vm.add(new EntryPointTransformer(entryPoint)); - vm.entryPoint(EntryPoint.class.getName()); + vm.setEntryPoint(EntryPoint.class.getName()); vm.installPlugins(); vm.build(buildTarget, name); List problems = vm.getProblemProvider().getSevereProblems(); diff --git a/tests/src/test/java/org/teavm/jso/export/ExportTest.java b/tests/src/test/java/org/teavm/jso/export/ExportTest.java new file mode 100644 index 000000000..eee99995d --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ExportTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.teavm.backend.javascript.JSModuleType; +import org.teavm.backend.javascript.JavaScriptTarget; +import org.teavm.browserrunner.BrowserRunDescriptor; +import org.teavm.browserrunner.BrowserRunner; +import org.teavm.tooling.ConsoleTeaVMToolLog; +import org.teavm.tooling.TeaVMProblemRenderer; +import org.teavm.vm.TeaVMBuilder; +import org.teavm.vm.TeaVMOptimizationLevel; + +public class ExportTest { + private static File targetFile = new File(new File(System.getProperty("teavm.junit.target")), "jso-export"); + private static BrowserRunner runner = new BrowserRunner( + targetFile, + "JAVASCRIPT", + BrowserRunner.pickBrowser(System.getProperty("teavm.junit.js.runner")), + false + ); + + @BeforeClass + public static void start() { + runner.start(); + } + + @AfterClass + public static void stop() { + runner.stop(); + } + + @Test + public void simple() { + testExport("simple", SimpleModule.class); + } + + @Test + public void initializer() { + testExport("initializer", ModuleWithInitializer.class); + } + + @Test + public void primitives() { + testExport("primitives", ModuleWithPrimitiveTypes.class); + } + + @Test + public void exportClassMembers() { + testExport("exportClassMembers", ModuleWithExportedClassMembers.class); + } + + @Test + public void importClassMembers() { + testExport("importClassMembers", ModuleWithConsumedObject.class); + } + + @Test + public void exportClasses() { + testExport("exportClasses", ModuleWithExportedClasses.class); + } + + private void testExport(String name, Class moduleClass) { + if (!Boolean.parseBoolean(System.getProperty("teavm.junit.js", "true"))) { + return; + } + try { + var jsTarget = new JavaScriptTarget(); + jsTarget.setModuleType(JSModuleType.ES2015); + var teavm = new TeaVMBuilder(jsTarget).build(); + var outputDir = new File(targetFile, name); + teavm.installPlugins(); + teavm.setEntryPoint(moduleClass.getName()); + teavm.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); + outputDir.mkdirs(); + teavm.build(outputDir, "test.js"); + if (!teavm.getProblemProvider().getSevereProblems().isEmpty()) { + var log = new ConsoleTeaVMToolLog(false); + TeaVMProblemRenderer.describeProblems(teavm, log); + throw new RuntimeException("TeaVM compilation error"); + } + + var testRunnerFile = new File(outputDir, "runner.js"); + try (var writer = new OutputStreamWriter(new FileOutputStream(testRunnerFile), StandardCharsets.UTF_8)) { + writer.write("import { test } from '/resources/org/teavm/jso/export/" + name + ".js';\n"); + writer.write("export function main(args, callback) {\n"); + writer.write(" test().then(() => callback()).catch(e => callback(e));\n"); + writer.write("}\n"); + } + + var descriptor = new BrowserRunDescriptor(name, "tests/" + name + "/runner.js", true, + List.of("resources/org/teavm/jso/export/assert.js"), null); + runner.runTest(descriptor); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/org/teavm/vm/TeaVMEntryPoint.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithConsumedObject.java similarity index 53% rename from core/src/main/java/org/teavm/vm/TeaVMEntryPoint.java rename to tests/src/test/java/org/teavm/jso/export/ModuleWithConsumedObject.java index 4457a093a..301126c57 100644 --- a/core/src/main/java/org/teavm/vm/TeaVMEntryPoint.java +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithConsumedObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Alexey Andreev. + * Copyright 2024 Alexey Andreev. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.teavm.vm; +package org.teavm.jso.export; -import org.teavm.dependency.MethodDependency; -import org.teavm.model.MethodReference; +import org.teavm.jso.JSExport; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; -public class TeaVMEntryPoint { - String publicName; - MethodDependency methodDep; - - TeaVMEntryPoint(String publicName, MethodDependency methodDep) { - this.publicName = publicName; - this.methodDep = methodDep; +public final class ModuleWithConsumedObject { + private ModuleWithConsumedObject() { } - public String getPublicName() { - return publicName; + @JSExport + public static String takeObject(I o) { + return "object taken: foo = " + o.foo() + ", bar = " + o.getBar(); } - public MethodReference getMethod() { - return methodDep.getReference(); + public interface I extends JSObject { + int foo(); + + @JSProperty + String getBar(); } } diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java new file mode 100644 index 000000000..edf3965f1 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import org.teavm.jso.JSExport; +import org.teavm.jso.JSProperty; + +public final class ModuleWithExportedClassMembers { + private ModuleWithExportedClassMembers() { + } + + @JSExport + public static C createObject(String prefix) { + return new C(prefix); + } + + @JSExport + public static String consumeObject(C c) { + return "consumeObject:" + c.bar(); + } + + public static class C { + private String prefix; + + public C(String prefix) { + this.prefix = prefix; + } + + @JSExport + @JSProperty + public int getFoo() { + return 23; + } + + @JSExport + public String bar() { + return prefix + ":" + 42; + } + + @JSExport + public static int baz() { + return 99; + } + + @JSExport + @JSProperty + public static String staticProp() { + return "I'm static"; + } + } +} diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java new file mode 100644 index 000000000..3ec019e38 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClasses.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import org.teavm.jso.JSClass; +import org.teavm.jso.JSExport; +import org.teavm.jso.JSExportClasses; +import org.teavm.jso.JSProperty; + +@JSExportClasses({ ModuleWithExportedClasses.A.class, ModuleWithExportedClasses.B.class }) +public class ModuleWithExportedClasses { + public static class A { + @JSExport + public static int foo() { + return 23; + } + } + + @JSClass(name = "BB") + public static class B { + private int bar; + + public B(int bar) { + this.bar = bar; + } + + @JSExport + @JSProperty + public int getBar() { + return bar; + } + + @JSExport + public static B create(int bar) { + return new B(bar); + } + } +} diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithInitializer.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithInitializer.java new file mode 100644 index 000000000..e22201989 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithInitializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import org.teavm.jso.JSExport; + +public final class ModuleWithInitializer { + private static int count; + + static { + count += AnotherInitialier.count * 10; + } + + private ModuleWithInitializer() { + } + + @JSExport + public static String foo() { + return "foo"; + } + + @JSExport + public static String bar() { + return "bar"; + } + + @JSExport + public static int getCount() { + return count; + } + + @JSExport + public static int getAnotherCount() { + return AnotherInitialier.count; + } + + static class AnotherInitialier { + private static int count; + + static { + count += 1; + } + } +} diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java new file mode 100644 index 000000000..7b80b2597 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import org.teavm.jso.JSExport; + +public final class ModuleWithPrimitiveTypes { + private ModuleWithPrimitiveTypes() { + } + + @JSExport + public static boolean boolResult() { + return true; + } + + @JSExport + public static byte byteResult() { + return 1; + } + + @JSExport + public static short shortResult() { + return 2; + } + + @JSExport + public static int intResult() { + return 3; + } + + @JSExport + public static float floatResult() { + return 4.1f; + } + + @JSExport + public static double doubleResult() { + return 5.2f; + } + + @JSExport + public static String stringResult() { + return "q"; + } + + @JSExport + public static boolean[] boolArrayResult() { + return new boolean[] { true, false }; + } + + @JSExport + public static byte[] byteArrayResult() { + return new byte[] { 1, 2 }; + } + + @JSExport + public static short[] shortArrayResult() { + return new short[] { 2, 3 }; + } + + @JSExport + public static int[] intArrayResult() { + return new int[] { 3, 4 }; + } + + @JSExport + public static float[] floatArrayResult() { + return new float[] { 4f, 5f }; + } + + @JSExport + public static double[] doubleArrayResult() { + return new double[] { 5, 6 }; + } + + @JSExport + public static String[] stringArrayResult() { + return new String[] { "q", "w" }; + } + + @JSExport + public static String boolParam(boolean param) { + return "bool:" + param; + } + + @JSExport + public static String byteParam(byte param) { + return "byte:" + param; + } + + @JSExport + public static String shortParam(short param) { + return "short:" + param; + } + + @JSExport + public static String intParam(int param) { + return "int:" + param; + } + + @JSExport + public static String floatParam(float param) { + return "float:" + param; + } + + @JSExport + public static String doubleParam(double param) { + return "double:" + param; + } + + @JSExport + public static String stringParam(String param) { + return "string:" + param; + } +} diff --git a/tests/src/test/java/org/teavm/jso/export/SimpleModule.java b/tests/src/test/java/org/teavm/jso/export/SimpleModule.java new file mode 100644 index 000000000..7ec62be97 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/export/SimpleModule.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.export; + +import org.teavm.jso.JSExport; + +public final class SimpleModule { + private SimpleModule() { + } + + @JSExport + public static int foo() { + return 23; + } +} diff --git a/tests/src/test/java/org/teavm/tests/JSOTest.java b/tests/src/test/java/org/teavm/tests/JSOTest.java index 9ab21e624..84dc993bc 100644 --- a/tests/src/test/java/org/teavm/tests/JSOTest.java +++ b/tests/src/test/java/org/teavm/tests/JSOTest.java @@ -97,7 +97,7 @@ public class JSOTest { TeaVM vm = new TeaVMBuilder(new JavaScriptTarget()).build(); vm.add(new DependencyTestPatcher(JSOTest.class.getName(), methodName)); vm.installPlugins(); - vm.entryPoint(JSOTest.class.getName()); + vm.setEntryPoint(JSOTest.class.getName()); vm.build(name -> new ByteArrayOutputStream(), "tmp"); return vm.getProblemProvider().getSevereProblems(); } diff --git a/tests/src/test/resources/org/teavm/jso/export/assert.js b/tests/src/test/resources/org/teavm/jso/export/assert.js new file mode 100644 index 000000000..81ff8e234 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/assert.js @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function assertEquals(a, b) { + if (a == b) { + return + } + if (a instanceof Array && b instanceof Array && a.length === b.length) { + let allEqual = true; + for (let i = 0; i < a.length; ++i) { + if (a[i] != b[i]) { + allEqual = false; + } + } + if (allEqual) { + return; + } + } + throw Error(`Assertion failed: ${a} != ${b}`); +} + +function assertApproxEquals(a, b) { + if (Math.abs(a - b) > 0.01) { + throw Error(`Assertion failed: ${a} != ${b}`); + } +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js b/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js new file mode 100644 index 000000000..c31c8579b --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createObject, consumeObject, C } from '/tests/exportClassMembers/test.js'; + +export async function test() { + let o = createObject("qwe"); + assertEquals(23, o.foo); + assertEquals("qwe:42", o.bar()); + assertEquals("consumeObject:qwe:42", consumeObject(o)); + assertEquals(99, C.baz()); + assertEquals("I'm static", C.staticProp); +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/exportClasses.js b/tests/src/test/resources/org/teavm/jso/export/exportClasses.js new file mode 100644 index 000000000..647bbaf70 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/exportClasses.js @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { A, BB } from '/tests/exportClasses/test.js'; + +export async function test() { + assertEquals(23, A.foo()); + let o = BB.create(42); + assertEquals(true, o instanceof BB); + assertEquals(false, o instanceof A); + assertEquals(42, o.bar); +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/importClassMembers.js b/tests/src/test/resources/org/teavm/jso/export/importClassMembers.js new file mode 100644 index 000000000..f0056f744 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/importClassMembers.js @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { takeObject } from '/tests/importClassMembers/test.js'; + +export async function test() { + assertEquals("object taken: foo = 23, bar = qw", takeObject({ + foo: () => 23, + bar: "qw" + })); +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/initializer.js b/tests/src/test/resources/org/teavm/jso/export/initializer.js new file mode 100644 index 000000000..553fde9cf --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/initializer.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { foo, bar, getCount, getAnotherCount } from '/tests/initializer/test.js'; + +export async function test() { + assertEquals("foo", foo()); + assertEquals(1, getAnotherCount()); + assertEquals(10, getCount()); + assertEquals("bar", bar()); + assertEquals(1, getAnotherCount()); + assertEquals(10, getCount()); +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/primitives.js b/tests/src/test/resources/org/teavm/jso/export/primitives.js new file mode 100644 index 000000000..86f95061f --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/primitives.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as java from '/tests/primitives/test.js'; + +function testReturnPrimitives() { + assertEquals(true, java.boolResult()); + assertEquals(1, java.byteResult()); + assertEquals(2, java.shortResult()); + assertEquals(3, java.intResult()); + assertApproxEquals(4.1, java.floatResult()); + assertApproxEquals(5.2, java.doubleResult()); + assertEquals("q", java.stringResult()); +} + +function testReturnArrays() { + assertEquals([true, false], java.boolArrayResult()); + assertEquals([1, 2], java.byteArrayResult()); + assertEquals([2, 3], java.shortArrayResult()); + assertEquals([3, 4], java.intArrayResult()); + assertEquals([4, 5], java.floatArrayResult()); + assertEquals([5, 6], java.doubleArrayResult()); + assertEquals(["q", "w"], java.stringArrayResult()); +} + +function testConsumePrimitives() { + assertEquals("bool:true", java.boolParam(true)); + assertEquals("byte:1", java.byteParam(1)); + assertEquals("short:2", java.shortParam(2)); + assertEquals("int:3", java.intParam(3)); + assertEquals("float:4.0", java.floatParam(4)); + assertEquals("double:5.0", java.doubleParam(5)); + assertEquals("string:q", java.stringParam("q")); +} + +export async function test() { + testReturnPrimitives(); + testReturnArrays(); + testConsumePrimitives(); +} \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/simple.js b/tests/src/test/resources/org/teavm/jso/export/simple.js new file mode 100644 index 000000000..9cf0a6133 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/export/simple.js @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { foo } from '/tests/simple/test.js'; + +export async function test() { + assertEquals(23, foo()); +} \ No newline at end of file diff --git a/tools/browser-runner/build.gradle.kts b/tools/browser-runner/build.gradle.kts new file mode 100644 index 000000000..49c81255e --- /dev/null +++ b/tools/browser-runner/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + `java-library` + `teavm-publish` +} + +description = "Runs JS tests in the browser" + +configurations { + create("js") +} + +dependencies { + implementation(libs.jackson.annotations) + implementation(libs.jackson.databind) + implementation(libs.javax.servlet) + implementation(libs.jetty.server) + implementation(libs.jetty.websocket.server) + implementation(libs.jetty.websocket.client) + implementation(libs.jetty.websocket.client) + + "js"(project(":tools:deobfuscator-js", "js")) +} + +tasks.withType().configureEach { + if (name == "relocateJar") { + dependsOn(configurations["js"]) + from(project.provider { configurations["js"].map { zipTree(it) } }) { + include("deobfuscator-lib.js") + into("test-server") + rename { "deobfuscator.js" } + } + } +} + +teavmPublish { + artifactId = "teavm-browser-runner" +} \ No newline at end of file diff --git a/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunDescriptor.java b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunDescriptor.java new file mode 100644 index 000000000..81aaf7301 --- /dev/null +++ b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunDescriptor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.browserrunner; + +import java.util.Collection; + +public class BrowserRunDescriptor { + private final String name; + private final String testPath; + private final boolean module; + private final Collection additionalFiles; + private final String argument; + + public BrowserRunDescriptor(String name, String testPath, boolean module, Collection additionalFiles, + String argument) { + this.name = name; + this.testPath = testPath; + this.module = module; + this.additionalFiles = additionalFiles; + this.argument = argument; + } + + public String getName() { + return name; + } + + public String getTestPath() { + return testPath; + } + + public boolean isModule() { + return module; + } + + public Collection getAdditionalFiles() { + return additionalFiles; + } + + public String getArgument() { + return argument; + } +} diff --git a/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java new file mode 100644 index 000000000..687ce7b3b --- /dev/null +++ b/tools/browser-runner/src/main/java/org/teavm/browserrunner/BrowserRunner.java @@ -0,0 +1,526 @@ +/* + * Copyright 2021 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.browserrunner; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Function; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.api.WebSocketBehavior; +import org.eclipse.jetty.websocket.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +public class BrowserRunner { + private boolean decodeStack; + private final File baseDir; + private final String type; + private final Function browserRunner; + private Process browserProcess; + private Server server; + private int port; + private AtomicInteger idGenerator = new AtomicInteger(0); + private BlockingQueue wsSessionQueue = new LinkedBlockingQueue<>(); + private ConcurrentMap awaitingRuns = new ConcurrentHashMap<>(); + private ObjectMapper objectMapper = new ObjectMapper(); + + public BrowserRunner(File baseDir, String type, Function browserRunner, boolean decodeStack) { + this.baseDir = baseDir; + this.type = type; + this.browserRunner = browserRunner; + this.decodeStack = decodeStack; + } + + public static Function pickBrowser(String name) { + switch (name) { + case "browser": + return BrowserRunner::customBrowser; + case "browser-chrome": + return BrowserRunner::chromeBrowser; + case "browser-firefox": + return BrowserRunner::firefoxBrowser; + case "none": + return null; + default: + throw new RuntimeException("Unknown run strategy: " + name); + } + } + + public void start() { + runServer(); + browserProcess = browserRunner.apply("http://localhost:" + port + "/index.html"); + } + + public void stop() { + try { + server.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + if (browserProcess != null) { + browserProcess.destroy(); + } + } + + private void runServer() { + server = new Server(); + var connector = new ServerConnector(server); + server.addConnector(connector); + + var context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + server.setHandler(context); + + var servlet = new TestCodeServlet(); + + var servletHolder = new ServletHolder(servlet); + servletHolder.setAsyncSupported(true); + context.addServlet(servletHolder, "/*"); + + try { + server.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + port = connector.getLocalPort(); + } + + static class CallbackWrapper { + private final CountDownLatch latch; + volatile Throwable error; + volatile boolean shouldRepeat; + + CallbackWrapper(CountDownLatch latch) { + this.latch = latch; + } + + void complete() { + latch.countDown(); + } + + void error(Throwable e) { + error = e; + latch.countDown(); + } + + void repeat() { + latch.countDown(); + shouldRepeat = true; + } + } + + public void runTest(BrowserRunDescriptor run) throws IOException { + while (!runTestOnce(run)) { + // repeat + } + } + + private boolean runTestOnce(BrowserRunDescriptor run) { + Session ws; + try { + do { + ws = wsSessionQueue.poll(1, TimeUnit.SECONDS); + } while (ws == null || !ws.isOpen()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return true; + } + + int id = idGenerator.incrementAndGet(); + var latch = new CountDownLatch(1); + + var callbackWrapper = new CallbackWrapper(latch); + awaitingRuns.put(id, callbackWrapper); + + var nf = objectMapper.getNodeFactory(); + var node = nf.objectNode(); + node.set("id", nf.numberNode(id)); + + var array = nf.arrayNode(); + node.set("tests", array); + + var testNode = nf.objectNode(); + testNode.set("type", nf.textNode(type)); + testNode.set("name", nf.textNode(run.getName())); + + var fileNode = nf.objectNode(); + fileNode.set("path", nf.textNode(run.getTestPath())); + fileNode.set("type", nf.textNode(run.isModule() ? "module" : "regular")); + testNode.set("file", fileNode); + + if (!run.getAdditionalFiles().isEmpty()) { + var additionalJsJson = nf.arrayNode(); + for (var additionalFile : run.getAdditionalFiles()) { + var additionFileObj = nf.objectNode(); + additionFileObj.set("path", nf.textNode(additionalFile)); + additionFileObj.set("type", nf.textNode("regular")); + additionalJsJson.add(additionFileObj); + } + testNode.set("additionalFiles", additionalJsJson); + } + + if (run.getArgument() != null) { + testNode.set("argument", nf.textNode(run.getArgument())); + } + array.add(testNode); + + var message = node.toString(); + ws.getRemote().sendStringByFuture(message); + + try { + latch.await(); + } catch (InterruptedException e) { + // do nothing + } + + if (ws.isOpen()) { + wsSessionQueue.offer(ws); + } + + if (callbackWrapper.error != null) { + var err = callbackWrapper.error; + if (err instanceof RuntimeException) { + throw (RuntimeException) err; + } else { + throw new RuntimeException(err); + } + } + + return !callbackWrapper.shouldRepeat; + } + + class TestCodeServlet extends HttpServlet { + private WebSocketServletFactory wsFactory; + private Map contentCache = new ConcurrentHashMap<>(); + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + var wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER); + wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy); + wsFactory.setCreator((req, resp) -> new TestCodeSocket()); + try { + wsFactory.start(); + } catch (Exception e) { + throw new ServletException(e); + } + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + var path = req.getRequestURI(); + if (path != null) { + if (!path.startsWith("/")) { + path = "/" + path; + } + if (req.getMethod().equals("GET")) { + switch (path) { + case "/index.html": + case "/frame.html": { + var content = getFromCache(path, "true".equals(req.getParameter("logging"))); + if (content != null) { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/html"); + resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + resp.getOutputStream().flush(); + return; + } + break; + } + case "/client.js": + case "/frame.js": + case "/deobfuscator.js": { + var content = getFromCache(path, false); + if (content != null) { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/javascript"); + resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + resp.getOutputStream().flush(); + return; + } + break; + } + } + if (path.startsWith("/tests/")) { + var relPath = path.substring("/tests/".length()); + var file = new File(baseDir, relPath); + if (file.isFile()) { + resp.setStatus(HttpServletResponse.SC_OK); + if (file.getName().endsWith(".js")) { + resp.setContentType("application/javascript"); + } else if (file.getName().endsWith(".wasm")) { + resp.setContentType("application/wasm"); + } + try (var input = new FileInputStream(file)) { + input.transferTo(resp.getOutputStream()); + } + resp.getOutputStream().flush(); + } + } + if (path.startsWith("/resources/")) { + var relPath = path.substring("/resources/".length()); + var classLoader = BrowserRunner.class.getClassLoader(); + try (var input = classLoader.getResourceAsStream(relPath)) { + if (input != null) { + if (relPath.endsWith(".js")) { + resp.setContentType("application/javascript"); + } + resp.setStatus(HttpServletResponse.SC_OK); + input.transferTo(resp.getOutputStream()); + } else { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + resp.getOutputStream().flush(); + } + } + } + if (path.equals("/ws") && wsFactory.isUpgradeRequest(req, resp) + && (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted())) { + return; + } + } + + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + + private String getFromCache(String fileName, boolean logging) { + return contentCache.computeIfAbsent(fileName, fn -> { + var loader = BrowserRunner.class.getClassLoader(); + try (var input = loader.getResourceAsStream("test-server" + fn); + var reader = new InputStreamReader(input)) { + var sb = new StringBuilder(); + var buffer = new char[2048]; + while (true) { + int charsRead = reader.read(buffer); + if (charsRead < 0) { + break; + } + sb.append(buffer, 0, charsRead); + } + return sb.toString() + .replace("{{PORT}}", String.valueOf(port)) + .replace("\"{{LOGGING}}\"", String.valueOf(logging)) + .replace("\"{{DEOBFUSCATION}}\"", String.valueOf(decodeStack)); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + }); + } + } + + class TestCodeSocket extends WebSocketAdapter { + @Override + public void onWebSocketConnect(Session sess) { + wsSessionQueue.offer(sess); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + for (CallbackWrapper run : awaitingRuns.values()) { + run.repeat(); + } + } + + @Override + public void onWebSocketText(String message) { + JsonNode node; + try { + node = objectMapper.readTree(new StringReader(message)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + int id = node.get("id").asInt(); + var run = awaitingRuns.remove(id); + if (run == null) { + System.err.println("Unexpected run id: " + id); + return; + } + + JsonNode resultNode = node.get("result"); + + JsonNode log = resultNode.get("log"); + if (log != null) { + for (JsonNode logEntry : log) { + String str = logEntry.get("message").asText(); + switch (logEntry.get("type").asText()) { + case "stdout": + System.out.println(str); + break; + case "stderr": + System.err.println(str); + break; + } + } + } + + String status = resultNode.get("status").asText(); + if (status.equals("OK")) { + run.complete(); + } else { + run.error(new RuntimeException(resultNode.get("errorMessage").asText())); + } + } + } + + public static Process customBrowser(String url) { + System.out.println("Open link to run tests: " + url + "?logging=true"); + return null; + } + + public static Process chromeBrowser(String url) { + return browserTemplate("chrome", url, (profile, params) -> { + addChromeCommand(params); + params.addAll(Arrays.asList( + "--headless", + "--disable-gpu", + "--remote-debugging-port=9222", + "--no-first-run", + "--user-data-dir=" + profile + )); + }); + } + + public static Process firefoxBrowser(String url) { + return browserTemplate("firefox", url, (profile, params) -> { + addFirefoxCommand(params); + params.addAll(Arrays.asList( + "--headless", + "--profile", + profile + )); + }); + } + + private static void addChromeCommand(List params) { + if (isMacos()) { + params.add("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); + } else if (isWindows()) { + params.add("cmd.exe"); + params.add("start"); + params.add("/C"); + params.add("chrome"); + } else { + params.add("google-chrome-stable"); + } + } + + private static void addFirefoxCommand(List params) { + if (isMacos()) { + params.add("/Applications/Firefox.app/Contents/MacOS/firefox"); + return; + } + if (isWindows()) { + params.add("cmd.exe"); + params.add("/C"); + params.add("start"); + } + params.add("firefox"); + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().startsWith("windows"); + } + + private static boolean isMacos() { + return System.getProperty("os.name").toLowerCase().startsWith("mac"); + } + + private static Process browserTemplate(String name, String url, BiConsumer> paramsBuilder) { + File temp; + try { + temp = File.createTempFile("teavm", "teavm"); + temp.delete(); + temp.mkdirs(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteDir(temp))); + System.out.println("Running " + name + " with user data dir: " + temp.getAbsolutePath()); + List params = new ArrayList<>(); + paramsBuilder.accept(temp.getAbsolutePath(), params); + params.add(url); + ProcessBuilder pb = new ProcessBuilder(params.toArray(new String[0])); + Process process = pb.start(); + logStream(process.getInputStream(), name + " stdout"); + logStream(process.getErrorStream(), name + " stderr"); + new Thread(() -> { + try { + System.out.println(name + " process terminated with code: " + process.waitFor()); + } catch (InterruptedException e) { + // ignore + } + }); + return process; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void logStream(InputStream stream, String name) { + new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + System.out.println(name + ": " + line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + } + + private static void deleteDir(File dir) { + for (File file : dir.listFiles()) { + if (file.isDirectory()) { + deleteDir(file); + } else { + file.delete(); + } + } + dir.delete(); + } +} diff --git a/tools/junit/src/main/resources/test-server/client.js b/tools/browser-runner/src/main/resources/test-server/client.js similarity index 100% rename from tools/junit/src/main/resources/test-server/client.js rename to tools/browser-runner/src/main/resources/test-server/client.js diff --git a/tools/junit/src/main/resources/test-server/frame.html b/tools/browser-runner/src/main/resources/test-server/frame.html similarity index 100% rename from tools/junit/src/main/resources/test-server/frame.html rename to tools/browser-runner/src/main/resources/test-server/frame.html diff --git a/tools/junit/src/main/resources/test-server/frame.js b/tools/browser-runner/src/main/resources/test-server/frame.js similarity index 98% rename from tools/junit/src/main/resources/test-server/frame.js rename to tools/browser-runner/src/main/resources/test-server/frame.js index b5a699f38..af187fb78 100644 --- a/tools/junit/src/main/resources/test-server/frame.js +++ b/tools/browser-runner/src/main/resources/test-server/frame.js @@ -85,7 +85,7 @@ function launchTest(argument, callback) { return teavmException; } let stack = ""; - let je = main.javaException(e); + let je = main.javaException ? main.javaException(e) : void 0; if (je && je.constructor.$meta) { stack = je.constructor.$meta.name + ": "; stack += je.getMessage(); diff --git a/tools/junit/src/main/resources/test-server/index.html b/tools/browser-runner/src/main/resources/test-server/index.html similarity index 100% rename from tools/junit/src/main/resources/test-server/index.html rename to tools/browser-runner/src/main/resources/test-server/index.html diff --git a/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java b/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java index 64200f0fe..3ed74b82e 100644 --- a/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java +++ b/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java @@ -346,7 +346,10 @@ public class IncrementalCBuilder { vm.installPlugins(); vm.setLastKnownClasses(lastReachedClasses); - vm.entryPoint(mainClass, mainFunctionName != null ? mainFunctionName : "main"); + vm.setEntryPoint(mainClass); + if (mainFunctionName != null) { + vm.setEntryPointName(mainFunctionName); + } log.info("Starting build"); progressListener.last = 0; diff --git a/tools/core/src/main/java/org/teavm/tooling/TeaVMTool.java b/tools/core/src/main/java/org/teavm/tooling/TeaVMTool.java index 421d6b913..033a3477d 100644 --- a/tools/core/src/main/java/org/teavm/tooling/TeaVMTool.java +++ b/tools/core/src/main/java/org/teavm/tooling/TeaVMTool.java @@ -457,8 +457,9 @@ public class TeaVMTool { for (ClassHolderTransformer transformer : resolveTransformers()) { vm.add(transformer); } - if (mainClass != null) { - vm.entryPoint(mainClass, entryPointName != null ? entryPointName : "main"); + vm.setEntryPoint(mainClass); + if (entryPointName != null) { + vm.setEntryPointName(entryPointName); } for (String className : classesToPreserve) { vm.preserveType(className); diff --git a/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java b/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java index eecc584fa..a24cac187 100644 --- a/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java +++ b/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java @@ -838,7 +838,7 @@ public class CodeServlet extends HttpServlet { vm.installPlugins(); vm.setLastKnownClasses(lastReachedClasses); - vm.entryPoint(mainClass); + vm.setEntryPoint(mainClass); log.info("Starting build"); progressListener.last = 0; diff --git a/tools/junit/build.gradle.kts b/tools/junit/build.gradle.kts index 7683d1b3a..638a49280 100644 --- a/tools/junit/build.gradle.kts +++ b/tools/junit/build.gradle.kts @@ -21,9 +21,6 @@ plugins { description = "Test runner for JUnit and TestNG annotations" -configurations { - create("js") -} dependencies { compileOnly(libs.junit) @@ -33,26 +30,7 @@ dependencies { implementation(project(":core")) implementation(project(":tools:core")) - implementation(libs.jackson.annotations) - implementation(libs.jackson.databind) - implementation(libs.javax.servlet) - implementation(libs.jetty.server) - implementation(libs.jetty.websocket.server) - implementation(libs.jetty.websocket.client) - implementation(libs.jetty.websocket.client) - - "js"(project(":tools:deobfuscator-js", "js")) -} - -tasks.withType().configureEach { - if (name == "relocateJar") { - dependsOn(configurations["js"]) - from(project.provider { configurations["js"].map { zipTree(it) } }) { - include("deobfuscator-lib.js") - into("test-server") - rename { "deobfuscator.js" } - } - } + implementation(project(":tools:browser-runner")) } teavmPublish { diff --git a/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java b/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java index 9d0dc54bd..ead2dad02 100644 --- a/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java +++ b/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java @@ -16,220 +16,52 @@ package org.teavm.junit; import static org.teavm.junit.PropertyNames.JS_DECODE_STACK; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; import java.util.function.Function; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; -import org.eclipse.jetty.websocket.api.WebSocketBehavior; -import org.eclipse.jetty.websocket.api.WebSocketPolicy; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import java.util.stream.Collectors; +import org.teavm.browserrunner.BrowserRunDescriptor; +import org.teavm.browserrunner.BrowserRunner; class BrowserRunStrategy implements TestRunStrategy { - private boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true")); - private final File baseDir; - private final String type; - private final Function browserRunner; - private Process browserProcess; - private Server server; - private int port; - private AtomicInteger idGenerator = new AtomicInteger(0); - private BlockingQueue wsSessionQueue = new LinkedBlockingQueue<>(); - private ConcurrentMap awaitingRuns = new ConcurrentHashMap<>(); - private ObjectMapper objectMapper = new ObjectMapper(); + private File baseDir; + private BrowserRunner runner; BrowserRunStrategy(File baseDir, String type, Function browserRunner) { this.baseDir = baseDir; - this.type = type; - this.browserRunner = browserRunner; + runner = new BrowserRunner(baseDir, type, browserRunner, + Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true"))); } @Override public void beforeAll() { - runServer(); - browserProcess = browserRunner.apply("http://localhost:" + port + "/index.html"); - } - - private void runServer() { - server = new Server(); - ServerConnector connector = new ServerConnector(server); - server.addConnector(connector); - - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.setContextPath("/"); - server.setHandler(context); - - TestCodeServlet servlet = new TestCodeServlet(); - - ServletHolder servletHolder = new ServletHolder(servlet); - servletHolder.setAsyncSupported(true); - context.addServlet(servletHolder, "/*"); - - try { - server.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } - port = connector.getLocalPort(); + runner.start(); } @Override public void afterAll() { - try { - server.stop(); - } catch (Exception e) { - e.printStackTrace(); - } - if (browserProcess != null) { - browserProcess.destroy(); - } - } - - static class CallbackWrapper implements TestRunCallback { - private final CountDownLatch latch; - volatile Throwable error; - volatile boolean shouldRepeat; - - CallbackWrapper(CountDownLatch latch) { - this.latch = latch; - } - - @Override - public void complete() { - latch.countDown(); - } - - @Override - public void error(Throwable e) { - error = e; - latch.countDown(); - } - - void repeat() { - latch.countDown(); - shouldRepeat = true; - } + runner.stop(); } @Override public void runTest(TestRun run) throws IOException { - while (!runTestOnce(run)) { - // repeat - } + var testFile = new File(run.getBaseDirectory(), run.getFileName()); + var testPath = baseDir.getAbsoluteFile().toPath().relativize(testFile.toPath()).toString(); + var descriptor = new BrowserRunDescriptor( + run.getFileName(), + "tests/" + testPath, + run.isModule(), + additionalJs(run).stream().map(p -> "resources/" + p).collect(Collectors.toList()), + run.getArgument() + ); + + runner.runTest(descriptor); } - private boolean runTestOnce(TestRun run) { - Session ws; - try { - do { - ws = wsSessionQueue.poll(1, TimeUnit.SECONDS); - } while (ws == null || !ws.isOpen()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return true; - } - - int id = idGenerator.incrementAndGet(); - var latch = new CountDownLatch(1); - - CallbackWrapper callbackWrapper = new CallbackWrapper(latch); - awaitingRuns.put(id, callbackWrapper); - - JsonNodeFactory nf = objectMapper.getNodeFactory(); - ObjectNode node = nf.objectNode(); - node.set("id", nf.numberNode(id)); - - ArrayNode array = nf.arrayNode(); - node.set("tests", array); - - File file = new File(run.getBaseDirectory(), run.getFileName()).getAbsoluteFile(); - String relPath = baseDir.getAbsoluteFile().toPath().relativize(file.toPath()).toString(); - ObjectNode testNode = nf.objectNode(); - testNode.set("type", nf.textNode(type)); - testNode.set("name", nf.textNode(run.getFileName())); - - var fileNode = nf.objectNode(); - fileNode.set("path", nf.textNode("tests/" + relPath)); - fileNode.set("type", nf.textNode(run.isModule() ? "module" : "regular")); - testNode.set("file", fileNode); - - var additionalJs = additionalJs(run); - if (additionalJs.length > 0) { - var additionalJsJson = nf.arrayNode(); - for (var additionalFile : additionalJs) { - var additionFileObj = nf.objectNode(); - additionFileObj.set("path", nf.textNode("resources/" + additionalFile)); - additionFileObj.set("type", nf.textNode("regular")); - additionalJsJson.add(additionFileObj); - } - testNode.set("additionalFiles", additionalJsJson); - } - - if (run.getArgument() != null) { - testNode.set("argument", nf.textNode(run.getArgument())); - } - array.add(testNode); - - String message = node.toString(); - ws.getRemote().sendStringByFuture(message); - - try { - latch.await(); - } catch (InterruptedException e) { - // do nothing - } - - if (ws.isOpen()) { - wsSessionQueue.offer(ws); - } - - if (callbackWrapper.error != null) { - var err = callbackWrapper.error; - if (err instanceof RuntimeException) { - throw (RuntimeException) err; - } else { - throw new RuntimeException(err); - } - } - - return !callbackWrapper.shouldRepeat; - } - - private String[] additionalJs(TestRun run) { + private Collection additionalJs(TestRun run) { var result = new LinkedHashSet(); var method = run.getMethod(); @@ -247,296 +79,6 @@ class BrowserRunStrategy implements TestRunStrategy { cls = cls.getSuperclass(); } - return result.toArray(new String[0]); - } - - class TestCodeServlet extends HttpServlet { - private WebSocketServletFactory wsFactory; - private Map contentCache = new ConcurrentHashMap<>(); - - @Override - public void init(ServletConfig config) throws ServletException { - super.init(config); - WebSocketPolicy wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER); - wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy); - wsFactory.setCreator((req, resp) -> new TestCodeSocket()); - try { - wsFactory.start(); - } catch (Exception e) { - throw new ServletException(e); - } - } - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { - String path = req.getRequestURI(); - if (path != null) { - if (!path.startsWith("/")) { - path = "/" + path; - } - if (req.getMethod().equals("GET")) { - switch (path) { - case "/index.html": - case "/frame.html": { - String content = getFromCache(path, "true".equals(req.getParameter("logging"))); - if (content != null) { - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("text/html"); - resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); - resp.getOutputStream().flush(); - return; - } - break; - } - case "/client.js": - case "/frame.js": - case "/deobfuscator.js": { - String content = getFromCache(path, false); - if (content != null) { - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("application/javascript"); - resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); - resp.getOutputStream().flush(); - return; - } - break; - } - } - if (path.startsWith("/tests/")) { - String relPath = path.substring("/tests/".length()); - File file = new File(baseDir, relPath); - if (file.isFile()) { - resp.setStatus(HttpServletResponse.SC_OK); - if (file.getName().endsWith(".js")) { - resp.setContentType("application/javascript"); - } else if (file.getName().endsWith(".wasm")) { - resp.setContentType("application/wasm"); - } - try (FileInputStream input = new FileInputStream(file)) { - input.transferTo(resp.getOutputStream()); - } - resp.getOutputStream().flush(); - } - } - if (path.startsWith("/resources/")) { - var relPath = path.substring("/resources/".length()); - var classLoader = BrowserRunStrategy.class.getClassLoader(); - try (var input = classLoader.getResourceAsStream(relPath)) { - if (input != null) { - resp.setStatus(HttpServletResponse.SC_OK); - input.transferTo(resp.getOutputStream()); - } else { - resp.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - resp.getOutputStream().flush(); - } - } - } - if (path.equals("/ws") && wsFactory.isUpgradeRequest(req, resp) - && (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted())) { - return; - } - } - - resp.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - - private String getFromCache(String fileName, boolean logging) { - return contentCache.computeIfAbsent(fileName, fn -> { - ClassLoader loader = BrowserRunStrategy.class.getClassLoader(); - try (InputStream input = loader.getResourceAsStream("test-server" + fn); - Reader reader = new InputStreamReader(input)) { - StringBuilder sb = new StringBuilder(); - char[] buffer = new char[2048]; - while (true) { - int charsRead = reader.read(buffer); - if (charsRead < 0) { - break; - } - sb.append(buffer, 0, charsRead); - } - return sb.toString() - .replace("{{PORT}}", String.valueOf(port)) - .replace("\"{{LOGGING}}\"", String.valueOf(logging)) - .replace("\"{{DEOBFUSCATION}}\"", String.valueOf(decodeStack)); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }); - } - } - - class TestCodeSocket extends WebSocketAdapter { - @Override - public void onWebSocketConnect(Session sess) { - wsSessionQueue.offer(sess); - } - - @Override - public void onWebSocketClose(int statusCode, String reason) { - for (CallbackWrapper run : awaitingRuns.values()) { - run.repeat(); - } - } - - @Override - public void onWebSocketText(String message) { - JsonNode node; - try { - node = objectMapper.readTree(new StringReader(message)); - } catch (IOException e) { - throw new RuntimeException(e); - } - - int id = node.get("id").asInt(); - TestRunCallback run = awaitingRuns.remove(id); - if (run == null) { - System.err.println("Unexpected run id: " + id); - return; - } - - JsonNode resultNode = node.get("result"); - - JsonNode log = resultNode.get("log"); - if (log != null) { - for (JsonNode logEntry : log) { - String str = logEntry.get("message").asText(); - switch (logEntry.get("type").asText()) { - case "stdout": - System.out.println(str); - break; - case "stderr": - System.err.println(str); - break; - } - } - } - - String status = resultNode.get("status").asText(); - if (status.equals("OK")) { - run.complete(); - } else { - run.error(new RuntimeException(resultNode.get("errorMessage").asText())); - } - } - } - - static Process customBrowser(String url) { - System.out.println("Open link to run tests: " + url + "?logging=true"); - return null; - } - - static Process chromeBrowser(String url) { - return browserTemplate("chrome", url, (profile, params) -> { - addChromeCommand(params); - params.addAll(Arrays.asList( - "--headless", - "--disable-gpu", - "--remote-debugging-port=9222", - "--no-first-run", - "--user-data-dir=" + profile - )); - }); - } - - static Process firefoxBrowser(String url) { - return browserTemplate("firefox", url, (profile, params) -> { - addFirefoxCommand(params); - params.addAll(Arrays.asList( - "--headless", - "--profile", - profile - )); - }); - } - - private static void addChromeCommand(List params) { - if (isMacos()) { - params.add("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); - } else if (isWindows()) { - params.add("cmd.exe"); - params.add("start"); - params.add("/C"); - params.add("chrome"); - } else { - params.add("google-chrome-stable"); - } - } - - private static void addFirefoxCommand(List params) { - if (isMacos()) { - params.add("/Applications/Firefox.app/Contents/MacOS/firefox"); - return; - } - if (isWindows()) { - params.add("cmd.exe"); - params.add("/C"); - params.add("start"); - } - params.add("firefox"); - } - - private static boolean isWindows() { - return System.getProperty("os.name").toLowerCase().startsWith("windows"); - } - - private static boolean isMacos() { - return System.getProperty("os.name").toLowerCase().startsWith("mac"); - } - - private static Process browserTemplate(String name, String url, BiConsumer> paramsBuilder) { - File temp; - try { - temp = File.createTempFile("teavm", "teavm"); - temp.delete(); - temp.mkdirs(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteDir(temp))); - System.out.println("Running " + name + " with user data dir: " + temp.getAbsolutePath()); - List params = new ArrayList<>(); - paramsBuilder.accept(temp.getAbsolutePath(), params); - params.add(url); - ProcessBuilder pb = new ProcessBuilder(params.toArray(new String[0])); - Process process = pb.start(); - logStream(process.getInputStream(), name + " stdout"); - logStream(process.getErrorStream(), name + " stderr"); - new Thread(() -> { - try { - System.out.println(name + " process terminated with code: " + process.waitFor()); - } catch (InterruptedException e) { - // ignore - } - }); - return process; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void logStream(InputStream stream, String name) { - new Thread(() -> { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - while (true) { - String line = reader.readLine(); - if (line == null) { - break; - } - System.out.println(name + ": " + line); - } - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); - } - - private static void deleteDir(File dir) { - for (File file : dir.listFiles()) { - if (file.isDirectory()) { - deleteDir(file); - } else { - file.delete(); - } - } - dir.delete(); + return result; } } diff --git a/tools/junit/src/main/java/org/teavm/junit/JSPlatformSupport.java b/tools/junit/src/main/java/org/teavm/junit/JSPlatformSupport.java index 275d7201a..c7d4a1d90 100644 --- a/tools/junit/src/main/java/org/teavm/junit/JSPlatformSupport.java +++ b/tools/junit/src/main/java/org/teavm/junit/JSPlatformSupport.java @@ -35,6 +35,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import org.teavm.backend.javascript.JSModuleType; import org.teavm.backend.javascript.JavaScriptTarget; +import org.teavm.browserrunner.BrowserRunner; import org.teavm.debugging.information.DebugInformation; import org.teavm.debugging.information.DebugInformationBuilder; import org.teavm.model.ClassHolderSource; @@ -49,22 +50,10 @@ class JSPlatformSupport extends TestPlatformSupport { @Override TestRunStrategy createRunStrategy(File outputDir) { - String runStrategyName = System.getProperty(JS_RUNNER); - if (runStrategyName != null) { - switch (runStrategyName) { - case "browser": - return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::customBrowser); - case "browser-chrome": - return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::chromeBrowser); - case "browser-firefox": - return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::firefoxBrowser); - case "none": - return null; - default: - throw new RuntimeException("Unknown run strategy: " + runStrategyName); - } - } - return null; + var runStrategyName = System.getProperty(JS_RUNNER); + return runStrategyName != null + ? new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunner.pickBrowser(runStrategyName)) + : null; } @Override diff --git a/tools/junit/src/main/java/org/teavm/junit/TestPlatformSupport.java b/tools/junit/src/main/java/org/teavm/junit/TestPlatformSupport.java index fcc315039..b6cc01e95 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestPlatformSupport.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestPlatformSupport.java @@ -93,7 +93,7 @@ abstract class TestPlatformSupport { new TestExceptionPlugin().install(vm); - vm.entryPoint(entryPoint); + vm.setEntryPoint(entryPoint); if (usesFileName()) { if (!outputFile.getParentFile().exists()) { diff --git a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyPlatformSupport.java b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyPlatformSupport.java index bca1b51d5..f64571953 100644 --- a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyPlatformSupport.java +++ b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyPlatformSupport.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import org.teavm.backend.wasm.WasmRuntimeType; import org.teavm.backend.wasm.WasmTarget; +import org.teavm.browserrunner.BrowserRunner; import org.teavm.model.ClassHolderSource; import org.teavm.model.MethodReference; import org.teavm.model.ReferenceCache; @@ -35,20 +36,9 @@ class WebAssemblyPlatformSupport extends BaseWebAssemblyPlatformSupport { @Override TestRunStrategy createRunStrategy(File outputDir) { var runStrategyName = System.getProperty(WASM_RUNNER); - if (runStrategyName != null) { - switch (runStrategyName) { - case "browser": - return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::customBrowser); - case "chrome": - case "browser-chrome": - return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::chromeBrowser); - case "browser-firefox": - return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::firefoxBrowser); - default: - throw new RuntimeException("Unknown run strategy: " + runStrategyName); - } - } - return null; + return runStrategyName != null + ? new BrowserRunStrategy(outputDir, "WASM", BrowserRunner.pickBrowser(runStrategyName)) + : null; } @Override