/** * Copyright (c) 2012-2016 André Bargull * Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms. * * <https://github.com/anba/es6draft> */ package com.github.anba.es6draft.util; import static java.util.Collections.emptyList; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.commons.configuration.Configuration; import org.junit.rules.ExternalResource; import com.github.anba.es6draft.Script; import com.github.anba.es6draft.compiler.Compiler; import com.github.anba.es6draft.parser.Parser; import com.github.anba.es6draft.runtime.ExecutionContext; import com.github.anba.es6draft.runtime.Realm; import com.github.anba.es6draft.runtime.World; import com.github.anba.es6draft.runtime.internal.CompatibilityOption; import com.github.anba.es6draft.runtime.internal.Console; import com.github.anba.es6draft.runtime.internal.ObjectAllocator; import com.github.anba.es6draft.runtime.internal.RuntimeContext; import com.github.anba.es6draft.runtime.internal.ScriptCache; import com.github.anba.es6draft.runtime.internal.ScriptLoader; import com.github.anba.es6draft.runtime.internal.Source; import com.github.anba.es6draft.runtime.modules.MalformedNameException; import com.github.anba.es6draft.runtime.modules.ModuleRecord; import com.github.anba.es6draft.runtime.modules.ResolutionException; import com.github.anba.es6draft.runtime.modules.SourceIdentifier; import com.github.anba.es6draft.runtime.objects.GlobalObject; /** * {@link ExternalResource} sub-class to facilitate creation of {@link GlobalObject} instances */ public class TestGlobals<GLOBAL extends GlobalObject, TEST extends TestInfo> extends ExternalResource { private static final String DEFAULT_MODE = "web-compatibility"; private static final String DEFAULT_VERSION = CompatibilityOption.Version.ECMAScript2016.name(); private static final String DEFAULT_STAGE = CompatibilityOption.Stage.Finished.name(); private static final List<String> DEFAULT_FEATURES = emptyList(); private final Configuration configuration; private final ObjectAllocator<GLOBAL> allocator; private final BiFunction<RuntimeContext, ScriptLoader, TestModuleLoader<?>> moduleLoader; private EnumSet<CompatibilityOption> options; private ScriptCache scriptCache; private List<Script> scripts; private PreloadModules modules; public TestGlobals(Configuration configuration, ObjectAllocator<GLOBAL> allocator) { this(configuration, allocator, TestFileModuleLoader::new); } public TestGlobals(Configuration configuration, ObjectAllocator<GLOBAL> allocator, BiFunction<RuntimeContext, ScriptLoader, TestModuleLoader<?>> moduleLoader) { this.configuration = configuration; this.allocator = allocator; this.moduleLoader = moduleLoader; } protected ExecutorService getExecutor() { return null; } protected EnumSet<CompatibilityOption> getOptions() { return EnumSet.copyOf(options); } protected EnumSet<Parser.Option> getParserOptions() { return EnumSet.noneOf(Parser.Option.class); } protected EnumSet<Compiler.Option> getCompilerOptions() { return EnumSet.noneOf(Compiler.Option.class); } protected Path getBaseDirectory() { return Resources.getTestSuitePath(configuration); } protected RuntimeContext createContext() { /* @formatter:off */ return new RuntimeContext.Builder() .setBaseDirectory(getBaseDirectory()) .setExecutor(getExecutor()) .setOptions(getOptions()) .setParserOptions(getParserOptions()) .setCompilerOptions(getCompilerOptions()) .setScriptCache(scriptCache) .setWorkerErrorReporter(this::workerErrorReporter) .build(); /* @formatter:on */ } protected RuntimeContext createContext(Console console, TEST test) { /* @formatter:off */ return new RuntimeContext.Builder() .setLocale(getLocale(test)) .setTimeZone(getTimeZone(test)) .setGlobalAllocator(allocator) .setModuleLoader(moduleLoader) .setBaseDirectory(test.getBaseDir()) .setExecutor(getExecutor()) .setConsole(console) .setOptions(getOptions()) .setParserOptions(getParserOptions()) .setCompilerOptions(getCompilerOptions()) .setScriptCache(scriptCache) .setWorkerErrorReporter(this::workerErrorReporter) .build(); /* @formatter:on */ } protected Locale getLocale(TEST test) { return Locale.getDefault(); } protected TimeZone getTimeZone(TEST test) { return TimeZone.getDefault(); } protected void workerErrorReporter(ExecutionContext cx, Throwable t) { t.printStackTrace(); } protected static ThreadPoolExecutor createDefaultSharedExecutor() { int coreSize = 4; int maxSize = 12; long timeout = 60L; int queueCapacity = 10; return new ThreadPoolExecutor(coreSize, maxSize, timeout, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy()); } @Override protected void before() throws Throwable { if (!Resources.isEnabled(configuration)) { // skip initialization if test suite not enabled return; } scriptCache = new ScriptCache(); // read options ... EnumSet<CompatibilityOption> compatibilityOptions = EnumSet.noneOf(CompatibilityOption.class); optionsFromMode(compatibilityOptions, configuration.getString("mode", DEFAULT_MODE)); optionsFromVersion(compatibilityOptions, configuration.getString("version", DEFAULT_VERSION)); optionsFromStage(compatibilityOptions, configuration.getString("stage", DEFAULT_STAGE)); optionsFromFeatures(compatibilityOptions, configuration.getList("features", DEFAULT_FEATURES)); options = compatibilityOptions; // pre-compile initialization scripts and modules scripts = compileScripts(); modules = compileModules(); } public final GLOBAL newGlobal(Console console, TEST test) throws MalformedNameException, ResolutionException, IOException, URISyntaxException { RuntimeContext context = createContext(console, test); World world = new World(context); Realm realm = world.newInitializedRealm(); // Evaluate additional initialization scripts and modules TestModuleLoader<?> moduleLoader = (TestModuleLoader<?>) world.getModuleLoader(); for (ModuleRecord module : modules.allModules) { moduleLoader.defineFromTemplate(module, realm); } for (ModuleRecord module : modules.mainModules) { ModuleRecord testModule = moduleLoader.get(module.getSourceCodeId(), realm); testModule.instantiate(); testModule.evaluate(); } for (Script script : scripts) { script.evaluate(realm); } @SuppressWarnings("unchecked") GLOBAL global = (GLOBAL) realm.getGlobalObject(); return global; } public void release(GLOBAL global) { if (global != null) { global.getRuntimeContext().getExecutor().shutdown(); global.getRuntimeContext().getWorkerExecutor().shutdown(); } } private static void optionsFromMode(EnumSet<CompatibilityOption> options, String mode) { switch (mode) { case "moz-compatibility": options.addAll(CompatibilityOption.MozCompatibility()); break; case "web-compatibility": options.addAll(CompatibilityOption.WebCompatibility()); break; case "strict-compatibility": options.addAll(CompatibilityOption.StrictCompatibility()); break; default: throw new IllegalArgumentException(String.format("Unsupported mode: '%s'", mode)); } } private static void optionsFromVersion(EnumSet<CompatibilityOption> options, String version) { options.addAll(Arrays.stream(CompatibilityOption.Version.values()).filter(v -> { return v.name().equalsIgnoreCase(version); }).findAny().map(CompatibilityOption::Version).orElseThrow(IllegalArgumentException::new)); } private static void optionsFromStage(EnumSet<CompatibilityOption> options, String stage) { options.addAll(Arrays.stream(CompatibilityOption.Stage.values()).filter(s -> { if (stage.length() == 1 && Character.isDigit(stage.charAt(0))) { return s.getLevel() == Character.digit(stage.charAt(0), 10); } else { return s.name().equalsIgnoreCase(stage); } }).findAny().map(CompatibilityOption::Stage).orElseThrow(IllegalArgumentException::new)); } private static void optionsFromFeatures(EnumSet<CompatibilityOption> options, List<Object> features) { streamNonEmpty(features).map(TestGlobals::optionFromFeature).forEach(options::add); } private static CompatibilityOption optionFromFeature(String feature) { return Arrays.stream(CompatibilityOption.values()).filter(o -> { return o.name().equalsIgnoreCase(feature); }).findAny().orElseThrow(IllegalArgumentException::new); } private List<Script> compileScripts() throws IOException { List<?> scriptNames = configuration.getList("scripts", emptyList()); if (scriptNames.isEmpty()) { return Collections.emptyList(); } Path basedir = getBaseDirectory(); RuntimeContext context = createContext(); ScriptLoader scriptLoader = new ScriptLoader(context); ArrayList<Script> scripts = new ArrayList<>(); for (String scriptName : nonEmpty(scriptNames)) { Source source = new Source(Resources.resourcePath(scriptName, basedir), scriptName, 1); Script script = scriptLoader.script(source, Resources.resource(scriptName, basedir)); scripts.add(script); } return scripts; } private PreloadModules compileModules() throws IOException, MalformedNameException { List<?> moduleNames = configuration.getList("modules", emptyList()); if (moduleNames.isEmpty()) { return new PreloadModules(Collections.<ModuleRecord> emptyList(), Collections.<ModuleRecord> emptyList()); } RuntimeContext context = createContext(); ScriptLoader scriptLoader = new ScriptLoader(context); TestModuleLoader<?> moduleLoader = this.moduleLoader.apply(context, scriptLoader); ArrayList<ModuleRecord> modules = new ArrayList<>(); for (String moduleName : nonEmpty(moduleNames)) { SourceIdentifier moduleId = moduleLoader.normalizeName(moduleName, null); modules.add(moduleLoader.load(moduleId)); } return new PreloadModules(modules, moduleLoader.getModules()); } private static final class PreloadModules { private final List<ModuleRecord> mainModules; private final Collection<ModuleRecord> allModules; PreloadModules(List<ModuleRecord> modules, Collection<? extends ModuleRecord> requires) { assert modules.size() <= requires.size(); this.mainModules = Collections.unmodifiableList(modules); this.allModules = Collections.<ModuleRecord> unmodifiableCollection(requires); } } private static Iterable<String> nonEmpty(List<?> c) { return () -> streamNonEmpty(c).iterator(); } private static Stream<String> streamNonEmpty(List<?> c) { return c.stream().filter(Objects::nonNull).map(Object::toString).filter(not(String::isEmpty)); } private static <T> Predicate<T> not(Predicate<T> p) { return p.negate(); } }