Wasm backend: make JUnit tests work

This commit is contained in:
Alexey Andreev 2018-05-08 19:59:15 +03:00 committed by Alexey Andreev
parent f532801f38
commit b087610c2c
17 changed files with 349 additions and 57 deletions

View File

@ -242,7 +242,7 @@ public class TCharacter extends TObject implements TComparable<TCharacter> {
return toLowerCaseSystem(codePoint);
}
@Import(module = "runtime", name = "towlower")
@Import(module = "teavm", name = "towlower")
private static native int toLowerCaseSystem(int codePoint);
public static char toUpperCase(char ch) {
@ -258,7 +258,7 @@ public class TCharacter extends TObject implements TComparable<TCharacter> {
return toUpperCaseSystem(codePoint);
}
@Import(module = "runtime", name = "towupper")
@Import(module = "teavm", name = "towupper")
private static native int toUpperCaseSystem(int codePoint);
public static int digit(char ch, int radix) {

View File

@ -32,6 +32,6 @@ class TConsoleOutputStreamStderr extends TOutputStream {
writeImpl(b);
}
@Import(name = "putwchar", module = "runtime")
@Import(name = "putwchar", module = "teavm")
static native void writeImpl(int b);
}

View File

@ -222,15 +222,15 @@ public class TDouble extends TNumber implements TComparable<TDouble> {
}
@JSBody(params = "v", script = "return isNaN(v);")
@Import(module = "runtime", name = "isnan")
@Import(module = "teavm", name = "isnan")
public static native boolean isNaN(double v);
@JSBody(script = "return NaN;")
@Import(module = "runtime", name = "TeaVM_getNaN")
@Import(module = "teavm", name = "TeaVM_getNaN")
private static native double getNaN();
@JSBody(params = "v", script = "return !isFinite(v);")
@Import(module = "runtime", name = "isinf")
@Import(module = "teavm", name = "isinf")
public static native boolean isInfinite(double v);
public static long doubleToRawLongBits(double value) {

View File

@ -90,7 +90,7 @@ public class TFloat extends TNumber implements TComparable<TFloat> {
}
@JSBody(params = "v", script = "return isNaN(v);")
@Import(module = "runtime", name = "isnan")
@Import(module = "teavm", name = "isnan")
public static native boolean isNaN(float v);
public static boolean isInfinite(float v) {
@ -98,11 +98,11 @@ public class TFloat extends TNumber implements TComparable<TFloat> {
}
@JSBody(params = "v", script = "return isFinite(v);")
@Import(module = "runtime", name = "isfinite")
@Import(module = "teavm", name = "isfinite")
private static native boolean isFinite(float v);
@JSBody(script = "return NaN;")
@Import(module = "runtime", name = "TeaVM_getNaN")
@Import(module = "teavm", name = "TeaVM_getNaN")
private static native float getNaN();
public static float parseFloat(TString string) throws TNumberFormatException {

View File

@ -26,27 +26,27 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "sin")
@Import(module = "teavmMath", name = "sin")
public static native double sin(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "cos")
@Import(module = "teavmMath", name = "cos")
public static native double cos(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "tan")
@Import(module = "teavmMath", name = "tan")
public static native double tan(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "asin")
@Import(module = "teavmMath", name = "asin")
public static native double asin(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "acos")
@Import(module = "teavmMath", name = "acos")
public static native double acos(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "atan")
@Import(module = "teavmMath", name = "atan")
public static native double atan(double a);
public static double toRadians(double angdeg) {
@ -58,11 +58,11 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "exp")
@Import(module = "teavmMath", name = "exp")
public static native double exp(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "log")
@Import(module = "teavmMath", name = "log")
public static native double log(double a);
public static double log10(double a) {
@ -70,7 +70,7 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "sqrt")
@Import(module = "teavmMath", name = "sqrt")
public static native double sqrt(double a);
public static double cbrt(double a) {
@ -83,15 +83,15 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "ceil")
@Import(module = "teavmMath", name = "ceil")
public static native double ceil(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "floor")
@Import(module = "teavmMath", name = "floor")
public static native double floor(double a);
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "pow")
@Import(module = "teavmMath", name = "pow")
public static native double pow(double x, double y);
public static double rint(double a) {
@ -99,7 +99,7 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "atan2")
@Import(module = "teavmMath", name = "atan2")
public static native double atan2(double y, double x);
public static int round(float a) {
@ -111,7 +111,7 @@ public final class TMath extends TObject {
}
@GeneratedBy(MathNativeGenerator.class)
@Import(module = "math", name = "random")
@Import(module = "teavmMath", name = "random")
public static native double random();
public static int min(int a, int b) {

View File

@ -114,7 +114,7 @@ public final class TSystem extends TObject {
}
}
@Import(name = "currentTimeMillis", module = "runtime")
@Import(name = "currentTimeMillis", module = "teavm")
private static native double currentTimeMillisWasm();
@Import(name = "currentTimeMillis")

View File

@ -114,6 +114,6 @@ public class TRandom extends TObject implements TSerializable {
}
@JSBody(script = "return Math.random();")
@Import(module = "math", name = "random")
@Import(module = "teavmMath", name = "random")
private static native double random();
}

View File

@ -73,9 +73,11 @@ public class WasmClassGenerator {
DataPrimitives.ADDRESS, /* item type */
DataPrimitives.ADDRESS, /* array type */
DataPrimitives.INT, /* isInstance function */
DataPrimitives.INT, /* init function */
DataPrimitives.ADDRESS, /* parent */
DataPrimitives.ADDRESS, /* enum values */
DataPrimitives.ADDRESS /* layout */);
DataPrimitives.ADDRESS, /* layout */
DataPrimitives.ADDRESS /* simple name */);
private IntegerArray staticGcRoots = new IntegerArray(1);
private int staticGcRootsAddress;
@ -87,9 +89,11 @@ public class WasmClassGenerator {
private static final int CLASS_ITEM_TYPE = 6;
private static final int CLASS_ARRAY_TYPE = 7;
private static final int CLASS_IS_INSTANCE = 8;
private static final int CLASS_PARENT = 9;
private static final int CLASS_ENUM_VALUES = 10;
private static final int CLASS_LAYOUT = 11;
private static final int CLASS_INIT = 9;
private static final int CLASS_PARENT = 10;
private static final int CLASS_ENUM_VALUES = 11;
private static final int CLASS_LAYOUT = 12;
private static final int CLASS_SIMPLE_NAME = 13;
public WasmClassGenerator(ClassReaderSource classSource, VirtualTableProvider vtableProvider,
TagRegistry tagRegistry, BinaryWriter binaryWriter) {
@ -170,6 +174,8 @@ public class WasmClassGenerator {
binaryData.data.setInt(CLASS_IS_INSTANCE, functionTable.size());
binaryData.data.setInt(CLASS_CANARY, RuntimeClass.computeCanary(4, 0));
functionTable.add(Mangling.mangleIsSupertype(type));
binaryData.data.setAddress(CLASS_SIMPLE_NAME, 0);
binaryData.data.setInt(CLASS_INIT, -1);
binaryData.start = binaryWriter.append(vtableSize > 0 ? wrapper : binaryData.data);
itemBinaryData.data.setAddress(CLASS_ARRAY_TYPE, binaryData.start);
@ -181,6 +187,8 @@ public class WasmClassGenerator {
value.setInt(CLASS_SIZE, size);
value.setInt(CLASS_FLAGS, RuntimeClass.PRIMITIVE);
value.setInt(CLASS_IS_INSTANCE, functionTable.size());
value.setAddress(CLASS_SIMPLE_NAME, 0);
value.setInt(CLASS_INIT, -1);
functionTable.add(Mangling.mangleIsSupertype(type));
return value;
}
@ -248,7 +256,16 @@ public class WasmClassGenerator {
flags |= RuntimeClass.ENUM;
}
if (cls != null && binaryData.start >= 0
&& cls.getMethod(new MethodDescriptor("<clinit>", ValueType.VOID)) != null) {
header.setInt(CLASS_INIT, functionTable.size());
functionTable.add(Mangling.mangleInitializer(name));
} else {
header.setInt(CLASS_INIT, -1);
}
header.setInt(CLASS_FLAGS, flags);
header.setAddress(CLASS_SIMPLE_NAME, 0);
return vtable != null ? wrapper : header;
}
@ -500,7 +517,7 @@ public class WasmClassGenerator {
}
public boolean hasClinit(String className) {
if (isStructure(className)) {
if (isStructure(className) || className.equals(Address.class.getName())) {
return false;
}
ClassReader cls = classSource.get(className);

View File

@ -35,6 +35,7 @@ public class PlatformIntrinsic implements WasmIntrinsic {
case "getPlatformObject":
case "asJavaClass":
case "getName":
case "createQueue":
return true;
default:
return false;
@ -48,6 +49,7 @@ public class PlatformIntrinsic implements WasmIntrinsic {
case "asJavaClass":
return manager.generate(invocation.getArguments().get(0));
case "getName":
case "createQueue":
return new WasmInt32Constant(0);
default:
throw new IllegalArgumentException(invocation.getMethod().toString());

View File

@ -0,0 +1,89 @@
/*
* Copyright 2018 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.
*/
var TeaVM = TeaVM || {};
TeaVM.wasm = function() {
let lineBuffer = "";
function putwchar(charCode) {
if (charCode === 10) {
console.log(lineBuffer);
lineBuffer = "";
} else {
lineBuffer += String.fromCharCode(charCode);
}
}
function towlower(code) {
return String.fromCharCode(code).toLowerCase().charCodeAt(0);
}
function towupper(code) {
return String.fromCharCode(code).toUpperCase().charCodeAt(0);
}
function currentTimeMillis() {
return new Date().getTime();
}
function importDefaults(obj) {
obj.teavm = {
currentTimeMillis: currentTimeMillis,
isnan: isNaN,
TeaVM_getNaN: function() { return NaN; },
isinf: function(n) { return !isFinite(n) },
isfinite: isFinite,
putwchar: putwchar,
towlower: towlower,
towupper: towupper
};
obj.teavmMath = Math;
}
function run(path, options) {
if (!options) {
options = {};
}
let callback = typeof options.callback !== "undefined" ? options.callback : function() {};
let errorCallback = typeof options.errorCallback !== "undefined" ? options.errorCallback : function() {};
let importObj = {};
importDefaults(importObj);
if (typeof options.installImports !== "undefined") {
options.installImports(importObj);
}
let xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("GET", path);
xhr.onload = function() {
let response = xhr.response;
if (!response) {
return;
}
WebAssembly.instantiate(response, importObj).then(function(resultObject) {
resultObject.instance.exports.main();
callback(resultObject);
}).catch(function(error) {
console.log("Error loading WebAssembly %o", error);
errorCallback(error);
});
};
xhr.send();
}
return { importDefaults: importDefaults, run: run };
}();

View File

@ -18,6 +18,8 @@
window.addEventListener("message", event => {
let request = event.data;
switch (request.type) {
case "js":
appendFiles(request.files, 0, () => {
launchTest(response => {
event.source.postMessage(response, "*");
@ -25,13 +27,24 @@ window.addEventListener("message", event => {
}, error => {
event.source.postMessage({ status: "failed", errorMessage: error }, "*");
});
break;
case "wasm":
appendFiles(request.files.filter(f => f.endsWith(".js")), 0, () => {
launchWasmTest(request.files.filter(f => f.endsWith(".wasm"))[0], response => {
event.source.postMessage(response, "*");
});
}, error => {
event.source.postMessage({ status: "failed", errorMessage: error }, "*");
});
break;
}
});
function appendFiles(files, index, callback, errorCallback) {
if (index === files.length) {
callback();
} else {
let fileName = "file://" + files[index];
let fileName = files[index];
let script = document.createElement("script");
script.onload = () => {
appendFiles(files, index + 1, callback, errorCallback);
@ -81,6 +94,44 @@ function launchTest(callback) {
}
}
function launchWasmTest(path, callback) {
var output = [];
var outputBuffer = "";
function putwchar(charCode) {
if (charCode === 10) {
switch (outputBuffer) {
case "SUCCESS":
callback({status: "OK"});
break;
case "FAILED":
callback({
status: "failed",
errorMessage: output.join("\n")
});
break;
default:
output.push(TeaVM_outputBuffer);
outputBuffer = "";
}
} else {
outputBuffer += String.fromCharCode(charCode);
}
}
TeaVM.wasm.run(path, {
installImports: function(o) {
o.teavm.putwchar = putwchar;
},
errorCallback: function(err) {
callback({
status: "failed",
errorMessage: err.message + '\n' + err.stack
});
}
});
}
function start() {
window.parent.postMessage("ready", "*");
}

View File

@ -16,17 +16,20 @@
"use strict";
import * as fs from "./promise-fs.js";
import * as nodePath from "path";
import * as http from "http";
import {server as WebSocketServer} from "websocket";
const TEST_FILE_NAME = "test.js";
const RUNTIME_FILE_NAME = "runtime.js";
const WASM_RUNTIME_FILE_NAME = "test.wasm-runtime.js";
const TEST_FILES = [
{ file: TEST_FILE_NAME, name: "simple" },
{ file: "test-min.js", name: "minified" },
{ file: "test-optimized.js", name: "optimized" }
{ file: TEST_FILE_NAME, name: "simple", type: "js" },
{ file: "test-min.js", name: "minified", type: "js" },
{ file: "test-optimized.js", name: "optimized", type: "js" },
{ file: "test.wasm", name: "wasm", type: "wasm" },
{ file: "test-optimized.wasm", name: "wasm-optimized", type: "wasm" }
];
const SERVER_PREFIX = "http://localhost:9090/";
let totalTests = 0;
class TestSuite {
@ -37,20 +40,30 @@ class TestSuite {
}
}
class TestCase {
constructor(name, files) {
constructor(type, name, files) {
this.type = type;
this.name = name;
this.files = files;
}
}
let rootDir = process.argv[2];
if (rootDir.endsWith("/")) {
rootDir = rootDir.substring(0, rootDir.length - 1);
}
async function runAll() {
const rootSuite = new TestSuite("root");
console.log("Searching tests");
await walkDir(process.argv[2], "root", rootSuite);
await walkDir("", "root", rootSuite);
console.log("Running tests");
const server = http.createServer((request, response) => {
if (request.url.endsWith(".js") || request.url.endsWith(".wasm")) {
serveFile(rootDir + "/" + request.url, response);
return;
}
response.writeHead(404);
response.end();
});
@ -94,14 +107,37 @@ async function runAll() {
}
}
async function serveFile(path, response) {
const stat = await fs.stat(path);
const contentType = path.endsWith(".wasm") ? "application/octet-stream" : "text/javascript";
if (stat.isFile()) {
const content = await fs.readFile(path);
response.writeHead(200, { 'Content-Type': contentType, 'Access-Control-Allow-Origin': "*" });
response.end(content, 'utf-8');
} else {
response.writeHead(404);
response.end();
}
}
async function walkDir(path, name, suite) {
const files = await fs.readdir(path);
if (files.includes(TEST_FILE_NAME) && files.includes(RUNTIME_FILE_NAME)) {
for (const { file: fileName, name: profileName } of TEST_FILES) {
const files = await fs.readdir(rootDir + "/" + path);
if (files.includes(WASM_RUNTIME_FILE_NAME) || files.includes(RUNTIME_FILE_NAME)) {
for (const { file: fileName, name: profileName, type: type } of TEST_FILES) {
if (files.includes(fileName)) {
switch (type) {
case "js":
suite.testCases.push(new TestCase(
name + " " + profileName,
[path + "/" + RUNTIME_FILE_NAME, path + "/" + fileName]));
"js", name + " " + profileName,
[SERVER_PREFIX + path + "/" + RUNTIME_FILE_NAME, SERVER_PREFIX + path + "/" + fileName]));
break;
case "wasm":
suite.testCases.push(new TestCase(
"wasm", name + " " + profileName,
[SERVER_PREFIX + path + "/" + WASM_RUNTIME_FILE_NAME,
SERVER_PREFIX + path + "/" + fileName]));
break;
}
totalTests++;
}
}
@ -110,7 +146,7 @@ async function walkDir(path, name, suite) {
suite.testSuites.push(childSuite);
await Promise.all(files.map(async file => {
const filePath = path + "/" + file;
const stat = await fs.stat(filePath);
const stat = await fs.stat(rootDir + "/" + filePath);
if (stat.isDirectory()) {
await walkDir(filePath, file, childSuite);
}
@ -148,8 +184,9 @@ class TestRunner {
let request = { id: this.requestIdGen++ };
request.tests = suite.testCases.map(testCase => {
return {
type: testCase.type,
name: testCase.name,
files: testCase.files.map(fileName => nodePath.resolve(process.cwd(), fileName))
files: testCase.files
};
});
this.testsRun += suite.testCases.length;

View File

@ -17,5 +17,6 @@ package org.teavm.junit;
enum RunKind {
JAVASCRIPT,
C
C,
WASM
}

View File

@ -17,6 +17,7 @@ package org.teavm.junit;
import org.teavm.backend.c.CTarget;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.backend.wasm.WasmTarget;
import org.teavm.vm.TeaVM;
import org.teavm.vm.TeaVMOptimizationLevel;
import org.teavm.vm.TeaVMTarget;
@ -79,6 +80,40 @@ interface TeaVMTestConfiguration<T extends TeaVMTarget> {
}
};
TeaVMTestConfiguration<WasmTarget> WASM_DEFAULT = new TeaVMTestConfiguration<WasmTarget>() {
@Override
public String getSuffix() {
return "";
}
@Override
public void apply(TeaVM vm) {
vm.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE);
}
@Override
public void apply(WasmTarget target) {
target.setMinHeapSize(32 * 1024 * 1024);
target.setWastEmitted(true);
}
};
TeaVMTestConfiguration<WasmTarget> WASM_OPTIMIZED = new TeaVMTestConfiguration<WasmTarget>() {
@Override
public String getSuffix() {
return "optimized";
}
@Override
public void apply(TeaVM vm) {
vm.setOptimizationLevel(TeaVMOptimizationLevel.FULL);
}
@Override
public void apply(WasmTarget target) {
}
};
TeaVMTestConfiguration<CTarget> C_DEFAULT = new TeaVMTestConfiguration<CTarget>() {
@Override
public String getSuffix() {
@ -95,11 +130,10 @@ interface TeaVMTestConfiguration<T extends TeaVMTarget> {
}
};
TeaVMTestConfiguration<CTarget> C_OPTIMIZED = new TeaVMTestConfiguration<CTarget>() {
@Override
public String getSuffix() {
return "";
return "optimized";
}
@Override

View File

@ -52,6 +52,7 @@ import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.teavm.backend.c.CTarget;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.backend.wasm.WasmTarget;
import org.teavm.callgraph.CallGraph;
import org.teavm.diagnostics.DefaultProblemTextConsumer;
import org.teavm.diagnostics.Problem;
@ -78,7 +79,8 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private static final String SELENIUM_URL = "teavm.junit.js.selenium.url";
private static final String JS_ENABLED = "teavm.junit.js";
private static final String C_ENABLED = "teavm.junit.c";
private static final String C_COMPILER = "teavm.junit.c-compiler";
private static final String WASM_ENABLED = "teavm.junit.wasm";
private static final String C_COMPILER = "teavm.junit.c.compiler";
private static final String MINIFIED = "teavm.junit.minified";
private static final String OPTIMIZED = "teavm.junit.optimized";
@ -275,6 +277,15 @@ public class TeaVMTestRunner extends Runner implements Filterable {
runs.add(run);
}
}
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
TestRun run = compile(child, notifier, RunKind.WASM,
m -> compileToWasm(m, configuration, outputPath), onSuccess.get(0));
if (run != null) {
runs.add(run);
}
}
} catch (Throwable e) {
notifier.fireTestFailure(new Failure(description, e));
notifier.fireTestFinished(description);
@ -414,7 +425,9 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private void copyJsFilesTo(File path) throws IOException {
resourceToFile("org/teavm/backend/javascript/runtime.js", new File(path, "runtime.js"));
resourceToFile("org/teavm/backend/wasm/wasm-runtime.js", new File(path, "test.wasm-runtime.js"));
resourceToFile("teavm-run-test.html", new File(path, "run-test.html"));
resourceToFile("teavm-run-test-wasm.html", new File(path, "run-test-wasm.html"));
}
private CompileResult compileToJs(Method method, TeaVMTestConfiguration<JavaScriptTarget> configuration,
@ -434,6 +447,13 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}, path, ".c");
}
private CompileResult compileToWasm(Method method, TeaVMTestConfiguration<WasmTarget> configuration,
File path) {
return compileTest(method, configuration, WasmTarget::new, vm -> {
vm.entryPoint("main", new MethodReference(TestEntryPoint.class, "main", String[].class, void.class));
}, path, ".wasm");
}
private <T extends TeaVMTarget> CompileResult compileTest(Method method, TeaVMTestConfiguration<T> configuration,
Supplier<T> targetSupplier, Consumer<TeaVM> preBuild, File path, String extension) {
CompileResult result = new CompileResult();
@ -497,6 +517,17 @@ public class TeaVMTestRunner extends Runner implements Filterable {
return configurations;
}
private List<TeaVMTestConfiguration<WasmTarget>> getWasmConfigurations() {
List<TeaVMTestConfiguration<WasmTarget>> configurations = new ArrayList<>();
if (Boolean.getBoolean(WASM_ENABLED)) {
configurations.add(TeaVMTestConfiguration.WASM_DEFAULT);
if (Boolean.getBoolean(OPTIMIZED)) {
configurations.add(TeaVMTestConfiguration.WASM_OPTIMIZED);
}
}
return configurations;
}
private List<TeaVMTestConfiguration<CTarget>> getCConfigurations() {
List<TeaVMTestConfiguration<CTarget>> configurations = new ArrayList<>();
if (Boolean.getBoolean(C_ENABLED)) {

View File

@ -39,6 +39,7 @@ final class TestEntryPoint {
System.out.println("SUCCESS");
} catch (Throwable e) {
e.printStackTrace(System.out);
System.out.println("FAILURE");
}
}
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<!--
~ Copyright 2018 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.
-->
<html>
<head>
<title>TeaVM JUnit test</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<script type="text/javascript" src="test.wasm-runtime.js"></script>
<script type="text/javascript">
TeaVM.wasm.run("test.wasm");
</script>
</body>
</html>