Add per-class compilation when running tests (requires @WholeClassCompilation annotation)

This commit is contained in:
Alexey Andreev 2020-02-27 18:43:08 +03:00
parent 150a613709
commit 95426e2159
14 changed files with 573 additions and 208 deletions

View File

@ -19,18 +19,20 @@
window.addEventListener("message", event => { window.addEventListener("message", event => {
let request = event.data; let request = event.data;
switch (request.type) { switch (request.type) {
case "js": case "JAVASCRIPT":
appendFiles(request.files, 0, () => { appendFiles([request.file], 0, () => {
launchTest(response => { launchTest(request.argument, response => {
event.source.postMessage(response, "*"); event.source.postMessage(response, "*");
}); });
}, error => { }, error => {
event.source.postMessage({ status: "failed", errorMessage: error }, "*"); event.source.postMessage({ status: "failed", errorMessage: error }, "*");
}); });
break; break;
case "wasm":
appendFiles(request.files.filter(f => f.endsWith(".js")), 0, () => { case "WASM":
launchWasmTest(request.files.filter(f => f.endsWith(".wasm"))[0], response => { const runtimeFile = request.file + "-runtime.js";
appendFiles([runtimeFile], 0, () => {
launchWasmTest(request.file, equest.argument, response => {
event.source.postMessage(response, "*"); event.source.postMessage(response, "*");
}); });
}, error => { }, error => {
@ -57,8 +59,8 @@ function appendFiles(files, index, callback, errorCallback) {
} }
} }
function launchTest(callback) { function launchTest(argument, callback) {
main([], result => { main(argument ? [argument] : [], result => {
if (result instanceof Error) { if (result instanceof Error) {
callback({ callback({
status: "failed", status: "failed",
@ -81,7 +83,7 @@ function launchTest(callback) {
} }
} }
function launchWasmTest(path, callback) { function launchWasmTest(path, argument, callback) {
var output = []; var output = [];
var outputBuffer = ""; var outputBuffer = "";

View File

@ -19,16 +19,6 @@ import * as fs from "./promise-fs.js";
import * as http from "http"; import * as http from "http";
import {server as WebSocketServer} from "websocket"; import {server as WebSocketServer} from "websocket";
const TEST_FILE_NAME = "test.js";
const WASM_RUNTIME_FILE_NAME = "test.wasm-runtime.js";
const TEST_FILES = [
{ 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; let totalTests = 0;
class TestSuite { class TestSuite {
@ -39,10 +29,10 @@ class TestSuite {
} }
} }
class TestCase { class TestCase {
constructor(type, name, files) { constructor(type, file, argument) {
this.type = type; this.type = type;
this.name = name; this.file = file;
this.files = files; this.argument = argument
} }
} }
@ -54,7 +44,7 @@ if (rootDir.endsWith("/")) {
async function runAll() { async function runAll() {
const rootSuite = new TestSuite("root"); const rootSuite = new TestSuite("root");
console.log("Searching tests"); console.log("Searching tests");
await walkDir("", "root", rootSuite); await walkDir("", rootSuite);
console.log("Running tests"); console.log("Running tests");
@ -119,38 +109,29 @@ async function serveFile(path, response) {
} }
} }
async function walkDir(path, name, suite) { async function walkDir(path, suite) {
const files = await fs.readdir(rootDir + "/" + path); const files = await fs.readdir(rootDir + "/" + path);
if (files.includes(WASM_RUNTIME_FILE_NAME) || files.includes("test.js")) { if (files.includes("tests.json")) {
for (const { file: fileName, name: profileName, type: type } of TEST_FILES) { const descriptor = JSON.parse(await fs.readFile(`${rootDir}/${path}/tests.json`));
if (files.includes(fileName)) { for (const { baseDir, fileName, kind, argument } of descriptor) {
switch (type) { switch (kind) {
case "js": case "JAVASCRIPT":
suite.testCases.push(new TestCase( case "WASM":
"js", name + " " + profileName, suite.testCases.push(new TestCase(kind, `${baseDir}/${fileName}`, argument));
[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++; totalTests++;
break;
}
} }
} }
} else if (files) {
const childSuite = new TestSuite(name);
suite.testSuites.push(childSuite);
await Promise.all(files.map(async file => { await Promise.all(files.map(async file => {
const filePath = path + "/" + file; const filePath = path + "/" + file;
const stat = await fs.stat(rootDir + "/" + filePath); const stat = await fs.stat(rootDir + "/" + filePath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
await walkDir(filePath, file, childSuite); const childSuite = new TestSuite(file);
suite.testSuites.push(childSuite);
await walkDir(filePath, childSuite);
} }
})); }));
}
} }
class TestRunner { class TestRunner {
@ -182,11 +163,15 @@ class TestRunner {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
let request = { id: this.requestIdGen++ }; let request = { id: this.requestIdGen++ };
request.tests = suite.testCases.map(testCase => { request.tests = suite.testCases.map(testCase => {
return { const result = {
type: testCase.type, type: testCase.type,
name: testCase.name, name: testCase.name,
files: testCase.files file: testCase.file
}; };
if (testCase.argument) {
result.argument = testCase.argument;
}
return result;
}); });
this.testsRun += suite.testCases.length; this.testsRun += suite.testCases.length;

View File

@ -20,11 +20,16 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
class CRunStrategy implements TestRunStrategy { class CRunStrategy implements TestRunStrategy {
private String compilerCommand; private String compilerCommand;
private ConcurrentMap<String, Compilation> compilationMap = new ConcurrentHashMap<>();
CRunStrategy(String compilerCommand) { CRunStrategy(String compilerCommand) {
this.compilerCommand = compilerCommand; this.compilerCommand = compilerCommand;
@ -47,18 +52,21 @@ class CRunStrategy implements TestRunStrategy {
} }
File outputFile = new File(run.getBaseDirectory(), exeName); File outputFile = new File(run.getBaseDirectory(), exeName);
List<String> compilerOutput = new ArrayList<>(); boolean compilerSuccess = compile(run.getBaseDirectory());
boolean compilerSuccess = runCompiler(run.getBaseDirectory(), compilerOutput);
if (!compilerSuccess) { if (!compilerSuccess) {
run.getCallback().error(new RuntimeException("C compiler error:\n" + mergeLines(compilerOutput))); run.getCallback().error(new RuntimeException("C compiler error"));
return; return;
} }
writeLines(compilerOutput);
List<String> runtimeOutput = new ArrayList<>(); List<String> runtimeOutput = new ArrayList<>();
List<String> stdout = new ArrayList<>(); List<String> stdout = new ArrayList<>();
outputFile.setExecutable(true); outputFile.setExecutable(true);
runProcess(new ProcessBuilder(outputFile.getPath()).start(), runtimeOutput, stdout); List<String> runCommand = new ArrayList<>();
runCommand.add(outputFile.getPath());
if (run.getArgument() != null) {
runCommand.add(run.getArgument());
}
runProcess(new ProcessBuilder(runCommand.toArray(new String[0])).start(), runtimeOutput, stdout);
if (!stdout.isEmpty() && stdout.get(stdout.size() - 1).equals("SUCCESS")) { if (!stdout.isEmpty() && stdout.get(stdout.size() - 1).equals("SUCCESS")) {
writeLines(runtimeOutput); writeLines(runtimeOutput);
run.getCallback().complete(); run.getCallback().complete();
@ -84,6 +92,24 @@ class CRunStrategy implements TestRunStrategy {
} }
} }
private boolean compile(File inputDir) throws IOException, InterruptedException {
Compilation compilation = compilationMap.computeIfAbsent(inputDir.getPath(), k -> new Compilation());
synchronized (compilation) {
if (!compilation.started) {
compilation.started = true;
compilation.success = doCompile(inputDir);
}
}
return compilation.success;
}
private boolean doCompile(File inputDir) throws IOException, InterruptedException {
List<String> compilerOutput = new ArrayList<>();
boolean compilerSuccess = runCompiler(inputDir, compilerOutput);
writeLines(compilerOutput);
return compilerSuccess;
}
private boolean runCompiler(File inputDir, List<String> output) private boolean runCompiler(File inputDir, List<String> output)
throws IOException, InterruptedException { throws IOException, InterruptedException {
String command = new File(compilerCommand).getAbsolutePath(); String command = new File(compilerCommand).getAbsolutePath();
@ -133,4 +159,9 @@ class CRunStrategy implements TestRunStrategy {
output.addAll(lines); output.addAll(lines);
return result; return result;
} }
static class Compilation {
volatile boolean started;
volatile boolean success;
}
} }

View File

@ -25,6 +25,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.sourceforge.htmlunit.corejs.javascript.BaseFunction; import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
@ -64,7 +65,6 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
HtmlPage pageRef = page.get(); HtmlPage pageRef = page.get();
pageRef.executeJavaScript(readFile(new File(run.getBaseDirectory(), run.getFileName()))); pageRef.executeJavaScript(readFile(new File(run.getBaseDirectory(), run.getFileName())));
boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true")); boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true"));
File debugFile = decodeStack ? new File(run.getBaseDirectory(), run.getFileName() + ".teavmdbg") : null; File debugFile = decodeStack ? new File(run.getBaseDirectory(), run.getFileName() + ".teavmdbg") : null;
@ -74,6 +74,7 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
Function function = (Function) page.get().executeJavaScript(readResource("teavm-htmlunit-adapter.js")) Function function = (Function) page.get().executeJavaScript(readResource("teavm-htmlunit-adapter.js"))
.getJavaScriptResult(); .getJavaScriptResult();
Object[] args = new Object[] { Object[] args = new Object[] {
run.getArgument(),
decodeStack ? createStackDecoderFunction(resultParser) : null, decodeStack ? createStackDecoderFunction(resultParser) : null,
new NativeJavaObject(function, asyncResult, AsyncResult.class) new NativeJavaObject(function, asyncResult, AsyncResult.class)
}; };
@ -161,7 +162,7 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
private String readFile(File file) throws IOException { private String readFile(File file) throws IOException {
try (InputStream input = new FileInputStream(file)) { try (InputStream input = new FileInputStream(file)) {
return IOUtils.toString(input, "UTF-8"); return IOUtils.toString(input, StandardCharsets.UTF_8);
} }
} }
@ -170,11 +171,11 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
if (input == null) { if (input == null) {
return ""; return "";
} }
return IOUtils.toString(input, "UTF-8"); return IOUtils.toString(input, StandardCharsets.UTF_8);
} }
} }
public class AsyncResult { public static class AsyncResult {
private CountDownLatch latch = new CountDownLatch(1); private CountDownLatch latch = new CountDownLatch(1);
private Object result; private Object result;

View File

@ -16,6 +16,7 @@
package org.teavm.junit; 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.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -109,6 +110,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private static final int stopTimeout = 15000; private static final int stopTimeout = 15000;
private Class<?> testClass; private Class<?> testClass;
private boolean isWholeClassCompilation;
private ClassHolderSource classSource; private ClassHolderSource classSource;
private ClassLoader classLoader; private ClassLoader classLoader;
private Description suiteDescription; private Description suiteDescription;
@ -120,6 +122,8 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private CountDownLatch latch; private CountDownLatch latch;
private List<Method> filteredChildren; private List<Method> filteredChildren;
private ReferenceCache referenceCache = new ReferenceCache(); private ReferenceCache referenceCache = new ReferenceCache();
private boolean classCompilationOk;
private List<TestRun> runsInCurrentClass = new ArrayList<>();
static class RunnerKindInfo { static class RunnerKindInfo {
volatile TestRunner runner; volatile TestRunner runner;
@ -193,10 +197,17 @@ public class TeaVMTestRunner extends Runner implements Filterable {
latch = new CountDownLatch(children.size()); latch = new CountDownLatch(children.size());
notifier.fireTestStarted(getDescription()); notifier.fireTestStarted(getDescription());
isWholeClassCompilation = testClass.isAnnotationPresent(WholeClassCompilation.class);
if (isWholeClassCompilation) {
classCompilationOk = compileWholeClass(children, notifier);
}
for (Method child : children) { for (Method child : children) {
runChild(child, notifier); runChild(child, notifier);
} }
writeRunsDescriptor();
runsInCurrentClass.clear();
while (true) { while (true) {
try { try {
if (latch.await(1000, TimeUnit.MILLISECONDS)) { if (latch.await(1000, TimeUnit.MILLISECONDS)) {
@ -246,6 +257,38 @@ public class TeaVMTestRunner extends Runner implements Filterable {
method.getName())); method.getName()));
} }
private boolean compileWholeClass(List<Method> children, RunNotifier notifier) {
File outputPath = getOutputPathForClass();
boolean hasErrors = false;
Description description = getDescription();
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
CompileResult result = compileToJs(wholeClass(children), "classTest", configuration, outputPath);
if (!result.success) {
hasErrors = true;
notifier.fireTestFailure(createFailure(description, result));
}
}
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
CompileResult result = compileToC(wholeClass(children), "classTest", configuration, outputPath);
if (!result.success) {
hasErrors = true;
notifier.fireTestFailure(createFailure(description, result));
}
}
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
CompileResult result = compileToWasm(wholeClass(children), "classTest", configuration, outputPath);
if (!result.success) {
hasErrors = true;
notifier.fireTestFailure(createFailure(description, result));
}
}
return !hasErrors;
}
private void runChild(Method child, RunNotifier notifier) { private void runChild(Method child, RunNotifier notifier) {
Description description = describeChild(child); Description description = describeChild(child);
notifier.fireTestStarted(description); notifier.fireTestStarted(description);
@ -273,62 +316,37 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
if (!child.isAnnotationPresent(SkipJVM.class) if (!child.isAnnotationPresent(SkipJVM.class) && !testClass.isAnnotationPresent(SkipJVM.class)) {
&& !testClass.isAnnotationPresent(SkipJVM.class)) {
ran = true; ran = true;
success = runInJvm(child, notifier, expectedExceptions); success = runInJvm(child, notifier, expectedExceptions);
} }
if (success && outputDir != null) { if (success && outputDir != null) {
int[] configurationIndex = new int[] { 0 }; int[] configurationIndex = new int[] { 0 };
List<Consumer<Boolean>> onSuccess = new ArrayList<>();
List<TestRun> runs = new ArrayList<>(); List<TestRun> runs = new ArrayList<>();
onSuccess.add(runSuccess -> { Consumer<Boolean> onSuccess = runSuccess -> {
if (runSuccess && configurationIndex[0] < runs.size()) { if (runSuccess && configurationIndex[0] < runs.size()) {
submitRun(runs.get(configurationIndex[0]++)); submitRun(runs.get(configurationIndex[0]++));
} else { } else {
notifier.fireTestFinished(description); notifier.fireTestFinished(description);
latch.countDown(); latch.countDown();
} }
}); };
try { if (isWholeClassCompilation) {
File outputPath = getOutputPath(child); if (!classCompilationOk) {
copyJsFilesTo(outputPath);
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
TestRun run = compile(child, notifier, RunKind.JAVASCRIPT,
m -> compileToJs(m, configuration, outputPath), onSuccess.get(0));
if (run != null) {
runs.add(run);
}
}
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
TestRun run = compile(child, notifier, RunKind.C,
m -> compileToC(m, configuration, outputPath), onSuccess.get(0));
if (run != null) {
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); notifier.fireTestFinished(description);
notifier.fireTestFailure(new Failure(description,
new AssertionError("Could not compile test class")));
latch.countDown(); latch.countDown();
return; } else {
runTestsFromWholeClass(child, notifier, runs, onSuccess);
onSuccess.accept(true);
}
} else {
runCompiledTest(child, notifier, runs, onSuccess);
} }
onSuccess.get(0).accept(true);
} else { } else {
if (!ran) { if (!ran) {
notifier.fireTestIgnored(description); notifier.fireTestIgnored(description);
@ -338,6 +356,81 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
private void runTestsFromWholeClass(Method child, RunNotifier notifier, List<TestRun> runs,
Consumer<Boolean> onSuccess) {
File outputPath = getOutputPathForClass();
MethodDescriptor descriptor = getDescriptor(child);
MethodReference reference = new MethodReference(testClass.getName(), descriptor);
File testFilePath = getOutputPath(child);
testFilePath.mkdirs();
boolean hasJsOrWasm = false;
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), false, ".js");
runs.add(createTestRun(testPath, child, RunKind.JAVASCRIPT, reference.toString(), notifier, onSuccess));
hasJsOrWasm = true;
}
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), false, ".wasm");
runs.add(createTestRun(testPath, child, RunKind.WASM, reference.toString(), notifier, onSuccess));
hasJsOrWasm = true;
}
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
File testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), true, ".c");
runs.add(createTestRun(testPath, child, RunKind.C, reference.toString(), notifier, onSuccess));
}
if (hasJsOrWasm) {
try {
copyJsFilesTo(testFilePath);
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
private void runCompiledTest(Method child, RunNotifier notifier, List<TestRun> runs, Consumer<Boolean> onSuccess) {
try {
File outputPath = getOutputPath(child);
copyJsFilesTo(outputPath);
for (TeaVMTestConfiguration<JavaScriptTarget> configuration : getJavaScriptConfigurations()) {
CompileResult compileResult = compileToJs(singleTest(child), "test", configuration, outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.JAVASCRIPT, onSuccess);
if (run != null) {
runs.add(run);
}
}
for (TeaVMTestConfiguration<CTarget> configuration : getCConfigurations()) {
CompileResult compileResult = compileToC(singleTest(child), "test", configuration, outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.C, onSuccess);
if (run != null) {
runs.add(run);
}
}
for (TeaVMTestConfiguration<WasmTarget> configuration : getWasmConfigurations()) {
CompileResult compileResult = compileToWasm(singleTest(child), "test", configuration,
outputPath);
TestRun run = prepareRun(child, compileResult, notifier, RunKind.WASM, onSuccess);
if (run != null) {
runs.add(run);
}
}
} catch (Throwable e) {
notifier.fireTestFailure(new Failure(describeChild(child), e));
notifier.fireTestFinished(describeChild(child));
latch.countDown();
return;
}
onSuccess.accept(true);
}
private String[] getExpectedExceptions(MethodHolder method) { private String[] getExpectedExceptions(MethodHolder method) {
AnnotationHolder annot = method.getAnnotations().get(JUNIT4_TEST); AnnotationHolder annot = method.getAnnotations().get(JUNIT4_TEST);
if (annot == null) { if (annot == null) {
@ -438,7 +531,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
void run() throws Throwable; void run() throws Throwable;
} }
class JUnit4Runner implements Runner { static class JUnit4Runner implements Runner {
Object instance; Object instance;
Method child; Method child;
@ -457,7 +550,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
class JUnit3Runner implements Runner { static class JUnit3Runner implements Runner {
Object instance; Object instance;
JUnit3Runner(Object instance) { JUnit3Runner(Object instance) {
@ -470,25 +563,24 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
private TestRun compile(Method child, RunNotifier notifier, RunKind kind, private TestRun prepareRun(Method child, CompileResult result, RunNotifier notifier, RunKind kind,
CompileFunction compiler, Consumer<Boolean> onComplete) { Consumer<Boolean> onComplete) {
Description description = describeChild(child); Description description = describeChild(child);
CompileResult compileResult; if (!result.success) {
try { notifier.fireTestFailure(createFailure(description, result));
compileResult = compiler.compile(child);
} catch (Exception e) {
notifier.fireTestFailure(new Failure(description, e));
notifier.fireTestFinished(description); notifier.fireTestFinished(description);
latch.countDown(); latch.countDown();
return null; return null;
} }
if (!compileResult.success) { return createTestRun(result.file, child, kind, null, notifier, onComplete);
notifier.fireTestFailure(new Failure(description, new AssertionError(compileResult.errorMessage)));
return null;
} }
private TestRun createTestRun(File file, Method child, RunKind kind, String argument, RunNotifier notifier,
Consumer<Boolean> onComplete) {
Description description = describeChild(child);
TestRunCallback callback = new TestRunCallback() { TestRunCallback callback = new TestRunCallback() {
@Override @Override
public void complete() { public void complete() {
@ -502,12 +594,21 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
}; };
return new TestRun(compileResult.file.getParentFile(), child, description, compileResult.file.getName(), return new TestRun(file.getParentFile(), child, description, file.getName(), kind,
kind, callback); argument, callback);
}
private Failure createFailure(Description description, CompileResult result) {
Throwable throwable = result.throwable;
if (throwable == null) {
throwable = new AssertionError(result.errorMessage);
}
return new Failure(description, throwable);
} }
private void submitRun(TestRun run) { private void submitRun(TestRun run) {
synchronized (TeaVMTestRunner.class) { synchronized (TeaVMTestRunner.class) {
runsInCurrentClass.add(run);
RunnerKindInfo info = runners.get(run.getKind()); RunnerKindInfo info = runners.get(run.getKind());
if (info.strategy == null) { if (info.strategy == null) {
@ -552,14 +653,20 @@ public class TeaVMTestRunner extends Runner implements Filterable {
return path; return path;
} }
private File getOutputPathForClass() {
File path = outputDir;
path = new File(path, testClass.getName().replace('.', '/'));
path.mkdirs();
return path;
}
private void copyJsFilesTo(File path) throws IOException { private void copyJsFilesTo(File path) throws IOException {
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.html", new File(path, "run-test.html"));
resourceToFile("teavm-run-test-wasm.html", new File(path, "run-test-wasm.html")); resourceToFile("teavm-run-test-wasm.html", new File(path, "run-test-wasm.html"));
} }
private CompileResult compileToJs(Method method, TeaVMTestConfiguration<JavaScriptTarget> configuration, private CompileResult compileToJs(Consumer<TeaVM> additionalProcessing, String baseName,
File path) { TeaVMTestConfiguration<JavaScriptTarget> configuration, File path) {
boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true")); boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true"));
DebugInformationBuilder debugEmitter = new DebugInformationBuilder(new ReferenceCache()); DebugInformationBuilder debugEmitter = new DebugInformationBuilder(new ReferenceCache());
Supplier<JavaScriptTarget> targetSupplier = () -> { Supplier<JavaScriptTarget> targetSupplier = () -> {
@ -595,12 +702,12 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
}; };
} }
return compileTest(method, configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js", return compile(configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js",
postBuild, false); postBuild, false, additionalProcessing, baseName);
} }
private CompileResult compileToC(Method method, TeaVMTestConfiguration<CTarget> configuration, private CompileResult compileToC(Consumer<TeaVM> additionalProcessing, String baseName,
File path) { TeaVMTestConfiguration<CTarget> configuration, File path) {
CompilePostProcessor postBuild = (vm, file) -> { CompilePostProcessor postBuild = (vm, file) -> {
try { try {
resourceToFile("teavm-CMakeLists.txt", new File(file.getParent(), "CMakeLists.txt")); resourceToFile("teavm-CMakeLists.txt", new File(file.getParent(), "CMakeLists.txt"));
@ -608,8 +715,8 @@ public class TeaVMTestRunner extends Runner implements Filterable {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}; };
return compileTest(method, configuration, this::createCTarget, TestNativeEntryPoint.class.getName(), path, ".c", return compile(configuration, this::createCTarget, TestNativeEntryPoint.class.getName(), path, ".c",
postBuild, true); postBuild, true, additionalProcessing, baseName);
} }
private CTarget createCTarget() { private CTarget createCTarget() {
@ -618,39 +725,50 @@ public class TeaVMTestRunner extends Runner implements Filterable {
return cTarget; return cTarget;
} }
private CompileResult compileToWasm(Method method, TeaVMTestConfiguration<WasmTarget> configuration, private CompileResult compileToWasm(Consumer<TeaVM> additionalProcessing, String baseName,
File path) { TeaVMTestConfiguration<WasmTarget> configuration, File path) {
return compileTest(method, configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path, return compile(configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path,
".wasm", null, false); ".wasm", null, false, additionalProcessing, baseName);
} }
private <T extends TeaVMTarget> CompileResult compileTest(Method method, TeaVMTestConfiguration<T> configuration, private Consumer<TeaVM> singleTest(Method method) {
ClassHolder classHolder = classSource.get(method.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
return vm -> {
Properties properties = new Properties();
applyProperties(method.getDeclaringClass(), properties);
vm.setProperties(properties);
new TestEntryPointTransformerForSingleMethod(methodHolder.getReference(), testClass.getName()).install(vm);
};
}
private Consumer<TeaVM> wholeClass(List<Method> methods) {
return vm -> {
Properties properties = new Properties();
applyProperties(testClass, properties);
vm.setProperties(properties);
List<MethodReference> methodReferences = new ArrayList<>();
for (Method method : methods) {
ClassHolder classHolder = classSource.get(method.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
methodReferences.add(methodHolder.getReference());
}
new TestEntryPointTransformerForWholeClass(methodReferences, testClass.getName()).install(vm);
};
}
private <T extends TeaVMTarget> CompileResult compile(TeaVMTestConfiguration<T> configuration,
Supplier<T> targetSupplier, String entryPoint, File path, String extension, Supplier<T> targetSupplier, String entryPoint, File path, String extension,
CompilePostProcessor postBuild, boolean separateDir) { CompilePostProcessor postBuild, boolean separateDir,
Consumer<TeaVM> additionalProcessing, String baseName) {
CompileResult result = new CompileResult(); CompileResult result = new CompileResult();
StringBuilder simpleName = new StringBuilder(); File outputFile = getOutputFile(path, baseName, configuration.getSuffix(), separateDir, extension);
simpleName.append("test");
String suffix = configuration.getSuffix();
if (!suffix.isEmpty()) {
if (!separateDir) {
simpleName.append('-').append(suffix);
}
}
File outputFile;
if (separateDir) {
outputFile = new File(new File(path, simpleName.toString()), "test" + extension);
} else {
simpleName.append(extension);
outputFile = new File(path, simpleName.toString());
}
result.file = outputFile; result.file = outputFile;
ClassLoader classLoader = TeaVMTestRunner.class.getClassLoader(); ClassLoader classLoader = TeaVMTestRunner.class.getClassLoader();
ClassHolder classHolder = classSource.get(method.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
T target = targetSupplier.get(); T target = targetSupplier.get();
configuration.apply(target); configuration.apply(target);
@ -660,6 +778,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
dependencyAnalyzerFactory = FastDependencyAnalyzer::new; dependencyAnalyzerFactory = FastDependencyAnalyzer::new;
} }
try {
TeaVM vm = new TeaVMBuilder(target) TeaVM vm = new TeaVMBuilder(target)
.setClassLoader(classLoader) .setClassLoader(classLoader)
.setClassSource(classSource) .setClassSource(classSource)
@ -667,15 +786,11 @@ public class TeaVMTestRunner extends Runner implements Filterable {
.setDependencyAnalyzerFactory(dependencyAnalyzerFactory) .setDependencyAnalyzerFactory(dependencyAnalyzerFactory)
.build(); .build();
Properties properties = new Properties();
applyProperties(method.getDeclaringClass(), properties);
vm.setProperties(properties);
configuration.apply(vm); configuration.apply(vm);
additionalProcessing.accept(vm);
vm.installPlugins(); vm.installPlugins();
new TestExceptionPlugin().install(vm); new TestExceptionPlugin().install(vm);
new TestEntryPointTransformer(methodHolder.getReference(), testClass.getName()).install(vm);
vm.entryPoint(entryPoint); vm.entryPoint(entryPoint);
@ -697,6 +812,31 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
return result; return result;
} catch (Exception e) {
result = new CompileResult();
result.success = false;
result.throwable = e;
return result;
}
}
private File getOutputFile(File path, String baseName, String suffix, boolean separateDir, String extension) {
StringBuilder simpleName = new StringBuilder();
simpleName.append(baseName);
if (!suffix.isEmpty()) {
if (!separateDir) {
simpleName.append('-').append(suffix);
}
}
File outputFile;
if (separateDir) {
outputFile = new File(new File(path, simpleName.toString()), "test" + extension);
} else {
simpleName.append(extension);
outputFile = new File(path, simpleName.toString());
}
return outputFile;
} }
interface CompilePostProcessor { interface CompilePostProcessor {
@ -796,13 +936,92 @@ public class TeaVMTestRunner extends Runner implements Filterable {
} }
} }
private void writeRunsDescriptor() {
if (runsInCurrentClass.isEmpty()) {
return;
}
File outputDir = getOutputPathForClass();
outputDir.mkdirs();
File descriptorFile = new File(outputDir, "tests.json");
try (OutputStream output = new FileOutputStream(descriptorFile);
OutputStream bufferedOutput = new BufferedOutputStream(output);
Writer writer = new OutputStreamWriter(bufferedOutput)) {
writer.write("[\n");
boolean first = true;
for (TestRun run : runsInCurrentClass) {
if (!first) {
writer.write(",\n");
}
first = false;
writer.write(" {\n");
writer.write(" \"baseDir\": ");
writeJsonString(writer, run.getBaseDirectory().getAbsolutePath().replace('\\', '/'));
writer.write(",\n");
writer.write(" \"fileName\": ");
writeJsonString(writer, run.getFileName());
writer.write(",\n");
writer.write(" \"kind\": \"" + run.getKind().name() + "\"");
if (run.getArgument() != null) {
writer.write(",\n");
writer.write(" \"argument\": ");
writeJsonString(writer, run.getArgument());
}
writer.write("\n }");
}
writer.write("\n]");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void writeJsonString(Writer writer, String s) throws IOException {
writer.write('"');
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
switch (c) {
case '"':
writer.write("\\\"");
case '\\':
writer.write("\\\\");
break;
case '\r':
writer.write("\\r");
break;
case '\n':
writer.write("\\n");
break;
case '\t':
writer.write("\\t");
break;
case '\f':
writer.write("\\f");
break;
case '\b':
writer.write("\\b");
break;
default:
if (c < ' ') {
writer.write("\\u00");
writer.write(hex(c / 16));
writer.write(hex(c % 16));
} else {
writer.write(c);
}
break;
}
}
writer.write('"');
}
private static char hex(int digit) {
return (char) (digit < 10 ? '0' + digit : 'A' + digit - 10);
}
static class CompileResult { static class CompileResult {
boolean success = true; boolean success = true;
String errorMessage; String errorMessage;
File file; File file;
} Throwable throwable;
interface CompileFunction {
CompileResult compile(Method method);
} }
} }

View File

@ -21,10 +21,10 @@ final class TestEntryPoint {
private TestEntryPoint() { private TestEntryPoint() {
} }
public static void run() throws Exception { public static void run(String name) throws Exception {
before(); before();
try { try {
launchTest(); launchTest(name);
} finally { } finally {
try { try {
after(); after();
@ -36,11 +36,11 @@ final class TestEntryPoint {
private static native void before(); private static native void before();
private static native void launchTest() throws Exception; private static native void launchTest(String name) throws Exception;
private static native void after(); private static native void after();
public static void main(String[] args) throws Throwable { public static void main(String[] args) throws Throwable {
run(); run(args.length == 1 ? args[0] : null);
} }
} }

View File

@ -45,12 +45,10 @@ import org.teavm.model.emit.ValueEmitter;
import org.teavm.vm.spi.TeaVMHost; import org.teavm.vm.spi.TeaVMHost;
import org.teavm.vm.spi.TeaVMPlugin; import org.teavm.vm.spi.TeaVMPlugin;
class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin { abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
private MethodReference testMethod;
private String testClassName; private String testClassName;
TestEntryPointTransformer(MethodReference testMethod, String testClassName) { TestEntryPointTransformer(String testClassName) {
this.testMethod = testMethod;
this.testClassName = testClassName; this.testClassName = testClassName;
} }
@ -91,11 +89,11 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
}); });
ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class); ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class);
if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testMethod.getClassName(), false)) { if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testClassName, false)) {
testCaseVar.cast(ValueType.object(JUNIT3_BASE_CLASS)).invokeVirtual(JUNIT3_BEFORE); testCaseVar.cast(ValueType.object(JUNIT3_BASE_CLASS)).invokeVirtual(JUNIT3_BEFORE);
} }
List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testMethod.getClassName()); List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testClassName);
Collections.reverse(classes); Collections.reverse(classes);
classes.stream() classes.stream()
.flatMap(cls -> cls.getMethods().stream()) .flatMap(cls -> cls.getMethods().stream())
@ -110,13 +108,13 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy); ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class); ValueEmitter testCaseVar = pe.getField(TestEntryPoint.class, "testCase", Object.class);
List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testMethod.getClassName()); List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testClassName);
classes.stream() classes.stream()
.flatMap(cls -> cls.getMethods().stream()) .flatMap(cls -> cls.getMethods().stream())
.filter(m -> m.getAnnotations().get(JUNIT4_AFTER) != null) .filter(m -> m.getAnnotations().get(JUNIT4_AFTER) != null)
.forEach(m -> testCaseVar.cast(ValueType.object(m.getOwnerName())).invokeVirtual(m.getReference())); .forEach(m -> testCaseVar.cast(ValueType.object(m.getOwnerName())).invokeVirtual(m.getReference()));
if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testMethod.getClassName(), false)) { if (hierarchy.isSuperType(JUNIT3_BASE_CLASS, testClassName, false)) {
testCaseVar.cast(ValueType.object(JUNIT3_BASE_CLASS)).invokeVirtual(JUNIT3_AFTER); testCaseVar.cast(ValueType.object(JUNIT3_BASE_CLASS)).invokeVirtual(JUNIT3_AFTER);
} }
@ -137,8 +135,10 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
return result; return result;
} }
private Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) { protected abstract Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy);
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
protected final void generateSingleMethodLaunchProgram(MethodReference testMethod,
ClassHierarchy hierarchy, ProgramEmitter pe) {
pe.getField(TestEntryPoint.class, "testCase", Object.class) pe.getField(TestEntryPoint.class, "testCase", Object.class)
.cast(ValueType.object(testMethod.getClassName())) .cast(ValueType.object(testMethod.getClassName()))
.invokeSpecial(testMethod); .invokeSpecial(testMethod);
@ -163,6 +163,5 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
} else { } else {
pe.exit(); pe.exit();
} }
return pe.getProgram();
} }
} }

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 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.model.ClassHierarchy;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.emit.ProgramEmitter;
class TestEntryPointTransformerForSingleMethod extends TestEntryPointTransformer {
private MethodReference testMethod;
TestEntryPointTransformerForSingleMethod(MethodReference testMethod, String testClassName) {
super(testClassName);
this.testMethod = testMethod;
}
@Override
protected Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
generateSingleMethodLaunchProgram(testMethod, hierarchy, pe);
return pe.getProgram();
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 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 java.util.List;
import org.teavm.model.ClassHierarchy;
import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReference;
import org.teavm.model.Program;
import org.teavm.model.emit.ForkEmitter;
import org.teavm.model.emit.ProgramEmitter;
import org.teavm.model.emit.ValueEmitter;
import org.teavm.model.instructions.BranchingCondition;
class TestEntryPointTransformerForWholeClass extends TestEntryPointTransformer {
private List<MethodReference> testMethods;
TestEntryPointTransformerForWholeClass(List<MethodReference> testMethods, String testClassName) {
super(testClassName);
this.testMethods = testMethods;
}
@Override
protected Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
ValueEmitter testName = pe.var(1, String.class);
for (MethodReference testMethod : testMethods) {
ValueEmitter isTest = testName.invokeSpecial("equals", boolean.class,
pe.constant(testMethod.toString()).cast(Object.class));
ForkEmitter fork = isTest.fork(BranchingCondition.NOT_EQUAL);
pe.enter(pe.getProgram().createBasicBlock());
fork.setThen(pe.getBlock());
generateSingleMethodLaunchProgram(testMethod, hierarchy, pe);
pe.enter(pe.getProgram().createBasicBlock());
fork.setElse(pe.getBlock());
}
pe.construct(IllegalArgumentException.class, pe.constant("Invalid test name")).raise();
return pe.getProgram();
}
}

View File

@ -25,7 +25,7 @@ final class TestNativeEntryPoint {
public static void main(String[] args) { public static void main(String[] args) {
try { try {
TestEntryPoint.run(); TestEntryPoint.run(args.length > 0 ? args[0] : null);
new PrintStream(StdoutOutputStream.INSTANCE).println("SUCCESS"); new PrintStream(StdoutOutputStream.INSTANCE).println("SUCCESS");
} catch (Throwable e) { } catch (Throwable e) {
PrintStream out = new PrintStream(StderrOutputStream.INSTANCE); PrintStream out = new PrintStream(StderrOutputStream.INSTANCE);

View File

@ -26,14 +26,16 @@ class TestRun {
private String fileName; private String fileName;
private RunKind kind; private RunKind kind;
private TestRunCallback callback; private TestRunCallback callback;
private String argument;
TestRun(File baseDirectory, Method method, Description description, String fileName, RunKind kind, TestRun(File baseDirectory, Method method, Description description, String fileName, RunKind kind,
TestRunCallback callback) { String argument, TestRunCallback callback) {
this.baseDirectory = baseDirectory; this.baseDirectory = baseDirectory;
this.method = method; this.method = method;
this.description = description; this.description = description;
this.fileName = fileName; this.fileName = fileName;
this.kind = kind; this.kind = kind;
this.argument = argument;
this.callback = callback; this.callback = callback;
} }
@ -57,6 +59,10 @@ class TestRun {
return kind; return kind;
} }
public String getArgument() {
return argument;
}
public TestRunCallback getCallback() { public TestRunCallback getCallback() {
return callback; return callback;
} }

View File

@ -0,0 +1,26 @@
/*
* Copyright 2020 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 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 WholeClassCompilation {
}

View File

@ -1,8 +1,8 @@
var $rt_decodeStack; var $rt_decodeStack;
function runMain(stackDecoder, callback) { function runMain(argument, stackDecoder, callback) {
$rt_decodeStack = stackDecoder; $rt_decodeStack = stackDecoder;
main([], function(result) { main(argument !== null ? [argument] : [], function(result) {
var message = {}; var message = {};
if (result instanceof Error) { if (result instanceof Error) {
makeErrorMessage(message, result); makeErrorMessage(message, result);

View File

@ -5,6 +5,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head> </head>
<body> <body>
<script type="text/javascript" src="classTest.js"></script>
<script type="text/javascript" src="test.js"></script> <script type="text/javascript" src="test.js"></script>
<script type="text/javascript"> <script type="text/javascript">
main([], function(result) { main([], function(result) {