| // Copyright 2020 The Bazel Authors. All rights reserved. |
| // |
| // 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 net.starlark.java.eval; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.io.Files; |
| import com.google.errorprone.annotations.FormatMethod; |
| import java.io.File; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| import net.starlark.java.annot.Param; |
| import net.starlark.java.annot.StarlarkBuiltin; |
| import net.starlark.java.annot.StarlarkMethod; |
| import net.starlark.java.lib.json.Json; |
| import net.starlark.java.syntax.FileOptions; |
| import net.starlark.java.syntax.ParserInput; |
| import net.starlark.java.syntax.SyntaxError; |
| |
| /** Script-based tests of Starlark evaluator. */ |
| public final class ScriptTest { |
| |
| // Tests for Starlark. |
| // |
| // In each test file, chunks are separated by "\n---\n". |
| // Each chunk is evaluated separately. |
| // A comment containing |
| // ### regular expression |
| // specifies an expected error on that line. |
| // The part after '###', with leading/trailing spaces removed, |
| // must be a valid regular expression matching the error. |
| // If there is no "###", the test will succeed iff there is no error. |
| // |
| // Within the file, the assert_ and assert_eq functions may be used to |
| // report errors without stopping the program. (They are not evaluation |
| // errors that can be caught with a '###' expectation.) |
| |
| // TODO(adonovan): improve this test driver (following go.starlark.net): |
| // |
| // - extract support for "chunked files" into a library |
| // and reuse it for tests of lexer, parser, resolver. |
| // - require that some frame of each EvalException match the file/line of the expectation. |
| |
| interface Reporter { |
| void reportError(StarlarkThread thread, String message); |
| } |
| |
| @StarlarkMethod( |
| name = "assert_", |
| documented = false, |
| parameters = { |
| @Param(name = "cond"), |
| @Param(name = "msg", defaultValue = "'assertion failed'"), |
| }, |
| useStarlarkThread = true) |
| public Object assertStarlark(Object cond, String msg, StarlarkThread thread) |
| throws EvalException { |
| if (!Starlark.truth(cond)) { |
| reportErrorf(thread, "assert_: %s", msg); |
| } |
| return Starlark.NONE; |
| } |
| |
| @StarlarkMethod( |
| name = "assert_eq", |
| documented = false, |
| parameters = { |
| @Param(name = "x"), |
| @Param(name = "y"), |
| }, |
| useStarlarkThread = true) |
| public Object assertEq(Object x, Object y, StarlarkThread thread) throws EvalException { |
| if (!x.equals(y)) { |
| reportErrorf(thread, "assert_eq: %s != %s", Starlark.repr(x), Starlark.repr(y)); |
| } |
| return Starlark.NONE; |
| } |
| |
| @StarlarkMethod( |
| name = "assert_fails", |
| doc = "assert_fails asserts that evaluation of f() fails with the specified error", |
| parameters = { |
| @Param(name = "f", doc = "the Starlark function to call"), |
| @Param( |
| name = "wantError", |
| doc = "a regular expression matching the expected error message"), |
| }, |
| useStarlarkThread = true) |
| public Object assertFails(StarlarkCallable f, String wantError, StarlarkThread thread) |
| throws EvalException, InterruptedException { |
| Pattern pattern; |
| try { |
| pattern = Pattern.compile(wantError); |
| } catch (PatternSyntaxException unused) { |
| throw Starlark.errorf("invalid regexp: %s", wantError); |
| } |
| |
| try { |
| Starlark.call(thread, f, ImmutableList.of(), ImmutableMap.of()); |
| reportErrorf(thread, "evaluation succeeded unexpectedly (want error matching %s)", wantError); |
| } catch (EvalException ex) { |
| // Verify error matches expectation. |
| String msg = ex.getMessage(); |
| if (!pattern.matcher(msg).find()) { |
| reportErrorf(thread, "regular expression (%s) did not match error (%s)", pattern, msg); |
| } |
| } |
| return Starlark.NONE; |
| } |
| |
| @FormatMethod |
| private static void reportErrorf(StarlarkThread thread, String format, Object... args) { |
| thread.getThreadLocal(Reporter.class).reportError(thread, String.format(format, args)); |
| } |
| |
| // Constructor for simple structs, for testing. |
| @StarlarkMethod(name = "struct", documented = false, extraKeywords = @Param(name = "kwargs")) |
| public Struct struct(Dict<String, Object> kwargs) throws EvalException { |
| return new ImmutableStruct(ImmutableMap.copyOf(kwargs)); |
| } |
| |
| @StarlarkMethod( |
| name = "mutablestruct", |
| documented = false, |
| extraKeywords = @Param(name = "kwargs")) |
| public Struct mutablestruct(Dict<String, Object> kwargs) throws EvalException { |
| return new MutableStruct(kwargs); |
| } |
| |
| @StarlarkMethod( |
| name = "freeze", |
| doc = "Shallow-freezes the operand. With no argument, freezes the thread.", |
| parameters = {@Param(name = "x", defaultValue = "unbound")}, |
| useStarlarkThread = true) |
| public void freeze(Object x, StarlarkThread thread) throws EvalException { |
| if (x == Starlark.UNBOUND) { |
| thread.mutability().close(); |
| return; |
| } |
| |
| if (x instanceof Mutability.Freezable) { |
| ((Mutability.Freezable) x).unsafeShallowFreeze(); |
| } else { |
| throw Starlark.errorf("%s value is not freezable", Starlark.type(x)); |
| } |
| } |
| |
| @StarlarkMethod( |
| name = "int_mul_slow", |
| doc = "Slow but reliable integer multiplication with round-trip to BigInteger", |
| parameters = {@Param(name = "x"), @Param(name = "y")}) |
| public StarlarkInt intMulSlow(StarlarkInt x, StarlarkInt y) { |
| return StarlarkInt.of(x.toBigInteger().multiply(y.toBigInteger())); |
| } |
| |
| private static boolean ok = true; |
| |
| public static void main(String[] args) throws Exception { |
| File root = new File("third_party/bazel"); // blaze |
| if (!root.exists()) { |
| root = new File("."); // bazel |
| } |
| File testdata = new File(root, "src/test/java/net/starlark/java/eval/testdata"); |
| for (String name : testdata.list()) { |
| File file = new File(testdata, name); |
| String content = Files.asCharSource(file, UTF_8).read(); |
| int linenum = 1; |
| for (String chunk : Splitter.on("\n---\n").split(content)) { |
| // prepare chunk |
| StringBuilder buf = new StringBuilder(); |
| for (int i = 1; i < linenum; i++) { |
| buf.append('\n'); |
| } |
| buf.append(chunk); |
| if (false) { |
| System.err.printf("%s:%d: <<%s>>\n", file, linenum, buf); |
| } |
| |
| // extract expectations: ### "regular expression" |
| Map<Pattern, Integer> expectations = new HashMap<>(); |
| for (int i = chunk.indexOf("###"); i >= 0; i = chunk.indexOf("###", i)) { |
| int j = chunk.indexOf("\n", i); |
| if (j < 0) { |
| j = chunk.length(); |
| } |
| |
| int line = linenum + newlines(chunk.substring(0, i)); |
| String comment = chunk.substring(i + 3, j); |
| i = j; |
| |
| // Compile regular expression in comment. |
| Pattern pattern; |
| try { |
| pattern = Pattern.compile(comment.trim()); |
| } catch (PatternSyntaxException ex) { |
| System.err.printf("%s:%d: invalid regexp: %s\n", file, line, ex.getMessage()); |
| ok = false; |
| continue; |
| } |
| |
| if (false) { |
| System.err.printf("%s:%d: expectation '%s'\n", file, line, pattern); |
| } |
| expectations.put(pattern, line); |
| } |
| |
| // parse & execute |
| ParserInput input = ParserInput.fromString(buf.toString(), file.toString()); |
| ImmutableMap.Builder<String, Object> predeclared = ImmutableMap.builder(); |
| Starlark.addMethods(predeclared, new ScriptTest()); // e.g. assert_eq |
| predeclared.put("json", Json.INSTANCE); |
| |
| StarlarkSemantics semantics = StarlarkSemantics.DEFAULT; |
| Module module = Module.withPredeclared(semantics, predeclared.build()); |
| try (Mutability mu = Mutability.createAllowingShallowFreeze("test")) { |
| StarlarkThread thread = new StarlarkThread(mu, semantics); |
| thread.setThreadLocal(Reporter.class, ScriptTest::reportError); |
| Starlark.execFile(input, FileOptions.DEFAULT, module, thread); |
| |
| } catch (SyntaxError.Exception ex) { |
| // parser/resolver errors |
| // |
| // Static errors cannot be suppressed by expectations: |
| // it would be dangerous because the presence of a static |
| // error prevents execution of any dynamic assertions in |
| // a chunk. Tests of static errors belong in syntax/. |
| for (SyntaxError err : ex.errors()) { |
| System.err.println(err); // includes location |
| ok = false; |
| } |
| |
| } catch (EvalException ex) { |
| // evaluation error |
| // |
| // TODO(adonovan): the old logic checks only that each error is matched |
| // by at least one expectation. Instead, ensure that errors |
| // and expections match exactly. Furthermore, look only at errors |
| // whose stack has a frame with a file/line that matches the expectation. |
| // This requires inspecting EvalException stack. |
| // (There can be at most one dynamic error per chunk. |
| // Do we even need to allow multiple expectations?) |
| if (!expected(expectations, ex.getMessage())) { |
| System.err.println(ex.getMessageWithStack()); |
| ok = false; |
| } |
| |
| } catch (Throwable ex) { |
| // unhandled exception (incl. InterruptedException) |
| System.err.printf( |
| "%s:%d: unhandled %s in this chunk: %s\n", |
| file, linenum, ex.getClass().getSimpleName(), ex.getMessage()); |
| ex.printStackTrace(); |
| ok = false; |
| } |
| |
| // unmatched expectations |
| for (Map.Entry<Pattern, Integer> e : expectations.entrySet()) { |
| System.err.printf("%s:%d: unmatched expectation: %s\n", file, e.getValue(), e.getKey()); |
| ok = false; |
| } |
| |
| // advance line number |
| linenum += newlines(chunk) + 2; // for "\n---\n" |
| } |
| } |
| if (!ok) { |
| System.exit(1); |
| } |
| } |
| |
| // Called by assert_ and assert_eq when the test encounters an error. |
| // Does not stop the program; multiple failures may be reported in a single run. |
| private static void reportError(StarlarkThread thread, String message) { |
| System.err.printf("Traceback (most recent call last):\n"); |
| List<StarlarkThread.CallStackEntry> stack = thread.getCallStack(); |
| stack = stack.subList(0, stack.size() - 1); // pop the built-in function |
| for (StarlarkThread.CallStackEntry fr : stack) { |
| System.err.printf("%s: called from %s\n", fr.location, fr.name); |
| } |
| System.err.println("Error: " + message); |
| ok = false; |
| } |
| |
| private static boolean expected(Map<Pattern, Integer> expectations, String message) { |
| for (Pattern pattern : expectations.keySet()) { |
| if (pattern.matcher(message).find()) { |
| expectations.remove(pattern); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static int newlines(String s) { |
| int n = 0; |
| for (int i = 0; i < s.length(); i++) { |
| if (s.charAt(i) == '\n') { |
| n++; |
| } |
| } |
| return n; |
| } |
| |
| // A trivial struct-like class with Starlark fields defined by a map. |
| private static class Struct implements StarlarkValue, Structure { |
| final Map<String, Object> fields; |
| |
| Struct(Map<String, Object> fields) { |
| this.fields = fields; |
| } |
| |
| @Override |
| public ImmutableList<String> getFieldNames() { |
| return ImmutableList.copyOf(fields.keySet()); |
| } |
| |
| @Override |
| public Object getValue(String name) { |
| return fields.get(name); |
| } |
| |
| @Override |
| public String getErrorMessageForUnknownField(String name) { |
| return null; |
| } |
| |
| @Override |
| public void repr(Printer p) { |
| // This repr function prints only the fields. |
| // Any methods are still accessible through dir/getattr/hasattr. |
| p.append(Starlark.type(this)); |
| p.append("("); |
| String sep = ""; |
| for (Map.Entry<String, Object> e : fields.entrySet()) { |
| p.append(sep).append(e.getKey()).append(" = ").repr(e.getValue()); |
| sep = ", "; |
| } |
| p.append(")"); |
| } |
| } |
| |
| @StarlarkBuiltin(name = "struct") |
| private static class ImmutableStruct extends Struct { |
| ImmutableStruct(ImmutableMap<String, Object> fields) { |
| super(fields); |
| } |
| } |
| |
| @StarlarkBuiltin(name = "mutablestruct") |
| private static class MutableStruct extends Struct { |
| MutableStruct(Dict<String, Object> fields) { |
| super(fields); |
| } |
| |
| @Override |
| public void setField(String field, Object value) throws EvalException { |
| if (value.equals("bad")) { |
| throw Starlark.errorf("bad field value"); |
| } |
| ((Dict<String, Object>) fields).putEntry(field, value); |
| } |
| } |
| } |