mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 00:04:10 -08:00
parent
cf850157f0
commit
8db406c603
|
@ -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<? extends ValueType> types) {
|
||||
Iterator<? extends TeaVMEntryPoint> 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<? extends TeaVMEntryPoint> 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
|
||||
|
|
|
@ -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<SourceWriter> name;
|
||||
final Consumer<NamingStrategy> nameFreq;
|
||||
final String alias;
|
||||
|
||||
public ExportedDeclaration(Consumer<SourceWriter> name, Consumer<NamingStrategy> nameFreq, String alias) {
|
||||
this.name = name;
|
||||
this.nameFreq = nameFreq;
|
||||
this.alias = alias;
|
||||
}
|
||||
}
|
|
@ -129,6 +129,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
|||
private final Map<String, String> importedModules = new LinkedHashMap<>();
|
||||
private JavaScriptTemplateFactory templateFactory;
|
||||
private JSModuleType moduleType = JSModuleType.UMD;
|
||||
private List<ExportedDeclaration> 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();
|
||||
}
|
||||
|
|
|
@ -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<ExportedDeclaration> exports;
|
||||
private String entryPoint;
|
||||
|
||||
public static final MethodDescriptor CLINIT_METHOD = new MethodDescriptor("<clinit>", ValueType.VOID);
|
||||
|
||||
public Renderer(SourceWriter writer, Set<MethodReference> asyncMethods, RenderingContext context,
|
||||
Diagnostics diagnostics, Map<MethodReference, Generator> generators,
|
||||
MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory) {
|
||||
MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory,
|
||||
List<ExportedDeclaration> 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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -47,6 +47,7 @@ class DependencyClassSource implements ClassHolderSource {
|
|||
Map<String, Optional<ClassHolder>> 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<ClassHolder> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,5 +29,7 @@ public interface ClassHolderTransformerContext {
|
|||
|
||||
boolean isStrict();
|
||||
|
||||
String getEntryPoint();
|
||||
|
||||
void submit(ClassHolder cls);
|
||||
}
|
||||
|
|
|
@ -50,14 +50,17 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo {
|
|||
private Map<MethodReference, MethodInfo> methodInfoMap = new HashMap<>();
|
||||
private ListableClassReaderSource classes;
|
||||
private ClassHierarchy hierarchy;
|
||||
private String entryPoint;
|
||||
private List<String> order = new ArrayList<>();
|
||||
private List<? extends String> 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) {
|
||||
|
|
|
@ -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("<clinit>", ValueType.VOID);
|
||||
|
||||
private final DependencyAnalyzer dependencyAnalyzer;
|
||||
private final AccumulationDiagnostics diagnostics = new AccumulationDiagnostics();
|
||||
private final ClassLoader classLoader;
|
||||
private final Map<String, TeaVMEntryPoint> entryPoints = new LinkedHashMap<>();
|
||||
private final Map<String, TeaVMEntryPoint> readonlyEntryPoints = Collections.unmodifiableMap(entryPoints);
|
||||
private String entryPoint;
|
||||
private String entryPointName = "main";
|
||||
private final Set<String> preservedClasses = new HashSet<>();
|
||||
private final Set<String> readonlyPreservedClasses = Collections.unmodifiableSet(preservedClasses);
|
||||
private final Map<Class<?>, 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, "<clinit>", 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<String, TeaVMEntryPoint> 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<String> 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
|
||||
|
|
|
@ -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<? extends String, ? extends TeaVMEntryPoint> getEntryPoints();
|
||||
String getEntryPoint();
|
||||
|
||||
String getEntryPointName();
|
||||
|
||||
Set<? extends String> getPreservedClasses();
|
||||
|
||||
|
|
27
jso/core/src/main/java/org/teavm/jso/JSClass.java
Normal file
27
jso/core/src/main/java/org/teavm/jso/JSClass.java
Normal file
|
@ -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 "";
|
||||
}
|
26
jso/core/src/main/java/org/teavm/jso/JSExport.java
Normal file
26
jso/core/src/main/java/org/teavm/jso/JSExport.java
Normal file
|
@ -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 {
|
||||
}
|
27
jso/core/src/main/java/org/teavm/jso/JSExportClasses.java
Normal file
27
jso/core/src/main/java/org/teavm/jso/JSExportClasses.java
Normal file
|
@ -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();
|
||||
}
|
|
@ -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<String, MethodDescriptor>();
|
||||
var properties = new HashMap<String, PropertyInfo>();
|
||||
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<MethodReader> filter) {
|
||||
var methods = new HashMap<String, MethodDescriptor>();
|
||||
var properties = new HashMap<String, PropertyInfo>();
|
||||
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<String, MethodDescriptor> methods;
|
||||
final Map<String, PropertyInfo> properties;
|
||||
|
||||
Members(Map<String, MethodDescriptor> methods, Map<String, PropertyInfo> 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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Instruction>();
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
|
|
36
samples/module-test/build.gradle.kts
Normal file
36
samples/module-test/build.gradle.kts
Normal file
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ include("kotlin")
|
|||
include("scala")
|
||||
include("web-apis")
|
||||
include("software3d")
|
||||
include("module-test")
|
||||
|
||||
gradle.allprojects {
|
||||
apply<WarPlugin>()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
||||
|
|
|
@ -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<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
||||
|
|
120
tests/src/test/java/org/teavm/jso/export/ExportTest.java
Normal file
120
tests/src/test/java/org/teavm/jso/export/ExportTest.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
28
tests/src/test/java/org/teavm/jso/export/SimpleModule.java
Normal file
28
tests/src/test/java/org/teavm/jso/export/SimpleModule.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
39
tests/src/test/resources/org/teavm/jso/export/assert.js
Normal file
39
tests/src/test/resources/org/teavm/jso/export/assert.js
Normal file
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
}));
|
||||
}
|
25
tests/src/test/resources/org/teavm/jso/export/initializer.js
Normal file
25
tests/src/test/resources/org/teavm/jso/export/initializer.js
Normal file
|
@ -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());
|
||||
}
|
52
tests/src/test/resources/org/teavm/jso/export/primitives.js
Normal file
52
tests/src/test/resources/org/teavm/jso/export/primitives.js
Normal file
|
@ -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();
|
||||
}
|
20
tests/src/test/resources/org/teavm/jso/export/simple.js
Normal file
20
tests/src/test/resources/org/teavm/jso/export/simple.js
Normal file
|
@ -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());
|
||||
}
|
53
tools/browser-runner/build.gradle.kts
Normal file
53
tools/browser-runner/build.gradle.kts
Normal file
|
@ -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<Jar>().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"
|
||||
}
|
|
@ -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<String> additionalFiles;
|
||||
private final String argument;
|
||||
|
||||
public BrowserRunDescriptor(String name, String testPath, boolean module, Collection<String> 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<String> getAdditionalFiles() {
|
||||
return additionalFiles;
|
||||
}
|
||||
|
||||
public String getArgument() {
|
||||
return argument;
|
||||
}
|
||||
}
|
|
@ -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<String, Process> browserRunner;
|
||||
private Process browserProcess;
|
||||
private Server server;
|
||||
private int port;
|
||||
private AtomicInteger idGenerator = new AtomicInteger(0);
|
||||
private BlockingQueue<Session> wsSessionQueue = new LinkedBlockingQueue<>();
|
||||
private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>();
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public BrowserRunner(File baseDir, String type, Function<String, Process> browserRunner, boolean decodeStack) {
|
||||
this.baseDir = baseDir;
|
||||
this.type = type;
|
||||
this.browserRunner = browserRunner;
|
||||
this.decodeStack = decodeStack;
|
||||
}
|
||||
|
||||
public static Function<String, Process> 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<String, String> 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<String> 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<String> 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<String, List<String>> 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<String> 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Jar>().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 {
|
||||
|
|
|
@ -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<String, Process> browserRunner;
|
||||
private Process browserProcess;
|
||||
private Server server;
|
||||
private int port;
|
||||
private AtomicInteger idGenerator = new AtomicInteger(0);
|
||||
private BlockingQueue<Session> wsSessionQueue = new LinkedBlockingQueue<>();
|
||||
private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>();
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
private File baseDir;
|
||||
private BrowserRunner runner;
|
||||
|
||||
BrowserRunStrategy(File baseDir, String type, Function<String, Process> 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<String> additionalJs(TestRun run) {
|
||||
var result = new LinkedHashSet<String>();
|
||||
|
||||
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<String, String> 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<String> 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<String> 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<String, List<String>> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<JavaScriptTarget> {
|
|||
|
||||
@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
|
||||
|
|
|
@ -93,7 +93,7 @@ abstract class TestPlatformSupport<T extends TeaVMTarget> {
|
|||
|
||||
new TestExceptionPlugin().install(vm);
|
||||
|
||||
vm.entryPoint(entryPoint);
|
||||
vm.setEntryPoint(entryPoint);
|
||||
|
||||
if (usesFileName()) {
|
||||
if (!outputFile.getParentFile().exists()) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user