package com.oracle.truffle.js.test.suite;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.APPLICATION_MIME_TYPE;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.MODULE_MIME_TYPE;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.junit.Assert;
import org.junit.internal.TextListener;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;
import com.oracle.truffle.js.runtime.JSContextOptions;
import com.oracle.truffle.js.test.JSTest;
import com.oracle.truffle.js.test.polyglot.PolyglotBuiltinTest;
import com.oracle.truffle.js.test.suite.JSTestRunner.TestCase;
public final class JSTestRunner extends ParentRunner<TestCase> {
private static final String FIXTURE_DIR = File.separator + "fixtures" + File.separator;
private static final String SCRIPT_SUFFIX = ".js";
private static final String MODULE_SUFFIX = ".mjs";
private static final String OPTION_REGEX = "@option\\s+([^=\\s]+)(?:\\s*=\\s*(\\S+))?$";
private static final String ARGUMENT_REGEX = "@argument\\s+([^=\\s]+)$";
private static final String LF = System.getProperty("line.separator");
private static final boolean USE_NASHORN_COMPAT_MODE = Boolean.getBoolean("polyglot.js.nashorn-compat");
static class TestCase {
protected final Description testName;
protected final String sourceName;
protected final Path sourceFile;
protected TestCase(Class<?> testClass, String baseName, String sourceName, Path sourceFile) {
this.testName = Description.createTestDescription(testClass, baseName);
this.sourceName = sourceName;
this.sourceFile = sourceFile;
}
}
private final List<TestCase> testCases;
public JSTestRunner(Class<?> runningClass) throws InitializationError {
super(runningClass);
try {
testCases = createTests(runningClass);
} catch (IOException e) {
throw new InitializationError(e);
}
}
@Override
protected Description describeChild(TestCase child) {
return child.testName;
}
@Override
protected List<TestCase> getChildren() {
return testCases;
}
protected static List<TestCase> createTests(final Class<?> c) throws IOException, InitializationError {
JSTestSuite suite = c.getAnnotation(JSTestSuite.class);
if (suite == null) {
throw new InitializationError(String.format("@%s annotation required on class '%s' to run with '%s'.", JSTestSuite.class.getSimpleName(), c.getName(), JSTestRunner.class.getSimpleName()));
}
String[] paths = suite.value();
Path root = null;
boolean pathExists = false;
for (String path : paths) {
root = Paths.get(path);
if (Files.exists(root)) {
pathExists = true;
break;
}
}
if (!pathExists) {
return new ArrayList<>();
}
if (root == null && paths.length > 0) {
throw new FileNotFoundException(paths[0]);
}
final List<TestCase> foundCases = new ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException {
String sourceFilePath = sourceFile.toString();
String sourceName = sourceFile.getFileName().toString();
String suffix = findSuffix(sourceName);
boolean isFixture = sourceFilePath.contains(FIXTURE_DIR);
if (suffix != null && !isFixture) {
String baseName = sourceName.substring(0, sourceName.length() - suffix.length());
foundCases.add(new TestCase(c, baseName, sourceName, sourceFile));
}
return FileVisitResult.CONTINUE;
}
});
return foundCases;
}
private static String findSuffix(String fileName) {
if (fileName.endsWith(SCRIPT_SUFFIX)) {
return SCRIPT_SUFFIX;
} else if (fileName.endsWith(MODULE_SUFFIX)) {
return MODULE_SUFFIX;
} else {
return null;
}
}
public static String readAllLines(Path file) throws IOException {
StringBuilder outFile = new StringBuilder();
for (String line : Files.readAllLines(file, Charset.defaultCharset())) {
outFile.append(line).append(LF);
}
return outFile.toString();
}
@Override
protected void runChild(TestCase testCase, RunNotifier notifier) {
try {
String sourceLines = readAllLines(testCase.sourceFile);
if (hasOption(sourceLines, "ignore-test")) {
notifier.fireTestIgnored(testCase.testName);
return;
}
notifier.fireTestStarted(testCase.testName);
Map<String, String> options = parseOptions(sourceLines);
boolean optionNashornCompat = Boolean.parseBoolean(options.getOrDefault(JSContextOptions.NASHORN_COMPATIBILITY_MODE_NAME, "false"));
boolean optionV8Compat = Boolean.parseBoolean(options.getOrDefault(JSContextOptions.V8_COMPATIBILITY_MODE_NAME, "false"));
if (USE_NASHORN_COMPAT_MODE && (optionV8Compat || !optionNashornCompat)) {
return;
}
options.put(JSContextOptions.DEBUG_BUILTIN_NAME, "true");
options.put(JSContextOptions.SHARED_ARRAY_BUFFER_NAME, "true");
options.put(JSContextOptions.INTL_402_NAME, "true");
String[] args = parseArgs(sourceLines);
Context engineContext = JSTest.newContextBuilder().options(options).allowAllAccess(true).arguments(ID, args).build();
engineContext.enter();
setTestGlobals(engineContext, optionNashornCompat || USE_NASHORN_COMPAT_MODE);
engineContext.leave();
String mimeType = testCase.sourceName.endsWith(MODULE_SUFFIX) ? MODULE_MIME_TYPE : APPLICATION_MIME_TYPE;
Source source = Source.newBuilder(ID, testCase.sourceFile.toFile()).name(testCase.sourceName).content(sourceLines).mimeType(mimeType).build();
Value result = engineContext.eval(source);
Assert.assertTrue(result.asBoolean());
} catch (Throwable ex) {
notifier.fireTestFailure(new Failure(testCase.testName, ex));
} finally {
notifier.fireTestFinished(testCase.testName);
}
}
private static Map<String, String> parseOptions(String sourceLines) {
Map<String, String> options = new LinkedHashMap<>();
Pattern patternOptions = Pattern.compile(OPTION_REGEX, Pattern.MULTILINE);
Matcher matcher = patternOptions.matcher(sourceLines);
String optionName;
String optionValue;
while (matcher.find()) {
optionName = matcher.group(1);
optionValue = matcher.group(2);
if (!optionName.startsWith(JSContextOptions.JS_OPTION_PREFIX)) {
optionName = JSContextOptions.JS_OPTION_PREFIX + optionName;
}
if (optionValue == null) {
optionValue = "true";
}
options.put(optionName, optionValue);
}
return options;
}
private static String[] parseArgs(String sourceLines) {
Pattern patternArguments = Pattern.compile(ARGUMENT_REGEX, Pattern.MULTILINE);
Matcher matcher = patternArguments.matcher(sourceLines);
List<String> args = new LinkedList<>();
while (matcher.find()) {
args.add(matcher.group(1));
}
return args.toArray(new String[0]);
}
private static boolean hasOption(String sourceLines, String optionName) {
return sourceLines.contains("@option " + optionName);
}
private static void setTestGlobals(Context context, boolean inNashornMode) {
Value globalBindings = context.getBindings("js");
globalBindings.putMember("OptionNashornCompat", inNashornMode);
PolyglotBuiltinTest.addTestPolyglotBuiltins(context);
}
public static void runInMain(Class<?> testClass, String[] args) throws InitializationError, NoTestsRemainException {
JUnitCore core = new JUnitCore();
core.addListener(new TextListener(System.out));
JSTestRunner suite = new JSTestRunner(testClass);
if (args.length > 0) {
suite.filter(new NameFilter(args[0]));
}
Result r = core.run(suite);
if (!r.wasSuccessful()) {
System.exit(1);
}
}
private static final class NameFilter extends Filter {
private final String pattern;
private NameFilter(String pattern) {
this.pattern = pattern.toLowerCase();
}
@Override
public boolean shouldRun(Description description) {
return description.getMethodName().toLowerCase().contains(pattern);
}
@Override
public String describe() {
return "Filter contains " + pattern;
}
}
}