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 => {
let request = event.data;
switch (request.type) {
case "js":
appendFiles(request.files, 0, () => {
launchTest(response => {
case "JAVASCRIPT":
appendFiles([request.file], 0, () => {
launchTest(request.argument, response => {
event.source.postMessage(response, "*");
});
}, 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 => {
case "WASM":
const runtimeFile = request.file + "-runtime.js";
appendFiles([runtimeFile], 0, () => {
launchWasmTest(request.file, equest.argument, response => {
event.source.postMessage(response, "*");
});
}, error => {
@ -57,8 +59,8 @@ function appendFiles(files, index, callback, errorCallback) {
}
}
function launchTest(callback) {
main([], result => {
function launchTest(argument, callback) {
main(argument ? [argument] : [], result => {
if (result instanceof Error) {
callback({
status: "failed",
@ -81,7 +83,7 @@ function launchTest(callback) {
}
}
function launchWasmTest(path, callback) {
function launchWasmTest(path, argument, callback) {
var output = [];
var outputBuffer = "";

View File

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

View File

@ -20,11 +20,16 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
class CRunStrategy implements TestRunStrategy {
private String compilerCommand;
private ConcurrentMap<String, Compilation> compilationMap = new ConcurrentHashMap<>();
CRunStrategy(String compilerCommand) {
this.compilerCommand = compilerCommand;
@ -47,18 +52,21 @@ class CRunStrategy implements TestRunStrategy {
}
File outputFile = new File(run.getBaseDirectory(), exeName);
List<String> compilerOutput = new ArrayList<>();
boolean compilerSuccess = runCompiler(run.getBaseDirectory(), compilerOutput);
boolean compilerSuccess = compile(run.getBaseDirectory());
if (!compilerSuccess) {
run.getCallback().error(new RuntimeException("C compiler error:\n" + mergeLines(compilerOutput)));
run.getCallback().error(new RuntimeException("C compiler error"));
return;
}
writeLines(compilerOutput);
List<String> runtimeOutput = new ArrayList<>();
List<String> stdout = new ArrayList<>();
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")) {
writeLines(runtimeOutput);
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)
throws IOException, InterruptedException {
String command = new File(compilerCommand).getAbsolutePath();
@ -133,4 +159,9 @@ class CRunStrategy implements TestRunStrategy {
output.addAll(lines);
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.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
@ -64,7 +65,6 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
throw new RuntimeException(e);
}
HtmlPage pageRef = page.get();
pageRef.executeJavaScript(readFile(new File(run.getBaseDirectory(), run.getFileName())));
boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true"));
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"))
.getJavaScriptResult();
Object[] args = new Object[] {
run.getArgument(),
decodeStack ? createStackDecoderFunction(resultParser) : null,
new NativeJavaObject(function, asyncResult, AsyncResult.class)
};
@ -161,7 +162,7 @@ class HtmlUnitRunStrategy implements TestRunStrategy {
private String readFile(File file) throws IOException {
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) {
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 Object result;

View File

@ -16,6 +16,7 @@
package org.teavm.junit;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -109,6 +110,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private static final int stopTimeout = 15000;
private Class<?> testClass;
private boolean isWholeClassCompilation;
private ClassHolderSource classSource;
private ClassLoader classLoader;
private Description suiteDescription;
@ -120,6 +122,8 @@ public class TeaVMTestRunner extends Runner implements Filterable {
private CountDownLatch latch;
private List<Method> filteredChildren;
private ReferenceCache referenceCache = new ReferenceCache();
private boolean classCompilationOk;
private List<TestRun> runsInCurrentClass = new ArrayList<>();
static class RunnerKindInfo {
volatile TestRunner runner;
@ -193,10 +197,17 @@ public class TeaVMTestRunner extends Runner implements Filterable {
latch = new CountDownLatch(children.size());
notifier.fireTestStarted(getDescription());
isWholeClassCompilation = testClass.isAnnotationPresent(WholeClassCompilation.class);
if (isWholeClassCompilation) {
classCompilationOk = compileWholeClass(children, notifier);
}
for (Method child : children) {
runChild(child, notifier);
}
writeRunsDescriptor();
runsInCurrentClass.clear();
while (true) {
try {
if (latch.await(1000, TimeUnit.MILLISECONDS)) {
@ -246,6 +257,38 @@ public class TeaVMTestRunner extends Runner implements Filterable {
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) {
Description description = describeChild(child);
notifier.fireTestStarted(description);
@ -273,62 +316,37 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
}
if (!child.isAnnotationPresent(SkipJVM.class)
&& !testClass.isAnnotationPresent(SkipJVM.class)) {
if (!child.isAnnotationPresent(SkipJVM.class) && !testClass.isAnnotationPresent(SkipJVM.class)) {
ran = true;
success = runInJvm(child, notifier, expectedExceptions);
}
if (success && outputDir != null) {
int[] configurationIndex = new int[] { 0 };
List<Consumer<Boolean>> onSuccess = new ArrayList<>();
List<TestRun> runs = new ArrayList<>();
onSuccess.add(runSuccess -> {
Consumer<Boolean> onSuccess = runSuccess -> {
if (runSuccess && configurationIndex[0] < runs.size()) {
submitRun(runs.get(configurationIndex[0]++));
} else {
notifier.fireTestFinished(description);
latch.countDown();
}
});
};
try {
File outputPath = getOutputPath(child);
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));
if (isWholeClassCompilation) {
if (!classCompilationOk) {
notifier.fireTestFinished(description);
notifier.fireTestFailure(new Failure(description,
new AssertionError("Could not compile test class")));
latch.countDown();
return;
} else {
runTestsFromWholeClass(child, notifier, runs, onSuccess);
onSuccess.accept(true);
}
} else {
runCompiledTest(child, notifier, runs, onSuccess);
}
onSuccess.get(0).accept(true);
} else {
if (!ran) {
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) {
AnnotationHolder annot = method.getAnnotations().get(JUNIT4_TEST);
if (annot == null) {
@ -438,7 +531,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
void run() throws Throwable;
}
class JUnit4Runner implements Runner {
static class JUnit4Runner implements Runner {
Object instance;
Method child;
@ -457,7 +550,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
}
class JUnit3Runner implements Runner {
static class JUnit3Runner implements Runner {
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,
CompileFunction compiler, Consumer<Boolean> onComplete) {
private TestRun prepareRun(Method child, CompileResult result, RunNotifier notifier, RunKind kind,
Consumer<Boolean> onComplete) {
Description description = describeChild(child);
CompileResult compileResult;
try {
compileResult = compiler.compile(child);
} catch (Exception e) {
notifier.fireTestFailure(new Failure(description, e));
if (!result.success) {
notifier.fireTestFailure(createFailure(description, result));
notifier.fireTestFinished(description);
latch.countDown();
return null;
}
if (!compileResult.success) {
notifier.fireTestFailure(new Failure(description, new AssertionError(compileResult.errorMessage)));
return null;
return createTestRun(result.file, child, kind, null, notifier, onComplete);
}
private TestRun createTestRun(File file, Method child, RunKind kind, String argument, RunNotifier notifier,
Consumer<Boolean> onComplete) {
Description description = describeChild(child);
TestRunCallback callback = new TestRunCallback() {
@Override
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(),
kind, callback);
return new TestRun(file.getParentFile(), child, description, file.getName(), kind,
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) {
synchronized (TeaVMTestRunner.class) {
runsInCurrentClass.add(run);
RunnerKindInfo info = runners.get(run.getKind());
if (info.strategy == null) {
@ -552,14 +653,20 @@ public class TeaVMTestRunner extends Runner implements Filterable {
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 {
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,
File path) {
private CompileResult compileToJs(Consumer<TeaVM> additionalProcessing, String baseName,
TeaVMTestConfiguration<JavaScriptTarget> configuration, File path) {
boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true"));
DebugInformationBuilder debugEmitter = new DebugInformationBuilder(new ReferenceCache());
Supplier<JavaScriptTarget> targetSupplier = () -> {
@ -595,12 +702,12 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
};
}
return compileTest(method, configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js",
postBuild, false);
return compile(configuration, targetSupplier, TestEntryPoint.class.getName(), path, ".js",
postBuild, false, additionalProcessing, baseName);
}
private CompileResult compileToC(Method method, TeaVMTestConfiguration<CTarget> configuration,
File path) {
private CompileResult compileToC(Consumer<TeaVM> additionalProcessing, String baseName,
TeaVMTestConfiguration<CTarget> configuration, File path) {
CompilePostProcessor postBuild = (vm, file) -> {
try {
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);
}
};
return compileTest(method, configuration, this::createCTarget, TestNativeEntryPoint.class.getName(), path, ".c",
postBuild, true);
return compile(configuration, this::createCTarget, TestNativeEntryPoint.class.getName(), path, ".c",
postBuild, true, additionalProcessing, baseName);
}
private CTarget createCTarget() {
@ -618,39 +725,50 @@ public class TeaVMTestRunner extends Runner implements Filterable {
return cTarget;
}
private CompileResult compileToWasm(Method method, TeaVMTestConfiguration<WasmTarget> configuration,
File path) {
return compileTest(method, configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path,
".wasm", null, false);
private CompileResult compileToWasm(Consumer<TeaVM> additionalProcessing, String baseName,
TeaVMTestConfiguration<WasmTarget> configuration, File path) {
return compile(configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path,
".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,
CompilePostProcessor postBuild, boolean separateDir) {
CompilePostProcessor postBuild, boolean separateDir,
Consumer<TeaVM> additionalProcessing, String baseName) {
CompileResult result = new CompileResult();
StringBuilder simpleName = new StringBuilder();
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());
}
File outputFile = getOutputFile(path, baseName, configuration.getSuffix(), separateDir, extension);
result.file = outputFile;
ClassLoader classLoader = TeaVMTestRunner.class.getClassLoader();
ClassHolder classHolder = classSource.get(method.getDeclaringClass().getName());
MethodHolder methodHolder = classHolder.getMethod(getDescriptor(method));
T target = targetSupplier.get();
configuration.apply(target);
@ -660,6 +778,7 @@ public class TeaVMTestRunner extends Runner implements Filterable {
dependencyAnalyzerFactory = FastDependencyAnalyzer::new;
}
try {
TeaVM vm = new TeaVMBuilder(target)
.setClassLoader(classLoader)
.setClassSource(classSource)
@ -667,15 +786,11 @@ public class TeaVMTestRunner extends Runner implements Filterable {
.setDependencyAnalyzerFactory(dependencyAnalyzerFactory)
.build();
Properties properties = new Properties();
applyProperties(method.getDeclaringClass(), properties);
vm.setProperties(properties);
configuration.apply(vm);
additionalProcessing.accept(vm);
vm.installPlugins();
new TestExceptionPlugin().install(vm);
new TestEntryPointTransformer(methodHolder.getReference(), testClass.getName()).install(vm);
vm.entryPoint(entryPoint);
@ -697,6 +812,31 @@ public class TeaVMTestRunner extends Runner implements Filterable {
}
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 {
@ -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 {
boolean success = true;
String errorMessage;
File file;
}
interface CompileFunction {
CompileResult compile(Method method);
Throwable throwable;
}
}

View File

@ -21,10 +21,10 @@ final class TestEntryPoint {
private TestEntryPoint() {
}
public static void run() throws Exception {
public static void run(String name) throws Exception {
before();
try {
launchTest();
launchTest(name);
} finally {
try {
after();
@ -36,11 +36,11 @@ final class TestEntryPoint {
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();
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.TeaVMPlugin;
class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
private MethodReference testMethod;
abstract class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
private String testClassName;
TestEntryPointTransformer(MethodReference testMethod, String testClassName) {
this.testMethod = testMethod;
TestEntryPointTransformer(String testClassName) {
this.testClassName = testClassName;
}
@ -91,11 +89,11 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
});
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);
}
List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testMethod.getClassName());
List<ClassReader> classes = collectSuperClasses(pe.getClassSource(), testClassName);
Collections.reverse(classes);
classes.stream()
.flatMap(cls -> cls.getMethods().stream())
@ -110,13 +108,13 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
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()
.flatMap(cls -> cls.getMethods().stream())
.filter(m -> m.getAnnotations().get(JUNIT4_AFTER) != null)
.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);
}
@ -137,8 +135,10 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
return result;
}
private Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy) {
ProgramEmitter pe = ProgramEmitter.create(method, hierarchy);
protected abstract Program generateLaunchProgram(MethodHolder method, ClassHierarchy hierarchy);
protected final void generateSingleMethodLaunchProgram(MethodReference testMethod,
ClassHierarchy hierarchy, ProgramEmitter pe) {
pe.getField(TestEntryPoint.class, "testCase", Object.class)
.cast(ValueType.object(testMethod.getClassName()))
.invokeSpecial(testMethod);
@ -163,6 +163,5 @@ class TestEntryPointTransformer implements ClassHolderTransformer, TeaVMPlugin {
} else {
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) {
try {
TestEntryPoint.run();
TestEntryPoint.run(args.length > 0 ? args[0] : null);
new PrintStream(StdoutOutputStream.INSTANCE).println("SUCCESS");
} catch (Throwable e) {
PrintStream out = new PrintStream(StderrOutputStream.INSTANCE);

View File

@ -26,14 +26,16 @@ class TestRun {
private String fileName;
private RunKind kind;
private TestRunCallback callback;
private String argument;
TestRun(File baseDirectory, Method method, Description description, String fileName, RunKind kind,
TestRunCallback callback) {
String argument, TestRunCallback callback) {
this.baseDirectory = baseDirectory;
this.method = method;
this.description = description;
this.fileName = fileName;
this.kind = kind;
this.argument = argument;
this.callback = callback;
}
@ -57,6 +59,10 @@ class TestRun {
return kind;
}
public String getArgument() {
return argument;
}
public TestRunCallback getCallback() {
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;
function runMain(stackDecoder, callback) {
function runMain(argument, stackDecoder, callback) {
$rt_decodeStack = stackDecoder;
main([], function(result) {
main(argument !== null ? [argument] : [], function(result) {
var message = {};
if (result instanceof Error) {
makeErrorMessage(message, result);

View File

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