From 95426e215912c98436143bc0159ff282d02538df Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 27 Feb 2020 18:43:08 +0300 Subject: [PATCH] Add per-class compilation when running tests (requires `@WholeClassCompilation` annotation) --- tests/src/test/js/frame.js | 20 +- tests/src/test/js/src/run-tests.js | 73 ++- .../java/org/teavm/junit/CRunStrategy.java | 41 +- .../org/teavm/junit/HtmlUnitRunStrategy.java | 9 +- .../java/org/teavm/junit/TeaVMTestRunner.java | 473 +++++++++++++----- .../java/org/teavm/junit/TestEntryPoint.java | 8 +- .../junit/TestEntryPointTransformer.java | 21 +- ...tEntryPointTransformerForSingleMethod.java | 38 ++ ...estEntryPointTransformerForWholeClass.java | 57 +++ .../org/teavm/junit/TestNativeEntryPoint.java | 2 +- .../main/java/org/teavm/junit/TestRun.java | 8 +- .../teavm/junit/WholeClassCompilation.java | 26 + .../main/resources/teavm-htmlunit-adapter.js | 4 +- .../src/main/resources/teavm-run-test.html | 1 + 14 files changed, 573 insertions(+), 208 deletions(-) create mode 100644 tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java create mode 100644 tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java create mode 100644 tools/junit/src/main/java/org/teavm/junit/WholeClassCompilation.java diff --git a/tests/src/test/js/frame.js b/tests/src/test/js/frame.js index d81aaedb3..5ce2fa112 100644 --- a/tests/src/test/js/frame.js +++ b/tests/src/test/js/frame.js @@ -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 = ""; diff --git a/tests/src/test/js/src/run-tests.js b/tests/src/test/js/src/run-tests.js index c7a210831..e9a793c4b 100644 --- a/tests/src/test/js/src/run-tests.js +++ b/tests/src/test/js/src/run-tests.js @@ -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,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); - 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; - } - totalTests++; + 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); - } - })); } + await Promise.all(files.map(async file => { + const filePath = path + "/" + file; + const stat = await fs.stat(rootDir + "/" + filePath); + if (stat.isDirectory()) { + const childSuite = new TestSuite(file); + suite.testSuites.push(childSuite); + await walkDir(filePath, childSuite); + } + })); } class TestRunner { @@ -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; diff --git a/tools/junit/src/main/java/org/teavm/junit/CRunStrategy.java b/tools/junit/src/main/java/org/teavm/junit/CRunStrategy.java index 08cfe4489..9cd3cd079 100644 --- a/tools/junit/src/main/java/org/teavm/junit/CRunStrategy.java +++ b/tools/junit/src/main/java/org/teavm/junit/CRunStrategy.java @@ -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 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 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 runtimeOutput = new ArrayList<>(); List stdout = new ArrayList<>(); outputFile.setExecutable(true); - runProcess(new ProcessBuilder(outputFile.getPath()).start(), runtimeOutput, stdout); + List 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 compilerOutput = new ArrayList<>(); + boolean compilerSuccess = runCompiler(inputDir, compilerOutput); + writeLines(compilerOutput); + return compilerSuccess; + } + private boolean runCompiler(File inputDir, List 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; + } } diff --git a/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java b/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java index 78a4a99f2..8bf7fe430 100644 --- a/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java +++ b/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java @@ -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; diff --git a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java index 0527e0c70..fa6a95189 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java +++ b/tools/junit/src/main/java/org/teavm/junit/TeaVMTestRunner.java @@ -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 filteredChildren; private ReferenceCache referenceCache = new ReferenceCache(); + private boolean classCompilationOk; + private List 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 children, RunNotifier notifier) { + File outputPath = getOutputPathForClass(); + boolean hasErrors = false; + Description description = getDescription(); + + for (TeaVMTestConfiguration configuration : getJavaScriptConfigurations()) { + CompileResult result = compileToJs(wholeClass(children), "classTest", configuration, outputPath); + if (!result.success) { + hasErrors = true; + notifier.fireTestFailure(createFailure(description, result)); + } + } + + for (TeaVMTestConfiguration configuration : getCConfigurations()) { + CompileResult result = compileToC(wholeClass(children), "classTest", configuration, outputPath); + if (!result.success) { + hasErrors = true; + notifier.fireTestFailure(createFailure(description, result)); + } + } + + for (TeaVMTestConfiguration 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> onSuccess = new ArrayList<>(); List runs = new ArrayList<>(); - onSuccess.add(runSuccess -> { + Consumer 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 configuration : getJavaScriptConfigurations()) { - TestRun run = compile(child, notifier, RunKind.JAVASCRIPT, - m -> compileToJs(m, configuration, outputPath), onSuccess.get(0)); - if (run != null) { - runs.add(run); - } + if (isWholeClassCompilation) { + if (!classCompilationOk) { + notifier.fireTestFinished(description); + notifier.fireTestFailure(new Failure(description, + new AssertionError("Could not compile test class"))); + latch.countDown(); + } else { + runTestsFromWholeClass(child, notifier, runs, onSuccess); + onSuccess.accept(true); } - - for (TeaVMTestConfiguration 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 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); - latch.countDown(); - return; + } 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 runs, + Consumer 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 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 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 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 runs, Consumer onSuccess) { + try { + File outputPath = getOutputPath(child); + copyJsFilesTo(outputPath); + + for (TeaVMTestConfiguration 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 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 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,24 +563,23 @@ public class TeaVMTestRunner extends Runner implements Filterable { } } - private TestRun compile(Method child, RunNotifier notifier, RunKind kind, - CompileFunction compiler, Consumer onComplete) { + private TestRun prepareRun(Method child, CompileResult result, RunNotifier notifier, RunKind kind, + Consumer 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 onComplete) { + Description description = describeChild(child); TestRunCallback callback = new TestRunCallback() { @Override @@ -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 configuration, - File path) { + private CompileResult compileToJs(Consumer additionalProcessing, String baseName, + TeaVMTestConfiguration configuration, File path) { boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true")); DebugInformationBuilder debugEmitter = new DebugInformationBuilder(new ReferenceCache()); Supplier 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 configuration, - File path) { + private CompileResult compileToC(Consumer additionalProcessing, String baseName, + TeaVMTestConfiguration 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,20 +725,104 @@ public class TeaVMTestRunner extends Runner implements Filterable { return cTarget; } - private CompileResult compileToWasm(Method method, TeaVMTestConfiguration configuration, - File path) { - return compileTest(method, configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path, - ".wasm", null, false); + private CompileResult compileToWasm(Consumer additionalProcessing, String baseName, + TeaVMTestConfiguration configuration, File path) { + return compile(configuration, WasmTarget::new, TestNativeEntryPoint.class.getName(), path, + ".wasm", null, false, additionalProcessing, baseName); } - private CompileResult compileTest(Method method, TeaVMTestConfiguration configuration, + private Consumer 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 wholeClass(List methods) { + return vm -> { + Properties properties = new Properties(); + applyProperties(testClass, properties); + vm.setProperties(properties); + List 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 CompileResult compile(TeaVMTestConfiguration configuration, Supplier targetSupplier, String entryPoint, File path, String extension, - CompilePostProcessor postBuild, boolean separateDir) { + CompilePostProcessor postBuild, boolean separateDir, + Consumer additionalProcessing, String baseName) { CompileResult result = new CompileResult(); + File outputFile = getOutputFile(path, baseName, configuration.getSuffix(), separateDir, extension); + result.file = outputFile; + + ClassLoader classLoader = TeaVMTestRunner.class.getClassLoader(); + + T target = targetSupplier.get(); + configuration.apply(target); + + DependencyAnalyzerFactory dependencyAnalyzerFactory = PreciseDependencyAnalyzer::new; + boolean fastAnalysis = Boolean.parseBoolean(System.getProperty(FAST_ANALYSIS)); + if (fastAnalysis) { + dependencyAnalyzerFactory = FastDependencyAnalyzer::new; + } + + try { + TeaVM vm = new TeaVMBuilder(target) + .setClassLoader(classLoader) + .setClassSource(classSource) + .setReferenceCache(referenceCache) + .setDependencyAnalyzerFactory(dependencyAnalyzerFactory) + .build(); + + configuration.apply(vm); + additionalProcessing.accept(vm); + vm.installPlugins(); + + new TestExceptionPlugin().install(vm); + + vm.entryPoint(entryPoint); + + if (fastAnalysis) { + vm.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE); + vm.addVirtualMethods(m -> true); + } + if (!outputFile.getParentFile().exists()) { + outputFile.getParentFile().mkdirs(); + } + vm.build(new DirectoryBuildTarget(outputFile.getParentFile()), outputFile.getName()); + if (!vm.getProblemProvider().getProblems().isEmpty()) { + result.success = false; + result.errorMessage = buildErrorMessage(vm); + } else { + if (postBuild != null) { + postBuild.process(vm, outputFile); + } + } + + 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("test"); - String suffix = configuration.getSuffix(); + simpleName.append(baseName); if (!suffix.isEmpty()) { if (!separateDir) { simpleName.append('-').append(suffix); @@ -644,59 +835,8 @@ public class TeaVMTestRunner extends Runner implements Filterable { simpleName.append(extension); outputFile = new File(path, simpleName.toString()); } - 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); - - DependencyAnalyzerFactory dependencyAnalyzerFactory = PreciseDependencyAnalyzer::new; - boolean fastAnalysis = Boolean.parseBoolean(System.getProperty(FAST_ANALYSIS)); - if (fastAnalysis) { - dependencyAnalyzerFactory = FastDependencyAnalyzer::new; - } - - TeaVM vm = new TeaVMBuilder(target) - .setClassLoader(classLoader) - .setClassSource(classSource) - .setReferenceCache(referenceCache) - .setDependencyAnalyzerFactory(dependencyAnalyzerFactory) - .build(); - - Properties properties = new Properties(); - applyProperties(method.getDeclaringClass(), properties); - vm.setProperties(properties); - - configuration.apply(vm); - vm.installPlugins(); - - new TestExceptionPlugin().install(vm); - new TestEntryPointTransformer(methodHolder.getReference(), testClass.getName()).install(vm); - - vm.entryPoint(entryPoint); - - if (fastAnalysis) { - vm.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE); - vm.addVirtualMethods(m -> true); - } - if (!outputFile.getParentFile().exists()) { - outputFile.getParentFile().mkdirs(); - } - vm.build(new DirectoryBuildTarget(outputFile.getParentFile()), outputFile.getName()); - if (!vm.getProblemProvider().getProblems().isEmpty()) { - result.success = false; - result.errorMessage = buildErrorMessage(vm); - } else { - if (postBuild != null) { - postBuild.process(vm, outputFile); - } - } - - return result; + 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; } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java index 296be8331..8187aa05b 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java @@ -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); } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java index 81d45bc9f..326010ae9 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformer.java @@ -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 classes = collectSuperClasses(pe.getClassSource(), testMethod.getClassName()); + List 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 classes = collectSuperClasses(pe.getClassSource(), testMethod.getClassName()); + List 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(); } } diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java new file mode 100644 index 000000000..713ee8cd3 --- /dev/null +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForSingleMethod.java @@ -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(); + } +} diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java new file mode 100644 index 000000000..4e4d6665b --- /dev/null +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPointTransformerForWholeClass.java @@ -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 testMethods; + + TestEntryPointTransformerForWholeClass(List 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(); + } +} diff --git a/tools/junit/src/main/java/org/teavm/junit/TestNativeEntryPoint.java b/tools/junit/src/main/java/org/teavm/junit/TestNativeEntryPoint.java index df4086b70..508d31256 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestNativeEntryPoint.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestNativeEntryPoint.java @@ -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); diff --git a/tools/junit/src/main/java/org/teavm/junit/TestRun.java b/tools/junit/src/main/java/org/teavm/junit/TestRun.java index 88f8b9feb..fb8f4dfd8 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestRun.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestRun.java @@ -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; } diff --git a/tools/junit/src/main/java/org/teavm/junit/WholeClassCompilation.java b/tools/junit/src/main/java/org/teavm/junit/WholeClassCompilation.java new file mode 100644 index 000000000..7de876c91 --- /dev/null +++ b/tools/junit/src/main/java/org/teavm/junit/WholeClassCompilation.java @@ -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 { +} diff --git a/tools/junit/src/main/resources/teavm-htmlunit-adapter.js b/tools/junit/src/main/resources/teavm-htmlunit-adapter.js index 384b7b28e..665a29526 100644 --- a/tools/junit/src/main/resources/teavm-htmlunit-adapter.js +++ b/tools/junit/src/main/resources/teavm-htmlunit-adapter.js @@ -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); diff --git a/tools/junit/src/main/resources/teavm-run-test.html b/tools/junit/src/main/resources/teavm-run-test.html index e2d8a8bed..b1d3a2a27 100644 --- a/tools/junit/src/main/resources/teavm-run-test.html +++ b/tools/junit/src/main/resources/teavm-run-test.html @@ -5,6 +5,7 @@ +