mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 16:14:10 -08:00
Add JS test runner that runs tests right in the browser
This commit is contained in:
parent
8b4f401bcb
commit
61db54e848
|
@ -73,7 +73,7 @@ function launchTest(argument, callback) {
|
||||||
|
|
||||||
function buildErrorMessage(e) {
|
function buildErrorMessage(e) {
|
||||||
let stack = "";
|
let stack = "";
|
||||||
var je = main.javaException(e);
|
let je = main.javaException(e);
|
||||||
if (je && je.constructor.$meta) {
|
if (je && je.constructor.$meta) {
|
||||||
stack = je.constructor.$meta.name + ": ";
|
stack = je.constructor.$meta.name + ": ";
|
||||||
stack += je.getMessage();
|
stack += je.getMessage();
|
||||||
|
@ -85,8 +85,8 @@ function launchTest(argument, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchWasmTest(path, argument, callback) {
|
function launchWasmTest(path, argument, callback) {
|
||||||
var output = [];
|
let output = [];
|
||||||
var outputBuffer = "";
|
let outputBuffer = "";
|
||||||
|
|
||||||
function putwchar(charCode) {
|
function putwchar(charCode) {
|
||||||
if (charCode === 10) {
|
if (charCode === 10) {
|
||||||
|
|
|
@ -44,6 +44,12 @@
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.teavm</groupId>
|
||||||
|
<artifactId>teavm-jso-apis</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 Alexey Andreev.
|
* Copyright 2021 konsoletyper.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.devserver.deobfuscate;
|
package org.teavm.tooling.deobfuscate.js;
|
||||||
|
|
||||||
import org.teavm.jso.JSFunctor;
|
import org.teavm.jso.JSFunctor;
|
||||||
import org.teavm.jso.JSObject;
|
import org.teavm.jso.JSObject;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 Alexey Andreev.
|
* Copyright 2021 konsoletyper.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.devserver.deobfuscate;
|
package org.teavm.tooling.deobfuscate.js;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -26,6 +26,7 @@ import org.teavm.debugging.information.SourceLocation;
|
||||||
import org.teavm.jso.JSBody;
|
import org.teavm.jso.JSBody;
|
||||||
import org.teavm.jso.ajax.XMLHttpRequest;
|
import org.teavm.jso.ajax.XMLHttpRequest;
|
||||||
import org.teavm.jso.core.JSArray;
|
import org.teavm.jso.core.JSArray;
|
||||||
|
import org.teavm.jso.core.JSObjects;
|
||||||
import org.teavm.jso.core.JSRegExp;
|
import org.teavm.jso.core.JSRegExp;
|
||||||
import org.teavm.jso.core.JSString;
|
import org.teavm.jso.core.JSString;
|
||||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||||
|
@ -33,9 +34,16 @@ import org.teavm.jso.typedarrays.Int8Array;
|
||||||
import org.teavm.model.MethodReference;
|
import org.teavm.model.MethodReference;
|
||||||
|
|
||||||
public final class Deobfuscator {
|
public final class Deobfuscator {
|
||||||
private static final JSRegExp FRAME_PATTERN = JSRegExp.create("^ +at ([^(]+) *\\((.+):([0-9]+):([0-9]+)\\) *$");
|
private static final JSRegExp FRAME_PATTERN = JSRegExp.create(""
|
||||||
|
+ "(^ +at ([^(]+) *\\((.+):([0-9]+):([0-9]+)\\) *$)|"
|
||||||
|
+ "(^([^@]*)@(.+):([0-9]+):([0-9]+)$)");
|
||||||
|
private DebugInformation debugInformation;
|
||||||
|
private String classesFileName;
|
||||||
|
|
||||||
private Deobfuscator() {
|
public Deobfuscator(ArrayBuffer buffer, String classesFileName) throws IOException {
|
||||||
|
Int8Array array = Int8Array.create(buffer);
|
||||||
|
debugInformation = DebugInformation.read(new Int8ArrayInputStream(array));
|
||||||
|
this.classesFileName = classesFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
@ -52,17 +60,7 @@ public final class Deobfuscator {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void installDeobfuscator(ArrayBuffer buffer, String classesFileName) {
|
public Frame[] deobfuscate(String stack) {
|
||||||
Int8Array array = Int8Array.create(buffer);
|
|
||||||
DebugInformation debugInformation;
|
|
||||||
try {
|
|
||||||
debugInformation = DebugInformation.read(new Int8ArrayInputStream(array));
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeobfuscateFunction(stack -> {
|
|
||||||
List<Frame> frames = new ArrayList<>();
|
List<Frame> frames = new ArrayList<>();
|
||||||
for (String line : splitLines(stack)) {
|
for (String line : splitLines(stack)) {
|
||||||
JSArray<JSString> groups = FRAME_PATTERN.exec(JSString.valueOf(line));
|
JSArray<JSString> groups = FRAME_PATTERN.exec(JSString.valueOf(line));
|
||||||
|
@ -70,10 +68,15 @@ public final class Deobfuscator {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String functionName = groups.get(1).stringValue();
|
int groupOffset = 1;
|
||||||
String fileName = groups.get(2).stringValue();
|
if (JSObjects.isUndefined(groups.get(1))) {
|
||||||
int lineNumber = Integer.parseInt(groups.get(3).stringValue());
|
groupOffset = 6;
|
||||||
int columnNumber = Integer.parseInt(groups.get(4).stringValue());
|
}
|
||||||
|
|
||||||
|
String functionName = groups.get(1 + groupOffset).stringValue();
|
||||||
|
String fileName = groups.get(2 + groupOffset).stringValue();
|
||||||
|
int lineNumber = Integer.parseInt(groups.get(3 + groupOffset).stringValue());
|
||||||
|
int columnNumber = Integer.parseInt(groups.get(4 + groupOffset).stringValue());
|
||||||
List<Frame> framesPerLine = deobfuscateFrames(debugInformation, classesFileName, fileName,
|
List<Frame> framesPerLine = deobfuscateFrames(debugInformation, classesFileName, fileName,
|
||||||
lineNumber, columnNumber);
|
lineNumber, columnNumber);
|
||||||
if (framesPerLine == null) {
|
if (framesPerLine == null) {
|
||||||
|
@ -82,7 +85,18 @@ public final class Deobfuscator {
|
||||||
frames.addAll(framesPerLine);
|
frames.addAll(framesPerLine);
|
||||||
}
|
}
|
||||||
return frames.toArray(new Frame[0]);
|
return frames.toArray(new Frame[0]);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private static void installDeobfuscator(ArrayBuffer buffer, String classesFileName) {
|
||||||
|
Deobfuscator deobfuscator;
|
||||||
|
try {
|
||||||
|
deobfuscator = new Deobfuscator(buffer, classesFileName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeobfuscateFunction(deobfuscator::deobfuscate);
|
||||||
DeobfuscatorCallback callback = getCallback();
|
DeobfuscatorCallback callback = getCallback();
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.run();
|
callback.run();
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 Alexey Andreev.
|
* Copyright 2021 konsoletyper.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.devserver.deobfuscate;
|
package org.teavm.tooling.deobfuscate.js;
|
||||||
|
|
||||||
import org.teavm.jso.JSFunctor;
|
import org.teavm.jso.JSFunctor;
|
||||||
import org.teavm.jso.JSObject;
|
import org.teavm.jso.JSObject;
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.tooling.deobfuscate.js;
|
||||||
|
|
||||||
|
import org.teavm.jso.JSObject;
|
||||||
|
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||||
|
|
||||||
|
public interface DeobfuscatorJs extends JSObject {
|
||||||
|
DeobfuscateFunction create(ArrayBuffer buffer, String classesFileName);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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.tooling.deobfuscate.js;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.teavm.jso.JSBody;
|
||||||
|
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||||
|
|
||||||
|
public final class DeobfuscatorLib implements DeobfuscatorJs {
|
||||||
|
private DeobfuscatorLib() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeobfuscateFunction create(ArrayBuffer buffer, String classesFileName) {
|
||||||
|
try {
|
||||||
|
return new Deobfuscator(buffer, classesFileName)::deobfuscate;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
install(new DeobfuscatorLib());
|
||||||
|
}
|
||||||
|
|
||||||
|
@JSBody(params = "instance", script =
|
||||||
|
"deobfuscator.create = function(buffer, classesFileName) {"
|
||||||
|
+ "return instance.create(buffer, classesFileName);"
|
||||||
|
+ "}"
|
||||||
|
)
|
||||||
|
private static native void install(DeobfuscatorJs js);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 Alexey Andreev.
|
* Copyright 2021 konsoletyper.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.devserver.deobfuscate;
|
package org.teavm.tooling.deobfuscate.js;
|
||||||
|
|
||||||
import org.teavm.jso.JSObject;
|
import org.teavm.jso.JSObject;
|
||||||
import org.teavm.jso.JSProperty;
|
import org.teavm.jso.JSProperty;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 Alexey Andreev.
|
* Copyright 2021 Alexey Andreev.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.devserver.deobfuscate;
|
package org.teavm.tooling.deobfuscate.js;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import org.teavm.jso.typedarrays.Int8Array;
|
import org.teavm.jso.typedarrays.Int8Array;
|
|
@ -61,12 +61,6 @@
|
||||||
<artifactId>teavm-core</artifactId>
|
<artifactId>teavm-core</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.teavm</groupId>
|
|
||||||
<artifactId>teavm-jso-apis</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.teavm</groupId>
|
<groupId>org.teavm</groupId>
|
||||||
<artifactId>teavm-tooling</artifactId>
|
<artifactId>teavm-tooling</artifactId>
|
||||||
|
@ -134,7 +128,7 @@
|
||||||
<targetFileName>deobfuscator.js</targetFileName>
|
<targetFileName>deobfuscator.js</targetFileName>
|
||||||
<minifying>true</minifying>
|
<minifying>true</minifying>
|
||||||
<optimizationLevel>ADVANCED</optimizationLevel>
|
<optimizationLevel>ADVANCED</optimizationLevel>
|
||||||
<mainClass>org.teavm.devserver.deobfuscate.Deobfuscator</mainClass>
|
<mainClass>org.teavm.tooling.deobfuscate.js.Deobfuscator</mainClass>
|
||||||
<entryPointName>$teavm_deobfuscator</entryPointName>
|
<entryPointName>$teavm_deobfuscator</entryPointName>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
|
@ -50,6 +50,34 @@
|
||||||
<artifactId>htmlunit</artifactId>
|
<artifactId>htmlunit</artifactId>
|
||||||
<version>2.33</version>
|
<version>2.33</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||||
|
<artifactId>javax-websocket-server-impl</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||||
|
<artifactId>websocket-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -70,6 +98,41 @@
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-javadoc-plugin</artifactId>
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.teavm</groupId>
|
||||||
|
<artifactId>teavm-maven-plugin</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.teavm</groupId>
|
||||||
|
<artifactId>teavm-jso-impl</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.teavm</groupId>
|
||||||
|
<artifactId>teavm-classlib</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile-deobfuscator</id>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<configuration>
|
||||||
|
<targetDirectory>${project.build.directory}/classes/test-server</targetDirectory>
|
||||||
|
<targetFileName>deobfuscator.js</targetFileName>
|
||||||
|
<minifying>true</minifying>
|
||||||
|
<optimizationLevel>ADVANCED</optimizationLevel>
|
||||||
|
<mainClass>org.teavm.tooling.deobfuscate.js.DeobfuscatorLib</mainClass>
|
||||||
|
<entryPointName>deobfuscator</entryPointName>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
|
@ -0,0 +1,346 @@
|
||||||
|
/*
|
||||||
|
* 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.junit;
|
||||||
|
|
||||||
|
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.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
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 BrowserRunStrategy implements TestRunStrategy {
|
||||||
|
private boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.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 AtomicReference<Session> wsSession = new AtomicReference<>();
|
||||||
|
private CountDownLatch wsSessionReady = new CountDownLatch(1);
|
||||||
|
private ConcurrentMap<Integer, TestRun> awaitingRuns = new ConcurrentHashMap<>();
|
||||||
|
private ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
|
||||||
|
this.baseDir = baseDir;
|
||||||
|
this.type = type;
|
||||||
|
this.browserRunner = browserRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterAll() {
|
||||||
|
try {
|
||||||
|
server.stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
if (browserProcess != null) {
|
||||||
|
browserProcess.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeThread() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterThread() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runTest(TestRun run) throws IOException {
|
||||||
|
try {
|
||||||
|
while (!wsSessionReady.await(1L, TimeUnit.SECONDS)) {
|
||||||
|
// keep waiting
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
run.getCallback().error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Session ws = wsSession.get();
|
||||||
|
if (ws == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int id = idGenerator.incrementAndGet();
|
||||||
|
awaitingRuns.put(id, run);
|
||||||
|
|
||||||
|
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()));
|
||||||
|
testNode.set("file", nf.textNode("tests/" + relPath));
|
||||||
|
if (run.getArgument() != null) {
|
||||||
|
testNode.set("argument", nf.textNode(run.getArgument()));
|
||||||
|
}
|
||||||
|
array.add(testNode);
|
||||||
|
|
||||||
|
String message = node.toString();
|
||||||
|
ws.getRemote().sendStringByFuture(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ServletException, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)) {
|
||||||
|
copy(input, resp.getOutputStream());
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copy(InputStream input, OutputStream output) throws IOException {
|
||||||
|
byte[] buffer = new byte[2048];
|
||||||
|
while (true) {
|
||||||
|
int bytes = input.read(buffer);
|
||||||
|
if (bytes < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output.write(buffer, 0, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestCodeSocket extends WebSocketAdapter {
|
||||||
|
private AtomicBoolean ready = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketConnect(Session sess) {
|
||||||
|
if (wsSession.compareAndSet(null, sess)) {
|
||||||
|
ready.set(true);
|
||||||
|
wsSessionReady.countDown();
|
||||||
|
} else {
|
||||||
|
System.err.println("Link opened in multiple browsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketClose(int statusCode, String reason) {
|
||||||
|
if (ready.get()) {
|
||||||
|
System.err.println("Browser has disconnected");
|
||||||
|
for (TestRun run : awaitingRuns.values()) {
|
||||||
|
run.getCallback().error(new RuntimeException("Browser disconnected unexpectedly"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketText(String message) {
|
||||||
|
if (!ready.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode node;
|
||||||
|
try {
|
||||||
|
node = objectMapper.readTree(new StringReader(message));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = node.get("id").asInt();
|
||||||
|
TestRun 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.getCallback().complete();
|
||||||
|
} else {
|
||||||
|
run.getCallback().error(new RuntimeException(resultNode.get("errorMessage").asText()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,14 @@ class CRunStrategy implements TestRunStrategy {
|
||||||
this.compilerCommand = compilerCommand;
|
this.compilerCommand = compilerCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeAll() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterAll() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeThread() {
|
public void beforeThread() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,14 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
|
||||||
private ThreadLocal<HtmlPage> page = new ThreadLocal<>();
|
private ThreadLocal<HtmlPage> page = new ThreadLocal<>();
|
||||||
private int runs;
|
private int runs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeAll() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterAll() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeThread() {
|
public void beforeThread() {
|
||||||
init();
|
init();
|
||||||
|
|
|
@ -17,10 +17,12 @@ package org.teavm.junit;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
@ -164,7 +166,12 @@ public class TeaVMTestRunner extends Runner implements Filterable {
|
||||||
case "htmlunit":
|
case "htmlunit":
|
||||||
jsRunStrategy = new HtmlUnitRunStrategy();
|
jsRunStrategy = new HtmlUnitRunStrategy();
|
||||||
break;
|
break;
|
||||||
case "":
|
case "browser":
|
||||||
|
jsRunStrategy = new BrowserRunStrategy(outputDir, "JAVASCRIPT", this::customBrowser);
|
||||||
|
break;
|
||||||
|
case "browser-chrome":
|
||||||
|
jsRunStrategy = new BrowserRunStrategy(outputDir, "JAVASCRIPT", this::chromeBrowser);
|
||||||
|
break;
|
||||||
case "none":
|
case "none":
|
||||||
jsRunStrategy = null;
|
jsRunStrategy = null;
|
||||||
break;
|
break;
|
||||||
|
@ -180,6 +187,73 @@ public class TeaVMTestRunner extends Runner implements Filterable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Process customBrowser(String url) {
|
||||||
|
System.out.println("Open link to run tests: " + url + "?logging=true");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process chromeBrowser(String url) {
|
||||||
|
File temp;
|
||||||
|
try {
|
||||||
|
temp = File.createTempFile("teavm", "teavm");
|
||||||
|
temp.delete();
|
||||||
|
temp.mkdirs();
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
deleteDir(temp);
|
||||||
|
}));
|
||||||
|
System.out.println("Running chrome with user data dir: " + temp.getAbsolutePath());
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
"google-chrome-stable",
|
||||||
|
"--headless",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--remote-debugging-port=9222",
|
||||||
|
"--no-first-run",
|
||||||
|
"--user-data-dir=" + temp.getAbsolutePath(),
|
||||||
|
url
|
||||||
|
);
|
||||||
|
Process process = pb.start();
|
||||||
|
logStream(process.getInputStream(), "Chrome stdout");
|
||||||
|
logStream(process.getErrorStream(), "Chrome stderr");
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
System.out.println("Chrome process terminated with code: " + process.waitFor());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return process;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 void deleteDir(File dir) {
|
||||||
|
for (File file : dir.listFiles()) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
deleteDir(file);
|
||||||
|
} else {
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.delete();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Description getDescription() {
|
public Description getDescription() {
|
||||||
if (suiteDescription == null) {
|
if (suiteDescription == null) {
|
||||||
|
@ -707,7 +781,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return compile(configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js",
|
return compile(configuration, targetSupplier, TestJsEntryPoint.class.getName(), path, ".js",
|
||||||
postBuild, false, additionalProcessing, baseName);
|
postBuild, false, additionalProcessing, baseName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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.junit;
|
||||||
|
|
||||||
|
import org.teavm.jso.JSBody;
|
||||||
|
|
||||||
|
final class TestJsEntryPoint {
|
||||||
|
private TestJsEntryPoint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Throwable {
|
||||||
|
try {
|
||||||
|
TestEntryPoint.run(args.length > 0 ? args[0] : null);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
printStackTrace(e, sb);
|
||||||
|
saveJavaException(sb.toString());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printStackTrace(Throwable e, StringBuilder stream) {
|
||||||
|
stream.append(e.getClass().getName());
|
||||||
|
String message = e.getLocalizedMessage();
|
||||||
|
if (message != null) {
|
||||||
|
stream.append(": " + message);
|
||||||
|
}
|
||||||
|
stream.append("\n");
|
||||||
|
StackTraceElement[] stackTrace = e.getStackTrace();
|
||||||
|
if (stackTrace != null) {
|
||||||
|
for (StackTraceElement element : stackTrace) {
|
||||||
|
stream.append("\tat ");
|
||||||
|
stream.append(element).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.getCause() != null && e.getCause() != e) {
|
||||||
|
stream.append("Caused by: ");
|
||||||
|
printStackTrace(e.getCause(), stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JSBody(params = "e", script = "window.teavmException = e")
|
||||||
|
private static native void saveJavaException(String e);
|
||||||
|
}
|
|
@ -18,6 +18,10 @@ package org.teavm.junit;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
interface TestRunStrategy {
|
interface TestRunStrategy {
|
||||||
|
void beforeAll();
|
||||||
|
|
||||||
|
void afterAll();
|
||||||
|
|
||||||
void beforeThread();
|
void beforeThread();
|
||||||
|
|
||||||
void afterThread();
|
void afterThread();
|
||||||
|
|
|
@ -37,6 +37,11 @@ class TestRunner {
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
latch = new CountDownLatch(numThreads);
|
latch = new CountDownLatch(numThreads);
|
||||||
|
strategy.beforeAll();
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
strategy.afterAll();
|
||||||
|
}));
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; ++i) {
|
for (int i = 0; i < numThreads; ++i) {
|
||||||
Thread thread = new Thread(() -> {
|
Thread thread = new Thread(() -> {
|
||||||
strategy.beforeThread();
|
strategy.beforeThread();
|
||||||
|
|
146
tools/junit/src/main/resources/test-server/client.js
Normal file
146
tools/junit/src/main/resources/test-server/client.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let logging = false;
|
||||||
|
let deobfuscation = false;
|
||||||
|
deobfuscator();
|
||||||
|
|
||||||
|
function tryConnect() {
|
||||||
|
let ws = new WebSocket("ws://localhost:{{PORT}}/ws");
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
if (logging) {
|
||||||
|
console.log("Connection established");
|
||||||
|
}
|
||||||
|
listen(ws);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
ws.close();
|
||||||
|
setTimeout(() => {
|
||||||
|
tryConnect();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = err => {
|
||||||
|
if (logging) {
|
||||||
|
console.log("Could not connect WebSocket", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen(ws) {
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
let request = JSON.parse(event.data);
|
||||||
|
if (logging) {
|
||||||
|
console.log("Request #" + request.id + " received");
|
||||||
|
}
|
||||||
|
runTests(ws, request.id, request.tests, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests(ws, suiteId, tests, index) {
|
||||||
|
if (index === tests.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let test = tests[index];
|
||||||
|
runSingleTest(test, result => {
|
||||||
|
if (logging) {
|
||||||
|
console.log("Sending response #" + suiteId);
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
id: suiteId,
|
||||||
|
index: index,
|
||||||
|
result: result
|
||||||
|
}));
|
||||||
|
runTests(ws, suiteId, tests, index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastDeobfuscator = null;
|
||||||
|
let lastDeobfuscatorFile = null;
|
||||||
|
let lastDeobfuscatorPromise = null;
|
||||||
|
function runSingleTest(test, callback) {
|
||||||
|
if (logging) {
|
||||||
|
console.log("Running test " + test.name);
|
||||||
|
}
|
||||||
|
if (deobfuscation) {
|
||||||
|
const fileName = test.file + ".teavmdbg";
|
||||||
|
if (lastDeobfuscatorFile === fileName) {
|
||||||
|
if (lastDeobfuscatorPromise === null) {
|
||||||
|
runSingleTestWithDeobfuscator(test, lastDeobfuscator, callback);
|
||||||
|
} else {
|
||||||
|
lastDeobfuscatorPromise.then(value => {
|
||||||
|
runSingleTestWithDeobfuscator(test, value, callback);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastDeobfuscatorFile = fileName;
|
||||||
|
lastDeobfuscator = null;
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = "arraybuffer";
|
||||||
|
lastDeobfuscatorPromise = new Promise(resolve => {
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
const newDeobfuscator = xhr.status === 200
|
||||||
|
? deobfuscator.create(xhr.response, "http://localhost:{{PORT}}/" + test.file)
|
||||||
|
: null;
|
||||||
|
if (lastDeobfuscatorFile === fileName) {
|
||||||
|
lastDeobfuscator = newDeobfuscator;
|
||||||
|
lastDeobfuscatorPromise = null;
|
||||||
|
}
|
||||||
|
resolve(newDeobfuscator);
|
||||||
|
runSingleTestWithDeobfuscator(test, newDeobfuscator, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open("GET", fileName);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runSingleTestWithDeobfuscator(test, null, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSingleTestWithDeobfuscator(test, deobfuscator, callback) {
|
||||||
|
let iframe = document.createElement("iframe");
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
let handshakeListener = handshakeEvent => {
|
||||||
|
if (handshakeEvent.source !== iframe.contentWindow || handshakeEvent.data !== "ready") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.removeEventListener("message", handshakeListener);
|
||||||
|
|
||||||
|
let listener = event => {
|
||||||
|
if (event.source !== iframe.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.removeEventListener("message", listener);
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
callback(event.data);
|
||||||
|
};
|
||||||
|
window.addEventListener("message", listener);
|
||||||
|
|
||||||
|
iframe.contentWindow.$rt_decodeStack = deobfuscator;
|
||||||
|
iframe.contentWindow.postMessage(test, "*");
|
||||||
|
};
|
||||||
|
window.addEventListener("message", handshakeListener);
|
||||||
|
iframe.src = "about:blank";
|
||||||
|
iframe.src = "frame.html";
|
||||||
|
}
|
25
tools/junit/src/main/resources/test-server/frame.html
Normal file
25
tools/junit/src/main/resources/test-server/frame.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="frame.js"></script>
|
||||||
|
</head>
|
||||||
|
<body onload="start()">
|
||||||
|
</body>
|
||||||
|
</html>
|
190
tools/junit/src/main/resources/test-server/frame.js
Normal file
190
tools/junit/src/main/resources/test-server/frame.js
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.addEventListener("message", event => {
|
||||||
|
let request = event.data;
|
||||||
|
switch (request.type) {
|
||||||
|
case "JAVASCRIPT":
|
||||||
|
appendFiles([request.file], 0, () => {
|
||||||
|
launchTest(request.argument, response => {
|
||||||
|
event.source.postMessage(response, "*");
|
||||||
|
});
|
||||||
|
}, error => {
|
||||||
|
event.source.postMessage(wrapResponse({ status: "failed", errorMessage: error }), "*");
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "WASM":
|
||||||
|
const runtimeFile = request.file + "-runtime.js";
|
||||||
|
appendFiles([runtimeFile], 0, () => {
|
||||||
|
launchWasmTest(request.file, equest.argument, response => {
|
||||||
|
event.source.postMessage(response, "*");
|
||||||
|
});
|
||||||
|
}, error => {
|
||||||
|
event.source.postMessage(wrapResponse({ status: "failed", errorMessage: error }), "*");
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function appendFiles(files, index, callback, errorCallback) {
|
||||||
|
if (index === files.length) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
let fileName = files[index];
|
||||||
|
let script = document.createElement("script");
|
||||||
|
script.onload = () => {
|
||||||
|
appendFiles(files, index + 1, callback, errorCallback);
|
||||||
|
};
|
||||||
|
script.onerror = () => {
|
||||||
|
errorCallback("failed to load script " + fileName);
|
||||||
|
};
|
||||||
|
script.src = fileName;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchTest(argument, callback) {
|
||||||
|
main(argument ? [argument] : [], result => {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
callback(wrapResponse({
|
||||||
|
status: "failed",
|
||||||
|
errorMessage: buildErrorMessage(result)
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
callback({ status: "OK" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildErrorMessage(e) {
|
||||||
|
if (typeof $rt_decodeStack === "function" && typeof teavmException == "string") {
|
||||||
|
return teavmException;
|
||||||
|
}
|
||||||
|
let stack = "";
|
||||||
|
let je = main.javaException(e);
|
||||||
|
if (je && je.constructor.$meta) {
|
||||||
|
stack = je.constructor.$meta.name + ": ";
|
||||||
|
stack += je.getMessage();
|
||||||
|
stack += "\n";
|
||||||
|
}
|
||||||
|
stack += e.stack;
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchWasmTest(path, argument, callback) {
|
||||||
|
let output = [];
|
||||||
|
let outputBuffer = "";
|
||||||
|
|
||||||
|
function putwchar(charCode) {
|
||||||
|
if (charCode === 10) {
|
||||||
|
switch (outputBuffer) {
|
||||||
|
case "SUCCESS":
|
||||||
|
callback(wrapResponse({ status: "OK" }));
|
||||||
|
break;
|
||||||
|
case "FAILURE":
|
||||||
|
callback(wrapResponse({
|
||||||
|
status: "failed",
|
||||||
|
errorMessage: output.join("\n")
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
output.push(outputBuffer);
|
||||||
|
outputBuffer = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputBuffer += String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TeaVM.wasm.run(path, {
|
||||||
|
installImports: function(o) {
|
||||||
|
o.teavm.putwchar = putwchar;
|
||||||
|
},
|
||||||
|
errorCallback: function(err) {
|
||||||
|
callback(wrapResponse({
|
||||||
|
status: "failed",
|
||||||
|
errorMessage: err.message + '\n' + err.stack
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
window.parent.postMessage("ready", "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = [];
|
||||||
|
|
||||||
|
function wrapResponse(response) {
|
||||||
|
if (log.length > 0) {
|
||||||
|
response.log = log;
|
||||||
|
log = [];
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let $rt_putStdoutCustom = createOutputFunction(msg => {
|
||||||
|
log.push({ type: "stdout", message: msg });
|
||||||
|
});
|
||||||
|
let $rt_putStderrCustom = createOutputFunction(msg => {
|
||||||
|
log.push({ type: "stderr", message: msg });
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOutputFunction(printFunction) {
|
||||||
|
let buffer = "";
|
||||||
|
let utf8Buffer = 0;
|
||||||
|
let utf8Remaining = 0;
|
||||||
|
|
||||||
|
function putCodePoint(ch) {
|
||||||
|
if (ch === 0xA) {
|
||||||
|
printFunction(buffer);
|
||||||
|
buffer = "";
|
||||||
|
} else if (ch < 0x10000) {
|
||||||
|
buffer += String.fromCharCode(ch);
|
||||||
|
} else {
|
||||||
|
ch = (ch - 0x10000) | 0;
|
||||||
|
var hi = (ch >> 10) + 0xD800;
|
||||||
|
var lo = (ch & 0x3FF) + 0xDC00;
|
||||||
|
buffer += String.fromCharCode(hi, lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch => {
|
||||||
|
if ((ch & 0x80) === 0) {
|
||||||
|
putCodePoint(ch);
|
||||||
|
} else if ((ch & 0xC0) === 0x80) {
|
||||||
|
if (utf8Buffer > 0) {
|
||||||
|
utf8Remaining <<= 6;
|
||||||
|
utf8Remaining |= ch & 0x3F;
|
||||||
|
if (--utf8Buffer === 0) {
|
||||||
|
putCodePoint(utf8Remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((ch & 0xE0) === 0xC0) {
|
||||||
|
utf8Remaining = ch & 0x1F;
|
||||||
|
utf8Buffer = 1;
|
||||||
|
} else if ((ch & 0xF0) === 0xE0) {
|
||||||
|
utf8Remaining = ch & 0x0F;
|
||||||
|
utf8Buffer = 2;
|
||||||
|
} else if ((ch & 0xF8) === 0xF0) {
|
||||||
|
utf8Remaining = ch & 0x07;
|
||||||
|
utf8Buffer = 3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
31
tools/junit/src/main/resources/test-server/index.html
Normal file
31
tools/junit/src/main/resources/test-server/index.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="deobfuscator.js"></script>
|
||||||
|
<script src="client.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
logging = "{{LOGGING}}";
|
||||||
|
deobfuscation = "{{DEOBFUSCATION}}";
|
||||||
|
tryConnect();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user