/** * 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.test262; import static java.util.Objects.requireNonNull; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.LineIterator; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.introspector.PropertyUtils; import com.github.anba.es6draft.util.TestInfo; /** * Parses and returns test case information from test262 js-doc comments. * * {@link http://wiki.ecmascript.org/doku.php?id=test262:test_case_format} * */ final class Test262Info extends TestInfo { private static final Pattern fileNamePattern = Pattern.compile("(.+?)(?:\\.([^.]*)$|$)"); private static final Pattern tags = Pattern.compile("\\s*\\*\\s*@(\\w+)\\s*(.+)?\\s*"); private static final Pattern contentPattern, yamlContentPattern; static { String fileHeader = "(?:\\s*(?://.*)?\\R)*+"; String descriptor = "/\\*\\*?((?s:.*?))\\*/"; String yamlDescriptor = "/\\*---((?s:.*?))---\\*/"; contentPattern = Pattern.compile(fileHeader + descriptor); yamlContentPattern = Pattern.compile(fileHeader + yamlDescriptor); } private String testName, description, errorType; private List<String> includes = Collections.emptyList(); private List<String> features = Collections.emptyList(); private boolean onlyStrict, noStrict, negative, async, module, raw; public Test262Info(Path basedir, Path script) { super(basedir, script); } @Override public String toString() { // return getTestName(); return super.toString(); } /** * Returns the test-name for the test case. */ public String getTestName() { if (testName == null) { String filename = getScript().getFileName().toString(); Matcher matcher = fileNamePattern.matcher(filename); if (!matcher.matches()) { assert false : "regexp failure"; } testName = matcher.group(1); } return testName; } /** * Returns the description for the test case. */ public String getDescription() { if (description == null) { return "<missing description>"; } return description; } /** * Returns the expected error-type if any. */ public String getErrorType() { return errorType; } /** * Returns the list of required includes. */ public List<String> getIncludes() { return includes; } /** * Returns the list of required features. */ public List<String> getFeatures() { return features; } /** * Returns whether the test should only be run in strict-mode. */ public boolean isOnlyStrict() { return onlyStrict; } /** * Returns whether the test should not be run in strict-mode. */ public boolean isNoStrict() { return noStrict; } /** * Returns {@code true} if the test case is expected to fail. */ public boolean isNegative() { return negative; } /** * Returns {@code true} for asynchronous test cases. */ public boolean isAsync() { return async; } /** * Returns {@code true} for module test cases. */ @Override public boolean isModule() { return module; } /** * Returns {@code true} if the test case should be run without preamble code. */ public boolean isRaw() { return raw; } /** * Returns {@code true} if the test configuration supports the requested strict (or sloppy) mode. * * @param strictTest * {@code true} if strict-mode test * @param unmarkedDefault * the default test mode * @return {@code true} if the test should be executed */ public boolean hasMode(boolean strictTest, DefaultMode unmarkedDefault) { if (module) { // Module tests don't need to run with explicit Use Strict directive. return !strictTest; } if (strictTest) { return !isNoStrict() && (isOnlyStrict() || unmarkedDefault != DefaultMode.NonStrict); } else { return !isOnlyStrict() && (isNoStrict() || unmarkedDefault != DefaultMode.Strict); } } /** * Returns {@code true} if the test configuration has the requested features. * * @param includeFeatures * the set of include features, ignored if empty * @param excludeFeatures * the set of exclude features, ignored if empty * @return {@code true} if the requested features are present */ public boolean hasFeature(Set<String> includeFeatures, Set<String> excludeFeatures) { if (!includeFeatures.isEmpty() && Collections.disjoint(includeFeatures, features)) { return false; } if (!excludeFeatures.isEmpty() && !Collections.disjoint(excludeFeatures, features)) { return false; } return true; } @SuppressWarnings("serial") static final class MalformedDataException extends Exception { MalformedDataException(String message) { super(message); } MalformedDataException(String message, Throwable cause) { super(message, cause); } } /** * Parses the test file information for this test case. * * @return the file content * @throws IOException * if there was any I/O error */ public String readFile() throws IOException { String fileContent = readFileContent(); try { readFileInformation(fileContent, true); } catch (MalformedDataException e) { throw new RuntimeException(e); } return fileContent; } /** * Reads the file content. * * @return the file content * @throws IOException * if there was any I/O error */ public String readFileContent() throws IOException { return new String(Files.readAllBytes(toFile()), StandardCharsets.UTF_8); } /** * Parses the test file information for this test case. * * @param content * the file content */ public void readFileInformation(String content) throws MalformedDataException { readFileInformation(content, false); } /** * Parses the test file information for this test case. * * @param content * the file content * @param lenient * if {@code true} ignore unknown test configurations * @throws MalformedDataException * if the test file information cannot be parsed */ private void readFileInformation(String content, boolean lenient) throws MalformedDataException { Matcher m; if ((m = yamlContentPattern.matcher(content)).lookingAt()) { readYaml(m.group(1), lenient); } else if ((m = contentPattern.matcher(content)).lookingAt()) { readTagged(m.group(1), lenient); } else { throw new MalformedDataException("Invalid test file: " + this); } this.async = content.contains("$DONE"); } private static final ConcurrentLinkedQueue<Yaml> yamlQueue = new ConcurrentLinkedQueue<>(); private void readYaml(String descriptor, boolean lenient) throws MalformedDataException { assert descriptor != null && !descriptor.isEmpty(); Yaml yaml = null; if (lenient) { yaml = yamlQueue.poll(); } if (yaml == null) { Constructor constructor = new Constructor(TestDescriptor.class); if (lenient) { PropertyUtils utils = new PropertyUtils(); utils.setSkipMissingProperties(true); constructor.setPropertyUtils(utils); } yaml = new Yaml(constructor); } TestDescriptor desc; try { desc = yaml.loadAs(descriptor, TestDescriptor.class); } catch (YAMLException e) { throw new MalformedDataException(e.getMessage(), e); } if (lenient) { yamlQueue.offer(yaml); } this.description = desc.getDescription(); this.includes = desc.getIncludes(); this.features = desc.getFeatures(); this.errorType = desc.getNegative(); this.negative = desc.getNegative() != null; if (!desc.getFlags().isEmpty()) { if (!lenient) { for (String flag : desc.getFlags()) { if (!allowedFlags.contains(flag)) { throw new MalformedDataException(String.format("Unknown flag '%s'", flag)); } } } this.negative |= desc.getFlags().contains("negative"); this.noStrict = desc.getFlags().contains("noStrict"); this.onlyStrict = desc.getFlags().contains("onlyStrict"); this.module = desc.getFlags().contains("module"); this.raw = desc.getFlags().contains("raw"); } } private static final HashSet<String> allowedFlags = new HashSet<>(Arrays.asList("negative", "onlyStrict", "noStrict", "module", "raw")); public static final class TestDescriptor { private String description; private String info; private List<String> includes = Collections.emptyList(); private List<String> flags = Collections.emptyList(); private List<String> features = Collections.emptyList(); private String negative; private String es5id; private String es6id; private String es7id; private String esid; private String bestPractice; private String author; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getInfo() { return info; } public void setInfo(String info) { this.info = info; } public List<String> getIncludes() { return includes; } public void setIncludes(List<String> includes) { this.includes = includes; } public List<String> getFlags() { return flags; } public void setFlags(List<String> flags) { this.flags = flags; } public List<String> getFeatures() { return features; } public void setFeatures(List<String> features) { this.features = features; } public String getNegative() { return negative; } public void setNegative(String negative) { this.negative = negative; } public String getEs5id() { return es5id; } public void setEs5id(String es5id) { this.es5id = es5id; } public String getEs6id() { return es6id; } public void setEs6id(String es6id) { this.es6id = es6id; } public String getEs7id() { return es7id; } public void setEs7id(String es7id) { this.es7id = es7id; } public String getEsid() { return esid; } public void setEsid(String esid) { this.esid = esid; } public String getBestPractice() { return bestPractice; } public void setBestPractice(String bestPractice) { this.bestPractice = bestPractice; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } } private void readTagged(String descriptor, boolean lenient) throws MalformedDataException { assert descriptor != null && !descriptor.isEmpty(); for (LineIterator lines = new LineIterator(new StringReader(descriptor)); lines.hasNext();) { String line = lines.next(); Matcher m = tags.matcher(line); if (m.matches()) { String type = m.group(1); String val = m.group(2); switch (type) { case "description": this.description = requireNonNull(val, "description must not be null"); break; case "noStrict": requireNull(val); this.noStrict = true; break; case "onlyStrict": requireNull(val); this.onlyStrict = true; break; case "negative": this.negative = true; this.errorType = Objects.toString(val, this.errorType); break; case "hostObject": case "reviewers": case "generator": case "verbatim": case "noHelpers": case "bestPractice": case "implDependent": case "author": // ignore for now break; // legacy case "strict_mode_negative": this.negative = true; this.onlyStrict = true; this.errorType = Objects.toString(val, this.errorType); break; case "strict_only": requireNull(val); this.onlyStrict = true; break; case "errortype": this.errorType = requireNonNull(val, "error-type must not be null"); break; case "assertion": case "section": case "path": case "comment": case "name": // ignore for now break; default: // error if (lenient) { break; } throw new MalformedDataException(String.format("unhandled type '%s' (%s)\n", type, this)); } } } } /** * Counterpart to {@link Objects#requireNonNull(Object, String)}. */ private static final <T> T requireNull(T t) { if (t != null) throw new IllegalStateException("object is not null"); return t; } }