bazel tests: delete TestMode and other test junk

The only differences in the test modes, BUILD and SKYLARK, were:
- the name of the Mutability, which is insignificant.
- whether "BUILD dialect checks", such as no def, no f(**kwargs),
  are applied. These are moved from syntax.EvaluationTest to packages.PackageFactoryTest.
- whether validation errors were reported as events or a thrown exception.
  Now it's always an exception.
- whether the validator gets the 'isBuildFile' flag.
  This affects a small (and diminishing) set of things,
  which should be checked directly.

The ModalTestCase hierarchy (base, BUILD only, Skylark only, both modes)
has been flattened into a single class, Scenario.

EvaluationTestCase:
- newStarlarkThreadWithSkylarkOptions is now setSemantics (stateful)
- inline the code formerly in TestMode and simplify newStarlarkThread et al.
- make bad dependencies on build-base explicit. (They are revealed, but not new.)
- hide fields
- simplify exec().

SkylarkEvaluationTest:
- break "extends EvaluationTest" edge:
  There's no need to duplicate the whole suite for both modes;
  nearly every test was independent of mode (and one suspects most
  were added with no knowledge of the intended design).
- move tests of FlagGuardedValue into StarlarkFlagGuardingTest.
  (They were the only tests to use the 'builtins' parameter, now removed.)
PiperOrigin-RevId: 294451364
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
index 137d0f6..507990d 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -1209,4 +1210,56 @@
     assertThat(globs).containsExactly("ab", "a", "**/*");
     assertThat(globsWithDirs).containsExactly("c");
   }
+
+  // Tests of BUILD file dialect checks:
+
+  @Test
+  public void testDefInBuild() throws Exception {
+    checkBuildDialectError(
+        "def func(): pass", //
+        "function definitions are not allowed in BUILD files");
+  }
+
+  @Test
+  public void testForStatementForbiddenInBuild() throws Exception {
+    checkBuildDialectError(
+        "for _ in []: pass", //
+        "for loops are not allowed");
+  }
+
+  @Test
+  public void testIfStatementForbiddenInBuild() throws Exception {
+    checkBuildDialectError(
+        "if False: pass", //
+        "if statements are not allowed");
+  }
+
+  @Test
+  public void testKwargsForbiddenInBuild() throws Exception {
+    checkBuildDialectError(
+        "print(**dict)", //
+        "**kwargs arguments are not allowed in BUILD files");
+    checkBuildDialectError(
+        "len(dict(**{'a': 1}))", //
+        "**kwargs arguments are not allowed in BUILD files");
+  }
+
+  @Test
+  public void testArgsForbiddenInBuild() throws Exception {
+    checkBuildDialectError(
+        "print(*['a'])", //
+        "*args arguments are not allowed in BUILD files");
+  }
+
+  // Asserts that evaluation of the specified BUILD file produces the expected error.
+  // Modifies: scratch, events, packages; be careful when calling more than once per @Test!
+  private void checkBuildDialectError(String content, String expectedError)
+      throws IOException, InterruptedException, NoSuchPackageException {
+    events.clear();
+    events.setFailFast(false);
+    Path file = scratch.overwriteFile("/p/BUILD", content);
+    Package pkg = packages.eval("p", RootedPath.toRootedPath(root, file));
+    assertThat(pkg.containsErrors()).isTrue();
+    events.assertContainsError(expectedError);
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcToolchainConfigureTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcToolchainConfigureTest.java
index b670186..43b0bbe 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcToolchainConfigureTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/SkylarkCcToolchainConfigureTest.java
@@ -50,8 +50,8 @@
         .testExpression("split_escaped('a%:', ':')", StarlarkList.of(mu, "a:"));
   }
 
-  private ModalTestCase newTest(String... skylarkOptions) throws IOException {
-    return new SkylarkTest(skylarkOptions)
+  private Scenario newTest(String... skylarkOptions) throws IOException {
+    return new Scenario(skylarkOptions)
         // A mock implementation of Label to be able to parse lib_cc_configure under default
         // Skylark environment (lib_cc_configure is meant to be used from the repository
         // environment).
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/util/SkylarkTestCase.java b/src/test/java/com/google/devtools/build/lib/skylark/util/SkylarkTestCase.java
index 3f1432e..1bf36cc 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/util/SkylarkTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/util/SkylarkTestCase.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.packages.SymbolGenerator;
 import com.google.devtools.build.lib.rules.platform.PlatformCommon;
 import com.google.devtools.build.lib.syntax.Module;
+import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
@@ -55,9 +56,10 @@
     EvaluationTestCase ev =
         new EvaluationTestCase() {
           @Override
-          public StarlarkThread newStarlarkThread() throws Exception {
+          public StarlarkThread newStarlarkThread() {
+            Mutability mu = Mutability.create("test");
             StarlarkThread thread =
-                StarlarkThread.builder(mutability)
+                StarlarkThread.builder(mu)
                     .setSemantics(getSkylarkSemantics())
                     .setGlobals(
                         globals.withLabel(
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java b/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
index fb26b45..c9e633c 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
@@ -136,23 +136,22 @@
 
   @Test
   public void testBadOrder() throws Exception {
-    new BothModesTest().testIfExactError(
-        "Invalid order: non_existing",
-        "depset(['a'], order='non_existing')");
+    new Scenario()
+        .testIfExactError("Invalid order: non_existing", "depset(['a'], order='non_existing')");
   }
 
   @Test
   public void testBadOrderDirect() throws Exception {
-    new BothModesTest().testIfExactError(
-        "Invalid order: non_existing",
-        "depset(direct = ['a'], order='non_existing')");
+    new Scenario()
+        .testIfExactError(
+            "Invalid order: non_existing", "depset(direct = ['a'], order='non_existing')");
   }
 
   @Test
   public void testBadOrderItems() throws Exception {
-    new BothModesTest().testIfExactError(
-        "Invalid order: non_existing",
-        "depset(items = ['a'], order='non_existing')");
+    new Scenario()
+        .testIfExactError(
+            "Invalid order: non_existing", "depset(items = ['a'], order='non_existing')");
   }
 
   @Test
@@ -195,49 +194,53 @@
 
   @Test
   public void testBadGenericType() throws Exception {
-    new BothModesTest().testIfExactError(
-        "cannot add an item of type 'int' to a depset of 'string'",
-        "depset(['a', 5])");
+    new Scenario()
+        .testIfExactError(
+            "cannot add an item of type 'int' to a depset of 'string'", "depset(['a', 5])");
   }
 
   @Test
   public void testBadGenericTypeDirect() throws Exception {
-    new BothModesTest().testIfExactError(
-        "cannot add an item of type 'int' to a depset of 'string'",
-        "depset(direct = ['a', 5])");
+    new Scenario()
+        .testIfExactError(
+            "cannot add an item of type 'int' to a depset of 'string'",
+            "depset(direct = ['a', 5])");
   }
 
   @Test
   public void testBadGenericTypeItems() throws Exception {
-    new BothModesTest().testIfExactError(
-        "cannot add an item of type 'int' to a depset of 'string'",
-        "depset(items = ['a', 5])");
+    new Scenario()
+        .testIfExactError(
+            "cannot add an item of type 'int' to a depset of 'string'", "depset(items = ['a', 5])");
   }
 
   @Test
   public void testBadGenericTypeTransitive() throws Exception {
-    new BothModesTest().testIfExactError(
-        "cannot add an item of type 'int' to a depset of 'string'",
-        "depset(['a', 'b'], transitive=[depset([1])])");
+    new Scenario()
+        .testIfExactError(
+            "cannot add an item of type 'int' to a depset of 'string'",
+            "depset(['a', 'b'], transitive=[depset([1])])");
   }
 
   @Test
   public void testLegacyAndNewApi() throws Exception {
-    new BothModesTest().testIfExactError(
-        "Do not pass both 'direct' and 'items' argument to depset constructor.",
-        "depset(['a', 'b'], direct = ['c', 'd'])");
+    new Scenario()
+        .testIfExactError(
+            "Do not pass both 'direct' and 'items' argument to depset constructor.",
+            "depset(['a', 'b'], direct = ['c', 'd'])");
   }
 
   @Test
   public void testItemsAndTransitive() throws Exception {
-    new BothModesTest().testIfExactError(
-        "expected type 'sequence' for items but got type 'depset' instead",
-        "depset(items = depset(), transitive = [depset()])");
+    new Scenario()
+        .testIfExactError(
+            "expected type 'sequence' for items but got type 'depset' instead",
+            "depset(items = depset(), transitive = [depset()])");
   }
 
   @Test
   public void testTooManyPositionals() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testIfErrorContains(
             "depset() accepts no more than 2 positional arguments but got 3",
             "depset([], 'default', [])");
@@ -272,10 +275,10 @@
 
   @Test
   public void testIncompatibleUnion() throws Exception {
-    new BothModesTest("--incompatible_depset_union=true")
+    new Scenario("--incompatible_depset_union=true")
         .testIfErrorContains("`+` operator on a depset is forbidden", "depset([]) + ['a']");
 
-    new BothModesTest("--incompatible_depset_union=true")
+    new Scenario("--incompatible_depset_union=true")
         .testIfErrorContains("`|` operator on a depset is forbidden", "depset([]) | ['a']");
   }
 
@@ -288,7 +291,7 @@
 
   @Test
   public void testUnionOrder() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():",
         "  s1 = depset()",
@@ -303,7 +306,7 @@
 
   @Test
   public void testUnionIncompatibleOrder() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     checkEvalError(
         "Order mismatch: topological != postorder",
         "depset(['a', 'b'], order='postorder') + depset(['c', 'd'], order='topological')");
@@ -311,7 +314,7 @@
 
   @Test
   public void testFunctionReturnsDepset() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():", //
         "  t = depset()",
@@ -324,7 +327,7 @@
 
   @Test
   public void testPlusEqualsWithList() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():", //
         "  t = depset()",
@@ -336,7 +339,7 @@
 
   @Test
   public void testPlusEqualsNoSideEffects() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():",
         "  s1 = depset()",
@@ -350,7 +353,7 @@
 
   @Test
   public void testFuncParamNoSideEffects() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func1(t):",
         "  t += ['b']",
@@ -365,7 +368,7 @@
 
   @Test
   public void testTransitiveOrdering() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():",
         "  sa = depset(['a'], order='postorder')",
@@ -379,7 +382,7 @@
 
   @Test
   public void testLeftRightDirectOrdering() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "def func():",
         "  t = depset()",
@@ -394,7 +397,7 @@
 
   @Test
   public void testToString() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "s = depset() + [2, 4, 6] + [3, 4, 5]", //
         "x = str(s)");
@@ -403,7 +406,7 @@
 
   @Test
   public void testToStringWithOrder() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "s = depset(order = 'topological') + [2, 4, 6] + [3, 4, 5]", //
         "x = str(s)");
@@ -416,7 +419,7 @@
 
   @Test
   public void testToList() throws Exception {
-    thread = newStarlarkThreadWithSkylarkOptions("--incompatible_depset_union=false");
+    setSemantics("--incompatible_depset_union=false");
     exec(
         "s = depset() + [2, 4, 6] + [3, 4, 5]", //
         "x = s.to_list()");
@@ -520,8 +523,7 @@
   @Test
   public void testMutableDepsetElementsLegacyBehavior() throws Exception {
     // See b/144992997 and github.com/bazelbuild/bazel/issues/10313.
-    thread =
-        newStarlarkThreadWithSkylarkOptions("--incompatible_always_check_depset_elements=false");
+    setSemantics("--incompatible_always_check_depset_elements=false");
 
     // Test legacy depset(...) and new depset(direct=...) constructors.
 
@@ -554,8 +556,7 @@
   @Test
   public void testMutableDepsetElementsDesiredBehavior() throws Exception {
     // See b/144992997 and github.com/bazelbuild/bazel/issues/10313.
-    thread =
-        newStarlarkThreadWithSkylarkOptions("--incompatible_always_check_depset_elements=true");
+    setSemantics("--incompatible_always_check_depset_elements=true");
 
     // Test legacy depset(...) and new depset(direct=...) constructors.
 
@@ -588,7 +589,7 @@
   @Test
   public void testDepthExceedsLimitDuringIteration() throws Exception {
     NestedSet.setApplicationDepthLimit(2000);
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def create_depset(depth):",
             "  x = depset([0])",
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
index 677f90b..f2ee7d1 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
@@ -74,19 +74,22 @@
 
     assertThat(EvalUtils.isImmutable(makeList(null))).isTrue();
     assertThat(EvalUtils.isImmutable(makeDict(null))).isTrue();
-    assertThat(EvalUtils.isImmutable(makeList(thread.mutability()))).isFalse();
-    assertThat(EvalUtils.isImmutable(makeDict(thread.mutability()))).isFalse();
+
+    Mutability mu = Mutability.create("test");
+    assertThat(EvalUtils.isImmutable(makeList(mu))).isFalse();
+    assertThat(EvalUtils.isImmutable(makeDict(mu))).isFalse();
   }
 
   @Test
   public void testDatatypeMutabilityDeep() throws Exception {
+    Mutability mu = Mutability.create("test");
     assertThat(EvalUtils.isImmutable(Tuple.of(makeList(null)))).isTrue();
-
-    assertThat(EvalUtils.isImmutable(Tuple.of(makeList(thread.mutability())))).isFalse();
+    assertThat(EvalUtils.isImmutable(Tuple.of(makeList(mu)))).isFalse();
   }
 
   @Test
   public void testComparatorWithDifferentTypes() throws Exception {
+    Mutability mu = Mutability.create("test");
     Object[] objects = {
       "1",
       2,
@@ -94,10 +97,10 @@
       Starlark.NONE,
       Tuple.of(1, 2, 3),
       Tuple.of("1", "2", "3"),
-      StarlarkList.of(thread.mutability(), 1, 2, 3),
-      StarlarkList.of(thread.mutability(), "1", "2", "3"),
-      Dict.of(thread.mutability(), "key", 123),
-      Dict.of(thread.mutability(), 123, "value"),
+      StarlarkList.of(mu, 1, 2, 3),
+      StarlarkList.of(mu, "1", "2", "3"),
+      Dict.of(mu, "key", 123),
+      Dict.of(mu, 123, "value"),
       StructProvider.STRUCT.create(ImmutableMap.of("key", (Object) "value"), "no field %s"),
     };
 
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
index e53b709..d4459f7 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -19,9 +19,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.events.EventCollector;
 import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
-import com.google.devtools.build.lib.testutil.TestMode;
 import java.util.Collections;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -29,29 +27,13 @@
 /** Test of evaluation behavior. (Implicitly uses lexer + parser.) */
 // TODO(adonovan): separate tests of parser, resolver, Starlark core evaluator,
 // and BUILD and .bzl features.
-// TODO(adonovan): make final. Requires changing SkylarkEvaluationTest.
 @RunWith(JUnit4.class)
-public class EvaluationTest extends EvaluationTestCase {
-  @Before
-  public final void setBuildMode() throws Exception {
-    super.setMode(TestMode.BUILD);
-  }
-
-  /**
-   * Creates a new instance of {@code ModalTestCase}.
-   *
-   * <p>If a test uses this method, it allows potential subclasses to run the very same test in a
-   * different mode in subclasses
-   */
-  protected ModalTestCase newTest(String... skylarkOptions) {
-    return new BuildTest(skylarkOptions);
-  }
+public final class EvaluationTest extends EvaluationTestCase {
 
   @Test
   public void testExecutionStopsAtFirstError() throws Exception {
     EventCollector printEvents = new EventCollector();
-    StarlarkThread thread =
-        createStarlarkThread(mutability, StarlarkThread.makeDebugPrintHandler(printEvents));
+    StarlarkThread thread = createStarlarkThread(StarlarkThread.makeDebugPrintHandler(printEvents));
     ParserInput input = ParserInput.fromLines("print('hello'); x = 1//0; print('goodbye')");
 
     assertThrows(EvalException.class, () -> EvalUtils.exec(input, thread));
@@ -64,8 +46,7 @@
   @Test
   public void testExecutionNotStartedOnInterrupt() throws Exception {
     EventCollector printEvents = new EventCollector();
-    StarlarkThread thread =
-        createStarlarkThread(mutability, StarlarkThread.makeDebugPrintHandler(printEvents));
+    StarlarkThread thread = createStarlarkThread(StarlarkThread.makeDebugPrintHandler(printEvents));
     ParserInput input = ParserInput.fromLines("print('hello');");
 
     try {
@@ -81,7 +62,7 @@
 
   @Test
   public void testForLoopAbortedOnInterrupt() throws Exception {
-    StarlarkThread thread = createStarlarkThread(mutability, (th, msg) -> {});
+    StarlarkThread thread = createStarlarkThread((th, msg) -> {});
     InterruptFunction interruptFunction = new InterruptFunction();
     thread.getGlobals().put("interrupt", interruptFunction);
 
@@ -104,7 +85,7 @@
 
   @Test
   public void testForComprehensionAbortedOnInterrupt() throws Exception {
-    StarlarkThread thread = createStarlarkThread(mutability, (th, msg) -> {});
+    StarlarkThread thread = createStarlarkThread((th, msg) -> {});
     InterruptFunction interruptFunction = new InterruptFunction();
     thread.getGlobals().put("interrupt", interruptFunction);
 
@@ -122,7 +103,7 @@
 
   @Test
   public void testFunctionCallsNotStartedOnInterrupt() throws Exception {
-    StarlarkThread thread = createStarlarkThread(mutability, (th, msg) -> {});
+    StarlarkThread thread = createStarlarkThread((th, msg) -> {});
     InterruptFunction interruptFunction = new InterruptFunction();
     thread.getGlobals().put("interrupt", interruptFunction);
 
@@ -159,10 +140,10 @@
     }
   }
 
-  private static StarlarkThread createStarlarkThread(
-      Mutability mutability, StarlarkThread.PrintHandler printHandler) {
+  private static StarlarkThread createStarlarkThread(StarlarkThread.PrintHandler printHandler) {
+    Mutability mu = Mutability.create("test");
     StarlarkThread thread =
-        StarlarkThread.builder(mutability)
+        StarlarkThread.builder(mu)
             .useDefaultSemantics()
             // Provide the UNIVERSE for print... this should not be necessary
             .setGlobals(Module.createForBuiltins(Starlark.UNIVERSE))
@@ -173,7 +154,7 @@
 
   @Test
   public void testExprs() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("'%sx' % 'foo' + 'bar1'", "fooxbar1")
         .testExpression("('%sx' % 'foo') + 'bar2'", "fooxbar2")
         .testExpression("'%sx' % ('foo' + 'bar3')", "foobar3x")
@@ -187,17 +168,17 @@
 
   @Test
   public void testListExprs() throws Exception {
-    newTest().testExactOrder("[1, 2, 3]", 1, 2, 3).testExactOrder("(1, 2, 3)", 1, 2, 3);
+    new Scenario().testExactOrder("[1, 2, 3]", 1, 2, 3).testExactOrder("(1, 2, 3)", 1, 2, 3);
   }
 
   @Test
   public void testStringFormatMultipleArgs() throws Exception {
-    newTest().testExpression("'%sY%s' % ('X', 'Z')", "XYZ");
+    new Scenario().testExpression("'%sY%s' % ('X', 'Z')", "XYZ");
   }
 
   @Test
   public void testConditionalExpressions() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("1 if True else 2", 1)
         .testExpression("1 if False else 2", 2)
         .testExpression("1 + 2 if 3 + 4 else 5 + 6", 3);
@@ -205,7 +186,7 @@
 
   @Test
   public void testListComparison() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("[] < [1]", true)
         .testExpression("[1] < [1, 1]", true)
         .testExpression("[1, 1] < [1, 2]", true)
@@ -225,7 +206,7 @@
 
   @Test
   public void testSetComparison() throws Exception {
-    newTest().testIfExactError("Cannot compare depsets", "depset([1, 2]) < depset([3, 4])");
+    new Scenario().testIfExactError("Cannot compare depsets", "depset([1, 2]) < depset([3, 4])");
   }
 
   @Test
@@ -247,7 +228,7 @@
           }
         };
 
-    newTest()
+    new Scenario()
         .update(sum.getName(), sum)
         .testExpression("sum(1, 2, 3, 4, 5, 6)", 21)
         .testExpression("sum", sum)
@@ -256,7 +237,7 @@
 
   @Test
   public void testNotCallInt() throws Exception {
-    newTest()
+    new Scenario()
         .setUp("sum = 123456")
         .testLookup("sum", 123456)
         .testIfExactError("'int' object is not callable", "sum(1, 2, 3, 4, 5, 6)")
@@ -265,7 +246,8 @@
 
   @Test
   public void testComplexFunctionCall() throws Exception {
-    newTest().setUp("functions = [min, max]", "l = [1,2]")
+    new Scenario()
+        .setUp("functions = [min, max]", "l = [1,2]")
         .testEval("(functions[0](l), functions[1](l))", "(1, 2)");
   }
 
@@ -288,7 +270,7 @@
           }
         };
 
-    newTest()
+    new Scenario()
         .update(kwargs.getName(), kwargs)
         .testEval(
             "kwargs(foo=1, bar='bar', wiz=[1,2,3]).items()",
@@ -300,7 +282,7 @@
 
   @Test
   public void testModulo() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("6 % 2", 0)
         .testExpression("6 % 4", 2)
         .testExpression("3 % 6", 3)
@@ -312,7 +294,7 @@
 
   @Test
   public void testMult() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("6 * 7", 42)
         .testExpression("3 * 'ab'", "ababab")
         .testExpression("0 * 'ab'", "")
@@ -323,12 +305,12 @@
 
   @Test
   public void testSlashOperatorIsForbidden() throws Exception {
-    newTest().testIfErrorContains("The `/` operator is not allowed.", "5 / 2");
+    new Scenario().testIfErrorContains("The `/` operator is not allowed.", "5 / 2");
   }
 
   @Test
   public void testFloorDivision() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("6 // 2", 3)
         .testExpression("6 // 4", 1)
         .testExpression("3 // 6", 0)
@@ -342,7 +324,7 @@
 
   @Test
   public void testCheckedArithmetic() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains("integer overflow", "2000000000 + 2000000000")
         .testIfErrorContains("integer overflow", "1234567890 * 987654321")
         .testIfErrorContains("integer overflow", "- 2000000000 - 2000000000")
@@ -354,7 +336,7 @@
 
   @Test
   public void testOperatorPrecedence() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("2 + 3 * 4", 14)
         .testExpression("2 + 3 // 4", 2)
         .testExpression("2 * 3 + 4 // -2", 4);
@@ -362,21 +344,27 @@
 
   @Test
   public void testConcatStrings() throws Exception {
-    newTest().testExpression("'foo' + 'bar'", "foobar");
+    new Scenario().testExpression("'foo' + 'bar'", "foobar");
   }
 
-  @SuppressWarnings("unchecked")
   @Test
   public void testConcatLists() throws Exception {
+    new Scenario()
+        .testExactOrder("[1,2] + [3,4]", 1, 2, 3, 4)
+        .testExactOrder("(1,2)", 1, 2)
+        .testExactOrder("(1,2) + (3,4)", 1, 2, 3, 4);
+
     // TODO(fwe): cannot be handled by current testing suite
     // list
     Object x = eval("[1,2] + [3,4]");
-    assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
-    assertThat(x).isEqualTo(StarlarkList.of(thread.mutability(), 1, 2, 3, 4));
+    assertThat((Iterable<?>) x).containsExactly(1, 2, 3, 4).inOrder();
+    assertThat(x).isInstanceOf(StarlarkList.class);
     assertThat(EvalUtils.isImmutable(x)).isFalse();
 
     // tuple
     x = eval("(1,2) + (3,4)");
+    assertThat((Iterable<?>) x).containsExactly(1, 2, 3, 4).inOrder();
+    assertThat(x).isInstanceOf(Tuple.class);
     assertThat(x).isEqualTo(Tuple.of(1, 2, 3, 4));
     assertThat(EvalUtils.isImmutable(x)).isTrue();
 
@@ -385,28 +373,30 @@
 
   @Test
   public void testListComprehensions() throws Exception {
-    newTest()
+    new Scenario()
         .testExactOrder("['foo/%s.java' % x for x in []]")
-        .testExactOrder("['foo/%s.java' % y for y in ['bar', 'wiz', 'quux']]", "foo/bar.java",
-            "foo/wiz.java", "foo/quux.java")
-        .testExactOrder("['%s/%s.java' % (z, t) for z in ['foo', 'bar'] "
-            + "for t in ['baz', 'wiz', 'quux']]",
+        .testExactOrder(
+            "['foo/%s.java' % y for y in ['bar', 'wiz', 'quux']]",
+            "foo/bar.java", "foo/wiz.java", "foo/quux.java")
+        .testExactOrder(
+            "['%s/%s.java' % (z, t) for z in ['foo', 'bar'] " + "for t in ['baz', 'wiz', 'quux']]",
             "foo/baz.java",
             "foo/wiz.java",
             "foo/quux.java",
             "bar/baz.java",
             "bar/wiz.java",
             "bar/quux.java")
-        .testExactOrder("['%s/%s.java' % (b, b) for a in ['foo', 'bar'] "
-            + "for b in ['baz', 'wiz', 'quux']]",
+        .testExactOrder(
+            "['%s/%s.java' % (b, b) for a in ['foo', 'bar'] " + "for b in ['baz', 'wiz', 'quux']]",
             "baz/baz.java",
             "wiz/wiz.java",
             "quux/quux.java",
             "baz/baz.java",
             "wiz/wiz.java",
             "quux/quux.java")
-        .testExactOrder("['%s/%s.%s' % (c, d, e) for c in ['foo', 'bar'] "
-            + "for d in ['baz', 'wiz', 'quux'] for e in ['java', 'cc']]",
+        .testExactOrder(
+            "['%s/%s.%s' % (c, d, e) for c in ['foo', 'bar'] "
+                + "for d in ['baz', 'wiz', 'quux'] for e in ['java', 'cc']]",
             "foo/baz.java",
             "foo/baz.cc",
             "foo/wiz.java",
@@ -425,10 +415,10 @@
 
   @Test
   public void testNestedListComprehensions() throws Exception {
-    newTest()
+    new Scenario()
         .setUp("li = [[1, 2], [3, 4]]")
         .testExactOrder("[j for i in li for j in i]", 1, 2, 3, 4);
-    newTest()
+    new Scenario()
         .setUp("input = [['abc'], ['def', 'ghi']]\n")
         .testExactOrder(
             "['%s %s' % (b, c) for a in input for b in a for c in b.elems()]",
@@ -437,48 +427,92 @@
 
   @Test
   public void testListComprehensionsMultipleVariables() throws Exception {
-    newTest().testEval("[x + y for x, y in [(1, 2), (3, 4)]]", "[3, 7]")
+    new Scenario()
+        .testEval("[x + y for x, y in [(1, 2), (3, 4)]]", "[3, 7]")
         .testEval("[z + t for (z, t) in [[1, 2], [3, 4]]]", "[3, 7]");
   }
 
   @Test
   public void testListComprehensionsMultipleVariablesFail() throws Exception {
-    newTest()
+    new Scenario()
         .testIfErrorContains(
             "assignment length mismatch: left-hand side has length 3, but right-hand side"
                 + " evaluates to value of length 2",
             "[x + y for x, y, z in [(1, 2), (3, 4)]]")
         .testIfExactError("type 'int' is not iterable", "[x + y for x, y in (1, 2)]");
+
+    new Scenario()
+        .testIfErrorContains(
+            "assignment length mismatch: left-hand side has length 3, but right-hand side "
+                + "evaluates to value of length 2",
+            "def foo (): return [x + y for x, y, z in [(1, 2), (3, 4)]]",
+            "foo()");
+
+    new Scenario()
+        .testIfErrorContains(
+            "type 'int' is not iterable", "def bar (): return [x + y for x, y in (1, 2)]", "bar()");
+
+    new Scenario()
+        .testIfErrorContains(
+            "assignment length mismatch: left-hand side has length 3, but right-hand side "
+                + "evaluates to value of length 2",
+            "[x + y for x, y, z in [(1, 2), (3, 4)]]");
+
+    new Scenario()
+        .testIfErrorContains("type 'int' is not iterable", "[x2 + y2 for x2, y2 in (1, 2)]");
+
+    new Scenario()
+        // returns [2] in Python, it's an error in Skylark
+        .testIfErrorContains("must have at least one item", "[2 for [] in [()]]");
   }
 
   @Test
   public void testListComprehensionsWithFiltering() throws Exception {
-    newTest()
+    new Scenario()
         .setUp("range3 = [0, 1, 2]")
         .testEval("[a for a in (4, None, 2, None, 1) if a != None]", "[4, 2, 1]")
         .testEval("[b+c for b in [0, 1, 2] for c in [0, 1, 2] if b + c > 2]", "[3, 3, 4]")
         .testEval("[d+e for d in range3 if d % 2 == 1 for e in range3]", "[1, 2, 3]")
-        .testEval("[[f,g] for f in [0, 1, 2, 3, 4] if f for g in [5, 6, 7, 8] if f * g % 12 == 0 ]",
+        .testEval(
+            "[[f,g] for f in [0, 1, 2, 3, 4] if f for g in [5, 6, 7, 8] if f * g % 12 == 0 ]",
             "[[2, 6], [3, 8], [4, 6]]")
         .testEval("[h for h in [4, 2, 0, 1] if h]", "[4, 2, 1]");
   }
 
   @Test
   public void testListComprehensionDefinitionOrder() throws Exception {
-    new BuildTest()
-        .testIfErrorContains(
-            "variable 'y' is referenced before assignment", //
-            "[x for x in (1, 2) if y for y in (3, 4)]");
-
-    new SkylarkTest()
+    // This exercises the .bzl file behavior. This is a dynamic error.
+    // (The error message for BUILD files is slightly different (no "local")
+    // because it doesn't record the scope in the syntax tree.)
+    new Scenario()
         .testIfErrorContains(
             "local variable 'y' is referenced before assignment", //
             "[x for x in (1, 2) if y for y in (3, 4)]");
+
+    // This is the corresponding test for BUILD files.
+    EvalException ex =
+        assertThrows(
+            EvalException.class, () -> execBUILD("[x for x in (1, 2) if y for y in (3, 4)]"));
+    assertThat(ex).hasMessageThat().isEqualTo("variable 'y' is referenced before assignment");
+  }
+
+  private static void execBUILD(String... lines)
+      throws SyntaxError, EvalException, InterruptedException {
+    ParserInput input = ParserInput.fromLines(lines);
+    StarlarkFile file = StarlarkFile.parse(input);
+    StarlarkSemantics semantics = StarlarkSemantics.DEFAULT_SEMANTICS;
+    StarlarkThread thread =
+        StarlarkThread.builder(Mutability.create("test")).setSemantics(semantics).build();
+    ValidationEnvironment.validateFile(file, thread.getGlobals(), semantics, /*isBuildFile=*/ true);
+    if (!file.ok()) {
+      throw new SyntaxError(file.errors());
+    }
+    EvalUtils.exec(file, thread);
   }
 
   @Test
   public void testTupleDestructuring() throws Exception {
-    newTest()
+    new Scenario()
         .setUp("a, b = 1, 2")
         .testLookup("a", 1)
         .testLookup("b", 2)
@@ -489,30 +523,32 @@
 
   @Test
   public void testSingleTuple() throws Exception {
-    newTest().setUp("(a,) = [1]").testLookup("a", 1);
+    new Scenario().setUp("(a,) = [1]").testLookup("a", 1);
   }
 
   @Test
   public void testHeterogeneousDict() throws Exception {
-    newTest().setUp("d = {'str': 1, 2: 3}", "a = d['str']", "b = d[2]").testLookup("a", 1)
+    new Scenario()
+        .setUp("d = {'str': 1, 2: 3}", "a = d['str']", "b = d[2]")
+        .testLookup("a", 1)
         .testLookup("b", 3);
   }
 
   @Test
   public void testAccessDictWithATupleKey() throws Exception {
-    newTest().setUp("x = {(1, 2): 3}[1, 2]").testLookup("x", 3);
+    new Scenario().setUp("x = {(1, 2): 3}[1, 2]").testLookup("x", 3);
   }
 
   @Test
   public void testDictWithDuplicatedKey() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "Duplicated key \"str\" when creating dictionary", "{'str': 1, 'x': 2, 'str': 3}");
   }
 
   @Test
   public void testRecursiveTupleDestructuring() throws Exception {
-    newTest()
+    new Scenario()
         .setUp("((a, b), (c, d)) = [(1, 2), (3, 4)]")
         .testLookup("a", 1)
         .testLookup("b", 2)
@@ -523,13 +559,15 @@
   @Test
   public void testListComprehensionAtTopLevel() throws Exception {
     // It is allowed to have a loop variable with the same name as a global variable.
-    newTest().update("x", 42).setUp("y = [x + 1 for x in [1,2,3]]")
+    new Scenario()
+        .update("x", 42)
+        .setUp("y = [x + 1 for x in [1,2,3]]")
         .testExactOrder("y", 2, 3, 4);
   }
 
   @Test
   public void testDictComprehensions() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("{a : a for a in []}", Collections.emptyMap())
         .testExpression("{b : b for b in [1, 2]}", ImmutableMap.of(1, 1, 2, 2))
         .testExpression(
@@ -544,12 +582,12 @@
 
   @Test
   public void testDictComprehensionOnNonIterable() throws Exception {
-    newTest().testIfExactError("type 'int' is not iterable", "{k : k for k in 3}");
+    new Scenario().testIfExactError("type 'int' is not iterable", "{k : k for k in 3}");
   }
 
   @Test
   public void testDictComprehension_ManyClauses() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testExpression(
             "{x : x * y for x in range(1, 10) if x % 2 == 0 for y in range(1, 10) if y == x}",
             ImmutableMap.of(2, 4, 4, 16, 6, 36, 8, 64));
@@ -557,7 +595,7 @@
 
   @Test
   public void testDictComprehensions_MultipleKey() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("{x : x for x in [1, 2, 1]}", ImmutableMap.of(1, 1, 2, 2))
         .testExpression(
             "{y : y for y in ['ab', 'c', 'a' + 'b']}", ImmutableMap.of("ab", "ab", "c", "c"));
@@ -565,8 +603,8 @@
 
   @Test
   public void testListConcatenation() throws Exception {
-    newTest()
-        .testExpression("[1, 2] + [3, 4]", StarlarkList.of(thread.mutability(), 1, 2, 3, 4))
+    new Scenario()
+        .testExpression("[1, 2] + [3, 4]", StarlarkList.of(null, 1, 2, 3, 4))
         .testExpression("(1, 2) + (3, 4)", Tuple.of(1, 2, 3, 4))
         .testIfExactError("unsupported binary operation: list + tuple", "[1, 2] + (3, 4)")
         .testIfExactError("unsupported binary operation: tuple + list", "(1, 2) + [3, 4]");
@@ -574,8 +612,8 @@
 
   @Test
   public void testListMultiply() throws Exception {
-    Mutability mu = thread.mutability();
-    newTest()
+    Mutability mu = Mutability.create("test");
+    new Scenario()
         .testExpression("[1, 2, 3] * 1", StarlarkList.of(mu, 1, 2, 3))
         .testExpression("[1, 2] * 2", StarlarkList.of(mu, 1, 2, 1, 2))
         .testExpression("[1, 2] * 3", StarlarkList.of(mu, 1, 2, 1, 2, 1, 2))
@@ -592,7 +630,7 @@
 
   @Test
   public void testTupleMultiply() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("(1, 2, 3) * 1", Tuple.of(1, 2, 3))
         .testExpression("(1, 2) * 2", Tuple.of(1, 2, 1, 2))
         .testExpression("(1, 2) * 3", Tuple.of(1, 2, 1, 2, 1, 2))
@@ -609,53 +647,52 @@
 
   @Test
   public void testListComprehensionFailsOnNonSequence() throws Exception {
-    newTest().testIfErrorContains("type 'int' is not iterable", "[x + 1 for x in 123]");
+    new Scenario().testIfErrorContains("type 'int' is not iterable", "[x + 1 for x in 123]");
   }
 
   @Test
   public void testListComprehensionOnStringIsForbidden() throws Exception {
-    newTest().testIfErrorContains("type 'string' is not iterable", "[x for x in 'abc']");
+    new Scenario().testIfErrorContains("type 'string' is not iterable", "[x for x in 'abc']");
   }
 
   @Test
   public void testInvalidAssignment() throws Exception {
-    newTest().testIfErrorContains(
-        "cannot assign to 'x + 1'", "x + 1 = 2");
+    new Scenario().testIfErrorContains("cannot assign to 'x + 1'", "x + 1 = 2");
   }
 
   @Test
   public void testListComprehensionOnDictionary() throws Exception {
-    newTest().testExactOrder("['var_' + n for n in {'a':1,'b':2}]", "var_a", "var_b");
+    new Scenario().testExactOrder("['var_' + n for n in {'a':1,'b':2}]", "var_a", "var_b");
   }
 
   @Test
   public void testListComprehensionOnDictionaryCompositeExpression() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("d = {1:'a',2:'b'}", "l = [d[x] for x in d]")
-        .testLookup("l", StarlarkList.of(thread.mutability(), "a", "b"));
+        .testLookup("l", StarlarkList.of(null, "a", "b"));
   }
 
   @Test
   public void testListComprehensionUpdate() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("xs = [1, 2, 3]")
-        .testIfErrorContains("trying to mutate a locked object",
-            "[xs.append(4) for x in xs]");
+        .testIfErrorContains("trying to mutate a locked object", "[xs.append(4) for x in xs]");
   }
 
   @Test
   public void testNestedListComprehensionUpdate() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("xs = [1, 2, 3]")
-        .testIfErrorContains("trying to mutate a locked object",
-            "[xs.append(4) for x in xs for y in xs]");
+        .testIfErrorContains(
+            "trying to mutate a locked object", "[xs.append(4) for x in xs for y in xs]");
   }
 
   @Test
   public void testListComprehensionUpdateInClause() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("xs = [1, 2, 3]")
-        .testIfErrorContains("trying to mutate a locked object",
+        .testIfErrorContains(
+            "trying to mutate a locked object",
             // Use short-circuiting to produce valid output in the event
             // the exception is not raised.
             "[y for x in xs for y in (xs.append(4) or xs)]");
@@ -663,16 +700,15 @@
 
   @Test
   public void testDictComprehensionUpdate() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("xs = {1:1, 2:2, 3:3}")
-        .testIfErrorContains("trying to mutate a locked object",
-            "[xs.popitem() for x in xs]");
+        .testIfErrorContains("trying to mutate a locked object", "[xs.popitem() for x in xs]");
   }
 
   @Test
   public void testListComprehensionScope() throws Exception {
     // Test list comprehension creates a scope, so outer variables kept unchanged
-    new BuildTest()
+    new Scenario()
         .setUp("x = 1", "l = [x * 3 for x in [2]]", "y = x")
         .testEval("y", "1")
         .testEval("l", "[6]");
@@ -680,7 +716,7 @@
 
   @Test
   public void testInOperator() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("'b' in ['a', 'b']", Boolean.TRUE)
         .testExpression("'c' in ['a', 'b']", Boolean.FALSE)
         .testExpression("'b' in ('a', 'b')", Boolean.TRUE)
@@ -694,7 +730,7 @@
 
   @Test
   public void testNotInOperator() throws Exception {
-    newTest()
+    new Scenario()
         .testExpression("'b' not in ['a', 'b']", Boolean.FALSE)
         .testExpression("'c' not in ['a', 'b']", Boolean.TRUE)
         .testExpression("'b' not in ('a', 'b')", Boolean.FALSE)
@@ -708,7 +744,7 @@
 
   @Test
   public void testInFail() throws Exception {
-    newTest()
+    new Scenario()
         .testIfErrorContains(
             "'in <string>' requires string as left operand, not 'int'", "1 in '123'")
         .testIfErrorContains("unsupported binary operation: string in int", "'a' in 1");
@@ -716,7 +752,7 @@
 
   @Test
   public void testInCompositeForPrecedence() throws Exception {
-    newTest().testExpression("not 'a' in ['a'] or 0", 0);
+    new Scenario().testExpression("not 'a' in ['a'] or 0", 0);
   }
 
   private StarlarkValue createObjWithStr() {
@@ -732,8 +768,8 @@
 
   @Test
   public void testPercentOnDummyValue() throws Exception {
-    newTest().update("obj", createObjWithStr()).testExpression("'%s' % obj", "<str marker>");
-    newTest()
+    new Scenario().update("obj", createObjWithStr()).testExpression("'%s' % obj", "<str marker>");
+    new Scenario()
         .update("unknown", new Dummy())
         .testExpression(
             "'%s' % unknown",
@@ -742,10 +778,10 @@
 
   @Test
   public void testPercentOnTupleOfDummyValues() throws Exception {
-    newTest()
+    new Scenario()
         .update("obj", createObjWithStr())
         .testExpression("'%s %s' % (obj, obj)", "<str marker> <str marker>");
-    newTest()
+    new Scenario()
         .update("unknown", new Dummy())
         .testExpression(
             "'%s %s' % (unknown, unknown)",
@@ -755,24 +791,25 @@
 
   @Test
   public void testPercOnObjectInvalidFormat() throws Exception {
-    newTest()
+    new Scenario()
         .update("obj", createObjWithStr())
         .testIfExactError("invalid argument <str marker> for format pattern %d", "'%d' % obj");
   }
 
   @Test
   public void testDictKeys() throws Exception {
-    newTest().testExactOrder("{'a': 1}.keys() + ['b', 'c']", "a", "b", "c");
+    new Scenario().testExactOrder("{'a': 1}.keys() + ['b', 'c']", "a", "b", "c");
   }
 
   @Test
   public void testDictKeysTooManyArgs() throws Exception {
-    newTest().testIfExactError("keys() got unexpected positional argument", "{'a': 1}.keys('abc')");
+    new Scenario()
+        .testIfExactError("keys() got unexpected positional argument", "{'a': 1}.keys('abc')");
   }
 
   @Test
   public void testDictKeysTooManyKeyArgs() throws Exception {
-    newTest()
+    new Scenario()
         .testIfExactError(
             "keys() got unexpected keyword argument 'arg'", "{'a': 1}.keys(arg='abc')");
   }
@@ -780,56 +817,23 @@
   @Test
   public void testDictKeysDuplicateKeyArgs() throws Exception {
     // TODO(adonovan): when the duplication is literal, this should be caught by a static check.
-    newTest()
+    new Scenario()
         .testIfExactError(
             "int() got multiple values for argument 'base'", "int('1', base=10, base=16)");
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError(
             "int() got multiple values for argument 'base'", "int('1', base=10, **dict(base=16))");
   }
 
   @Test
   public void testArgBothPosKey() throws Exception {
-    newTest()
+    new Scenario()
         .testIfErrorContains(
             "int() got multiple values for argument 'base'", "int('2', 3, base=3)");
   }
 
   @Test
   public void testStaticNameResolution() throws Exception {
-    newTest().testIfErrorContains("name 'foo' is not defined", "[foo for x in []]");
-  }
-
-  @Test
-  public void testDefInBuild() throws Exception {
-    new BuildTest()
-        .testIfErrorContains(
-            "function definitions are not allowed in BUILD files", "def func(): pass");
-  }
-
-  @Test
-  public void testForStatementForbiddenInBuild() throws Exception {
-    new BuildTest().testIfErrorContains("for loops are not allowed", "for _ in []: pass");
-  }
-
-  @Test
-  public void testIfStatementForbiddenInBuild() throws Exception {
-    new BuildTest().testIfErrorContains("if statements are not allowed", "if False: pass");
-  }
-
-  @Test
-  public void testKwargsForbiddenInBuild() throws Exception {
-    new BuildTest()
-        .testIfErrorContains("**kwargs arguments are not allowed in BUILD files", "print(**dict)");
-
-    new BuildTest()
-        .testIfErrorContains(
-            "**kwargs arguments are not allowed in BUILD files", "len(dict(**{'a': 1}))");
-  }
-
-  @Test
-  public void testArgsForbiddenInBuild() throws Exception {
-    new BuildTest()
-        .testIfErrorContains("*args arguments are not allowed in BUILD files", "print(*['a'])");
+    new Scenario().testIfErrorContains("name 'foo' is not defined", "[foo for x in []]");
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index 11e049d..e180595 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -37,7 +37,7 @@
 
   @Test
   public void testStackTraceLocation() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "Traceback (most recent call last):"
                 + LINE_SEPARATOR
@@ -64,7 +64,7 @@
 
   @Test
   public void testStackTraceWithIf() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "File \"\", line 5"
                 + LINE_SEPARATOR
@@ -82,7 +82,7 @@
 
   @Test
   public void testStackTraceWithAugmentedAssignment() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "File \"\", line 4"
                 + LINE_SEPARATOR
@@ -103,7 +103,7 @@
   public void testStackTraceSkipBuiltInOnly() throws Exception {
     // The error message should not include the stack trace when there is
     // only one built-in function.
-    new BothModesTest()
+    new Scenario()
         .testIfExactError(
             "in call to index(), parameter 'sub' got value of type 'int', want 'string'",
             "'test'.index(1)");
@@ -113,7 +113,7 @@
   public void testStackTrace() throws Exception {
     // Unlike SkylarintegrationTests#testStackTraceErrorInFunction(), this test
     // has neither a BUILD nor a bzl file.
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError(
             "Traceback (most recent call last):"
                 + LINE_SEPARATOR
@@ -140,7 +140,7 @@
 
   @Test
   public void testBuiltinFunctionErrorMessage() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testIfErrorContains("substring \"z\" not found in \"abc\"", "'abc'.index('z')")
         .testIfErrorContains(
             "in call to startswith(), parameter 'sub' got value of type 'int', want 'string or"
@@ -151,7 +151,7 @@
 
   @Test
   public void testHasAttr() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testExpression("hasattr(depset(), 'to_list')", Boolean.TRUE)
         .testExpression("hasattr('test', 'count')", Boolean.TRUE)
         .testExpression("hasattr(dict(a = 1, b = 2), 'items')", Boolean.TRUE)
@@ -160,7 +160,7 @@
 
   @Test
   public void testGetAttrMissingField() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError(
             "'string' value has no field or method 'not_there'", "getattr('a string', 'not_there')")
         .testExpression("getattr('a string', 'not_there', 'use this')", "use this")
@@ -192,7 +192,7 @@
 
   @Test
   public void testGetAttrMissingField_typoDetection() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("s", new AStruct())
         .testIfExactError(
             "'AStruct' value has no field or method 'feild' (did you mean 'field'?)",
@@ -202,14 +202,14 @@
   @Test
   public void testGetAttrWithMethods() throws Exception {
     String msg = "'string' value has no field or method 'cnt'";
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError(msg, "getattr('a string', 'cnt')")
         .testExpression("getattr('a string', 'cnt', 'default')", "default");
   }
 
   @Test
   public void testDir() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testExpression(
             "str(dir({}))",
             "[\"clear\", \"get\", \"items\", \"keys\","
@@ -218,18 +218,17 @@
 
   @Test
   public void testBoolean() throws Exception {
-    new BothModesTest().testExpression("False", Boolean.FALSE).testExpression("True", Boolean.TRUE);
+    new Scenario().testExpression("False", Boolean.FALSE).testExpression("True", Boolean.TRUE);
   }
 
   @Test
   public void testBooleanUnsupportedOperationFails() throws Exception {
-    new BothModesTest()
-        .testIfErrorContains("unsupported binary operation: bool + bool", "True + True");
+    new Scenario().testIfErrorContains("unsupported binary operation: bool + bool", "True + True");
   }
 
   @Test
   public void testListSort() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testEval("sorted([0,1,2,3])", "[0, 1, 2, 3]")
         .testEval("sorted([])", "[]")
         .testEval("sorted([3, 2, 1, 0])", "[0, 1, 2, 3]")
@@ -245,28 +244,24 @@
 
   @Test
   public void testDictionaryCopy() throws Exception {
-    new BothModesTest()
-        .setUp("x = {1 : 2}", "y = dict(x)")
-        .testEval("x[1] == 2 and y[1] == 2", "True");
+    new Scenario().setUp("x = {1 : 2}", "y = dict(x)").testEval("x[1] == 2 and y[1] == 2", "True");
   }
 
   @Test
   public void testDictionaryCopyKeyCollision() throws Exception {
-    new BothModesTest()
-        .setUp("x = {'test' : 2}", "y = dict(x, test = 3)")
-        .testEval("y['test']", "3");
+    new Scenario().setUp("x = {'test' : 2}", "y = dict(x, test = 3)").testEval("y['test']", "3");
   }
 
   @Test
   public void testDictionaryKeyNotFound() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testIfErrorContains("key \"0\" not found in dictionary", "{}['0']")
         .testIfErrorContains("key 0 not found in dictionary", "{'0': 1, 2: 3, 4: 5}[0]");
   }
 
   @Test
   public void testDictionaryAccess() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testEval("{1: ['foo']}[1]", "['foo']")
         .testExpression("{'4': 8}['4']", 8)
         .testExpression("{'a': 'aa', 'b': 'bb', 'c': 'cc'}['b']", "bb");
@@ -274,14 +269,14 @@
 
   @Test
   public void testDictionaryVariableAccess() throws Exception {
-    new BothModesTest().setUp("d = {'a' : 1}", "a = d['a']").testLookup("a", 1);
+    new Scenario().setUp("d = {'a' : 1}", "a = d['a']").testLookup("a", 1);
   }
 
   @Test
   public void testDictionaryCreation() throws Exception {
     String expected = "{'a': 1, 'b': 2, 'c': 3}";
 
-    new BothModesTest()
+    new Scenario()
         .testEval("dict([('a', 1), ('b', 2), ('c', 3)])", expected)
         .testEval("dict(a = 1, b = 2, c = 3)", expected)
         .testEval("dict([('a', 1)], b = 2, c = 3)", expected);
@@ -289,19 +284,19 @@
 
   @Test
   public void testDictionaryCreationInnerLists() throws Exception {
-    new BothModesTest().testEval("dict([[1, 2], [3, 4]], a = 5)", "{1: 2, 3: 4, 'a': 5}");
+    new Scenario().testEval("dict([[1, 2], [3, 4]], a = 5)", "{1: 2, 3: 4, 'a': 5}");
   }
 
   @Test
   public void testDictionaryCreationEmpty() throws Exception {
-    new BothModesTest().testEval("dict()", "{}").testEval("dict([])", "{}");
+    new Scenario().testEval("dict()", "{}").testEval("dict([])", "{}");
   }
 
   @Test
   public void testDictionaryCreationDifferentKeyTypes() throws Exception {
     String expected = "{'a': 1, 2: 3}";
 
-    new BothModesTest()
+    new Scenario()
         .testEval("dict([('a', 1), (2, 3)])", expected)
         .testEval("dict([(2, 3)], a = 1)", expected);
   }
@@ -310,15 +305,15 @@
   public void testDictionaryCreationKeyCollision() throws Exception {
     String expected = "{'a': 1, 'b': 2, 'c': 3}";
 
-    new BothModesTest()
+    new Scenario()
         .testEval("dict([('a', 42), ('b', 2), ('a', 1), ('c', 3)])", expected)
         .testEval("dict([('a', 42)], a = 1, b = 2, c = 3)", expected);
-    new SkylarkTest().testEval("dict([('a', 42)], **{'a': 1, 'b': 2, 'c': 3})", expected);
+    new Scenario().testEval("dict([('a', 42)], **{'a': 1, 'b': 2, 'c': 3})", expected);
   }
 
   @Test
   public void testDictionaryCreationInvalidPositional() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testIfErrorContains("in dict, got string, want iterable", "dict('a')")
         .testIfErrorContains(
             "in dict, dictionary update sequence element #0 is not iterable (string)",
@@ -336,7 +331,7 @@
 
   @Test
   public void testDictionaryValues() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testEval("{1: 'foo'}.values()", "['foo']")
         .testEval("{}.values()", "[]")
         .testEval("{True: 3, False: 5}.values()", "[3, 5]")
@@ -346,7 +341,7 @@
 
   @Test
   public void testDictionaryKeys() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testEval("{1: 'foo'}.keys()", "[1]")
         .testEval("{}.keys()", "[]")
         .testEval("{True: 3, False: 5}.keys()", "[True, False]")
@@ -356,7 +351,7 @@
 
   @Test
   public void testDictionaryGet() throws Exception {
-    new BuildTest()
+    new Scenario()
         .testExpression("{1: 'foo'}.get(1)", "foo")
         .testExpression("{1: 'foo'}.get(2)", Starlark.NONE)
         .testExpression("{1: 'foo'}.get(2, 'a')", "a")
@@ -366,7 +361,7 @@
 
   @Test
   public void testDictionaryItems() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testEval("{'a': 'foo'}.items()", "[('a', 'foo')]")
         .testEval("{}.items()", "[]")
         .testEval("{1: 3, 2: 5}.items()", "[(1, 3), (2, 5)]")
@@ -375,7 +370,7 @@
 
   @Test
   public void testDictionaryClear() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "d = {1: 'foo', 2: 'bar', 3: 'baz'}",
             "len(d) == 3 or fail('clear 1')",
@@ -385,7 +380,7 @@
 
   @Test
   public void testDictionaryPop() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "KeyError: 1",
             "d = {1: 'foo', 2: 'bar', 3: 'baz'}\n"
@@ -400,7 +395,7 @@
 
   @Test
   public void testDictionaryPopItem() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "popitem(): dictionary is empty",
             "d = {2: 'bar', 3: 'baz', 1: 'foo'}\n"
@@ -414,17 +409,17 @@
 
   @Test
   public void testDictionaryUpdate() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .setUp("foo = {'a': 2}", "foo.update({'b': 4})")
         .testEval("foo", "{'a': 2, 'b': 4}");
-    new BothModesTest()
+    new Scenario()
         .setUp("foo = {'a': 2}", "foo.update({'a': 3, 'b': 4})")
         .testEval("foo", "{'a': 3, 'b': 4}");
   }
 
   @Test
   public void testDictionarySetDefault() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "d = {2: 'bar', 1: 'foo'}",
             "len(d) == 2 or fail('setdefault 0')",
@@ -437,7 +432,7 @@
 
   @Test
   public void testListIndexMethod() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("['a', 'b', 'c'].index('a')", 0)
         .testExpression("['a', 'b', 'c'].index('b')", 1)
         .testExpression("['a', 'b', 'c'].index('c')", 2)
@@ -451,7 +446,7 @@
   @Test
   public void testHash() throws Exception {
     // We specify the same string hashing algorithm as String.hashCode().
-    new SkylarkTest()
+    new Scenario()
         .testExpression("hash('skylark')", "skylark".hashCode())
         .testExpression("hash('google')", "google".hashCode())
         .testIfErrorContains(
@@ -461,7 +456,7 @@
 
   @Test
   public void testRangeType() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .setUp("a = range(3)")
         .testExpression("len(a)", 3)
         .testExpression("str(a)", "range(0, 3)")
@@ -532,7 +527,7 @@
 
   @Test
   public void testEnumerate() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("str(enumerate([]))", "[]")
         .testExpression("str(enumerate([5]))", "[(0, 5)]")
         .testExpression("str(enumerate([5, 3]))", "[(0, 5), (1, 3)]")
@@ -542,12 +537,12 @@
 
   @Test
   public void testEnumerateBadArg() throws Exception {
-    new BothModesTest().testIfErrorContains("type 'string' is not iterable", "enumerate('a')");
+    new Scenario().testIfErrorContains("type 'string' is not iterable", "enumerate('a')");
   }
 
   @Test
   public void testReassignmentOfPrimitivesNotForbiddenByCoreLanguage() throws Exception {
-    new BuildTest()
+    new Scenario()
         .setUp("cc_binary = (['hello.cc'])")
         .testIfErrorContains(
             "'list' object is not callable",
@@ -556,34 +551,34 @@
 
   @Test
   public void testLenOnString() throws Exception {
-    new BothModesTest().testExpression("len('abc')", 3);
+    new Scenario().testExpression("len('abc')", 3);
   }
 
   @Test
   public void testLenOnList() throws Exception {
-    new BothModesTest().testExpression("len([1,2,3])", 3);
+    new Scenario().testExpression("len([1,2,3])", 3);
   }
 
   @Test
   public void testLenOnDict() throws Exception {
-    new BothModesTest().testExpression("len({'a' : 1, 'b' : 2})", 2);
+    new Scenario().testExpression("len({'a' : 1, 'b' : 2})", 2);
   }
 
   @Test
   public void testLenOnBadType() throws Exception {
-    new BothModesTest().testIfErrorContains("int is not iterable", "len(1)");
+    new Scenario().testIfErrorContains("int is not iterable", "len(1)");
   }
 
   @Test
   public void testIndexOnFunction() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testIfErrorContains("type 'function' has no operator [](int)", "len[1]")
         .testIfErrorContains("invalid slice operand: function", "len[1:4]");
   }
 
   @Test
   public void testBool() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("bool(1)", Boolean.TRUE)
         .testExpression("bool(0)", Boolean.FALSE)
         .testExpression("bool([1, 2])", Boolean.TRUE)
@@ -593,7 +588,7 @@
 
   @Test
   public void testStr() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("str(1)", "1")
         .testExpression("str(-2)", "-2")
         .testExpression("str([1, 2])", "[1, 2]")
@@ -605,12 +600,12 @@
 
   @Test
   public void testStrFunction() throws Exception {
-    new SkylarkTest().setUp("def foo(x): pass").testExpression("str(foo)", "<function foo>");
+    new Scenario().setUp("def foo(x): pass").testExpression("str(foo)", "<function foo>");
   }
 
   @Test
   public void testType() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testExpression("type(1)", "int")
         .testExpression("type('a')", "string")
         .testExpression("type([1, 2])", "list")
@@ -622,7 +617,7 @@
 
   @Test
   public void testZipFunction() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("str(zip())", "[]")
         .testExpression("str(zip([1, 2]))", "[(1,), (2,)]")
         .testExpression("str(zip([1, 2], ['a', 'b']))", "[(1, \"a\"), (2, \"b\")]")
@@ -642,13 +637,13 @@
       String input, Object chars,
       String expLeft, String expRight, String expBoth) throws Exception {
     if (chars == null) {
-      new BothModesTest()
+      new Scenario()
           .update("s", input)
           .testExpression("s.lstrip()", expLeft)
           .testExpression("s.rstrip()", expRight)
           .testExpression("s.strip()", expBoth);
     } else {
-      new BothModesTest()
+      new Scenario()
           .update("s", input)
           .update("chars", chars)
           .testExpression("s.lstrip(chars)", expLeft)
@@ -684,14 +679,12 @@
 
   @Test
   public void testFail() throws Exception {
-    new SkylarkTest()
-        .testIfErrorContains("abc", "fail('abc')")
-        .testIfErrorContains("18", "fail(18)");
+    new Scenario().testIfErrorContains("abc", "fail('abc')").testIfErrorContains("18", "fail(18)");
   }
 
   @Test
   public void testTupleCoercion() throws Exception {
-    new BothModesTest()
+    new Scenario()
         .testExpression("tuple([1, 2]) == (1, 2)", true)
         // Depends on current implementation of dict
         .testExpression("tuple({1: 'foo', 2: 'bar'}) == (1, 2)", true);
@@ -702,7 +695,7 @@
   // keyword, or may be None, even in places where it does not quite make sense.
   @Test
   public void testLegacyNamed() throws Exception {
-    new SkylarkTest("--incompatible_restrict_named_params=false")
+    new Scenario("--incompatible_restrict_named_params=false")
         // Parameters which may be specified by keyword but are not explicitly 'named'.
         .testExpression("all(elements=[True, True])", Boolean.TRUE)
         .testExpression("any(elements=[True, False])", Boolean.TRUE)
@@ -740,7 +733,7 @@
 
   @Test
   public void testExperimentalStarlarkConfig() throws Exception {
-    new SkylarkTest("--incompatible_restrict_named_params")
+    new Scenario("--incompatible_restrict_named_params")
         .testIfErrorContains(
             "join() got named argument for positional-only parameter 'elements'",
             "','.join(elements=['foo', 'bar'])");
@@ -748,14 +741,14 @@
 
   @Test
   public void testStringJoinRequiresStrings() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "expected string for sequence element 1, got 'int'", "', '.join(['foo', 2])");
   }
 
   @Test
   public void testDepsetItemsKeywordAndPositional() throws Exception {
-    new SkylarkTest("--incompatible_disable_depset_items=false")
+    new Scenario("--incompatible_disable_depset_items=false")
         .testIfErrorContains(
             "parameter 'items' cannot be specified both positionally and by keyword",
             "depset([0, 1], 'default', items=[0,1])");
@@ -763,7 +756,7 @@
 
   @Test
   public void testDepsetDirectInvalidType() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "expected type 'sequence' for direct but got type 'string' instead",
             "depset(direct='hello')");
@@ -771,7 +764,7 @@
 
   @Test
   public void testDisableDepsetItems() throws Exception {
-    new SkylarkTest("--incompatible_disable_depset_items")
+    new Scenario("--incompatible_disable_depset_items")
         .setUp("x = depset([0])", "y = depset(direct = [1])")
         .testEval("depset([2, 3], transitive = [x, y]).to_list()", "[0, 1, 2, 3]")
         .testIfErrorContains(
@@ -787,7 +780,7 @@
   @Test
   public void testDepsetDepthLimit() throws Exception {
     NestedSet.setApplicationDepthLimit(2000);
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def create_depset(depth):",
             "  x = depset([0])",
@@ -806,7 +799,7 @@
   @Test
   public void testDepsetDebugDepth() throws Exception {
     NestedSet.setApplicationDepthLimit(2000);
-    new SkylarkTest("--debug_depset_depth=true")
+    new Scenario("--debug_depset_depth=true")
         .setUp(
             "def create_depset(depth):",
             "  x = depset([0])",
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index 7256e3c..403fbce 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -35,35 +35,19 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkGlobalLibrary;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
-import com.google.devtools.build.lib.syntax.StarlarkSemantics.FlagIdentifier;
-import com.google.devtools.build.lib.testutil.TestMode;
+import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
 import java.util.List;
 import java.util.Map;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests of Starlark evaluation. */
-// This test uses 'extends' to make a copy of EvaluationTest whose
-// mode is overridden to SKYLARK, changing various environmental parameters.
+// There is no clear distinction between this and EvaluationTest.
+// TODO(adonovan): reorganize.
 @SkylarkGlobalLibrary // required for @SkylarkCallable-annotated methods
 @RunWith(JUnit4.class)
-public final class SkylarkEvaluationTest extends EvaluationTest {
-
-  @Before
-  public final void setup() throws Exception {
-    setMode(TestMode.SKYLARK);
-  }
-
-  /**
-   * Creates an instance of {@code SkylarkTest} in order to run the tests from the base class in a
-   * Skylark context
-   */
-  @Override
-  protected ModalTestCase newTest(String... skylarkOptions) {
-    return new SkylarkTest(skylarkOptions);
-  }
+public final class SkylarkEvaluationTest extends EvaluationTestCase {
 
   @Immutable
   static class Bad {
@@ -543,7 +527,7 @@
   // declaration, due to the interface's method declaration being generic.
   @Test
   public void testParameterizedMock() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new ParameterizedMock())
         .setUp("result = mock.method('bar')")
         .testLookup("result", "bar");
@@ -551,22 +535,16 @@
 
   @Test
   public void testSimpleIf() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  a = 0",
-        "  x = 0",
-        "  if x: a = 5",
-        "  return a",
-        "a = foo()").testLookup("a", 0);
+    new Scenario()
+        .setUp("def foo():", "  a = 0", "  x = 0", "  if x: a = 5", "  return a", "a = foo()")
+        .testLookup("a", 0);
   }
 
   @Test
   public void testIfPass() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  a = 1",
-        "  x = True",
-        "  if x: pass",
-        "  return a",
-        "a = foo()").testLookup("a", 1);
+    new Scenario()
+        .setUp("def foo():", "  a = 1", "  x = True", "  if x: pass", "  return a", "a = foo()")
+        .testLookup("a", 1);
   }
 
   @Test
@@ -578,17 +556,20 @@
 
   private void executeNestedIf(int x, int y, int expected) throws Exception {
     String fun = String.format("foo%s%s", x, y);
-    new SkylarkTest().setUp("def " + fun + "():",
-        "  x = " + x,
-        "  y = " + y,
-        "  a = 0",
-        "  b = 0",
-        "  if x:",
-        "    if y:",
-        "      a = 2",
-        "    b = 3",
-        "  return a + b",
-        "x = " + fun + "()").testLookup("x", expected);
+    new Scenario()
+        .setUp(
+            "def " + fun + "():",
+            "  x = " + x,
+            "  y = " + y,
+            "  a = 0",
+            "  b = 0",
+            "  if x:",
+            "    if y:",
+            "      a = 2",
+            "    b = 3",
+            "  return a + b",
+            "x = " + fun + "()")
+        .testLookup("x", expected);
   }
 
   @Test
@@ -598,14 +579,17 @@
   }
 
   private void executeIfElse(String fun, String y, int expected) throws Exception {
-    new SkylarkTest().setUp("def " + fun + "():",
-        "  y = '" + y + "'",
-        "  x = 5",
-        "  if x:",
-        "    if y: a = 2",
-        "    else: a = 3",
-        "  return a",
-        "z = " + fun + "()").testLookup("z", expected);
+    new Scenario()
+        .setUp(
+            "def " + fun + "():",
+            "  y = '" + y + "'",
+            "  x = 5",
+            "  if x:",
+            "    if y: a = 2",
+            "    else: a = 3",
+            "  return a",
+            "z = " + fun + "()")
+        .testLookup("z", expected);
   }
 
   @Test
@@ -624,147 +608,172 @@
   }
 
   private void execIfElifElse(int x, int y, int v) throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  x = " + x + "",
-        "  y = " + y + "",
-        "  if x:",
-        "    return 1",
-        "  elif y:",
-        "    return 2",
-        "  else:",
-        "    return 3",
-        "v = foo()").testLookup("v", v);
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  x = " + x + "",
+            "  y = " + y + "",
+            "  if x:",
+            "    return 1",
+            "  elif y:",
+            "    return 2",
+            "  else:",
+            "    return 3",
+            "v = foo()")
+        .testLookup("v", v);
   }
 
   @Test
   public void testForOnList() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  s = ''",
-        "  for i in ['hello', ' ', 'world']:",
-        "    s = s + i",
-        "  return s",
-        "s = foo()").testLookup("s", "hello world");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  s = ''",
+            "  for i in ['hello', ' ', 'world']:",
+            "    s = s + i",
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "hello world");
   }
 
   @Test
   public void testForAssignmentList() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  d = ['a', 'b', 'c']",
-        "  s = ''",
-        "  for i in d:",
-        "    s = s + i",
-        "    d = ['d', 'e', 'f']", // check that we use the old list
-        "  return s",
-        "s = foo()").testLookup("s", "abc");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  d = ['a', 'b', 'c']",
+            "  s = ''",
+            "  for i in d:",
+            "    s = s + i",
+            "    d = ['d', 'e', 'f']", // check that we use the old list
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "abc");
   }
 
   @Test
   public void testForAssignmentDict() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = {'a' : 1, 'b' : 2, 'c' : 3}",
-        "  s = ''",
-        "  for i in d:",
-        "    s = s + i",
-        "    d = {'d' : 1, 'e' : 2, 'f' : 3}",
-        "  return s",
-        "s = func()").testLookup("s", "abc");
+    new Scenario()
+        .setUp(
+            "def func():",
+            "  d = {'a' : 1, 'b' : 2, 'c' : 3}",
+            "  s = ''",
+            "  for i in d:",
+            "    s = s + i",
+            "    d = {'d' : 1, 'e' : 2, 'f' : 3}",
+            "  return s",
+            "s = func()")
+        .testLookup("s", "abc");
   }
 
   @Test
   public void testForUpdateList() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2, 3]",
-        "  for x in xs:",
-        "    if x == 1:",
-        "      xs.append(10)"
-        ).testIfErrorContains("trying to mutate a locked object", "foo()");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2, 3]",
+            "  for x in xs:",
+            "    if x == 1:",
+            "      xs.append(10)")
+        .testIfErrorContains("trying to mutate a locked object", "foo()");
   }
 
   @Test
   public void testForUpdateDict() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  d = {'a': 1, 'b': 2, 'c': 3}",
-        "  for k in d:",
-        "    d[k] *= 2"
-        ).testIfErrorContains("trying to mutate a locked object", "foo()");
+    new Scenario()
+        .setUp("def foo():", "  d = {'a': 1, 'b': 2, 'c': 3}", "  for k in d:", "    d[k] *= 2")
+        .testIfErrorContains("trying to mutate a locked object", "foo()");
   }
 
   @Test
   public void testForUnlockedAfterBreak() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2]",
-        "  for x in xs:",
-        "    break",
-        "  xs.append(3)",
-        "  return xs"
-        ).testEval("foo()", "[1, 2, 3]");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2]",
+            "  for x in xs:",
+            "    break",
+            "  xs.append(3)",
+            "  return xs")
+        .testEval("foo()", "[1, 2, 3]");
   }
 
   @Test
   public void testForNestedOnSameListStillLocked() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2]",
-        "  ys = []",
-        "  for x1 in xs:",
-        "    for x2 in xs:",
-        "      ys.append(x1 * x2)",
-        "    xs.append(4)",
-        "  return ys"
-        ).testIfErrorContains("trying to mutate a locked object", "foo()");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2]",
+            "  ys = []",
+            "  for x1 in xs:",
+            "    for x2 in xs:",
+            "      ys.append(x1 * x2)",
+            "    xs.append(4)",
+            "  return ys")
+        .testIfErrorContains("trying to mutate a locked object", "foo()");
   }
 
   @Test
   public void testForNestedOnSameListErrorMessage() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2]",
-        "  ys = []",
-        "  for x1 in xs:",
-        "    for x2 in xs:",
-        "      ys.append(x1 * x2)",
-        "      xs.append(4)",
-        "  return ys"
-        // No file name in message, due to how test is set up.
-        ).testIfErrorContains("Object locked at the following location(s): :4:3, :5:5", "foo()");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2]",
+            "  ys = []",
+            "  for x1 in xs:",
+            "    for x2 in xs:",
+            "      ys.append(x1 * x2)",
+            "      xs.append(4)",
+            "  return ys"
+            // No file name in message, due to how test is set up.
+            )
+        .testIfErrorContains("Object locked at the following location(s): :4:3, :5:5", "foo()");
   }
 
   @Test
   public void testForNestedOnSameListUnlockedAtEnd() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2]",
-        "  ys = []",
-        "  for x1 in xs:",
-        "    for x2 in xs:",
-        "      ys.append(x1 * x2)",
-        "  xs.append(4)",
-        "  return ys"
-        ).testEval("foo()", "[1, 2, 2, 4]");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2]",
+            "  ys = []",
+            "  for x1 in xs:",
+            "    for x2 in xs:",
+            "      ys.append(x1 * x2)",
+            "  xs.append(4)",
+            "  return ys")
+        .testEval("foo()", "[1, 2, 2, 4]");
   }
 
   @Test
   public void testForNestedWithListCompGood() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2]",
-        "  ys = []",
-        "  for x in xs:",
-        "    zs = [None for x in xs for y in (ys.append(x) or ys)]",
-        "  return ys"
-        ).testEval("foo()", "[1, 2, 1, 2]");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2]",
+            "  ys = []",
+            "  for x in xs:",
+            "    zs = [None for x in xs for y in (ys.append(x) or ys)]",
+            "  return ys")
+        .testEval("foo()", "[1, 2, 1, 2]");
   }
   @Test
   public void testForNestedWithListCompBad() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  xs = [1, 2, 3]",
-        "  ys = []",
-        "  for x in xs:",
-        "    zs = [None for x in xs for y in (xs.append(x) or ys)]",
-        "  return ys"
-        ).testIfErrorContains("trying to mutate a locked object", "foo()");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  xs = [1, 2, 3]",
+            "  ys = []",
+            "  for x in xs:",
+            "    zs = [None for x in xs for y in (xs.append(x) or ys)]",
+            "  return ys")
+        .testIfErrorContains("trying to mutate a locked object", "foo()");
   }
 
   @Test
   public void testForDeepUpdate() throws Exception {
     // Check that indirectly reachable values can still be manipulated as normal.
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def foo():",
             "  xs = [['a'], ['b'], ['c']]",
@@ -780,7 +789,7 @@
 
   @Test
   public void testForNotIterable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "type 'int' is not iterable",
@@ -791,7 +800,7 @@
 
   @Test
   public void testForStringNotIterable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "type 'string' is not iterable", "def func():", "  for i in 'abc': a = i", "func()\n");
@@ -799,39 +808,46 @@
 
   @Test
   public void testForOnDictionary() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  d = {1: 'a', 2: 'b', 3: 'c'}",
-        "  s = ''",
-        "  for i in d: s = s + d[i]",
-        "  return s",
-        "s = foo()").testLookup("s", "abc");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  d = {1: 'a', 2: 'b', 3: 'c'}",
+            "  s = ''",
+            "  for i in d: s = s + d[i]",
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "abc");
   }
 
   @Test
   public void testBadDictKey() throws Exception {
-    new SkylarkTest().testIfErrorContains(
-        "unhashable type: 'list'",
-        "{ [1, 2]: [3, 4] }");
+    new Scenario().testIfErrorContains("unhashable type: 'list'", "{ [1, 2]: [3, 4] }");
   }
 
   @Test
   public void testForLoopReuseVariable() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  s = ''",
-        "  for i in ['a', 'b']:",
-        "    for i in ['c', 'd']: s = s + i",
-        "  return s",
-        "s = foo()").testLookup("s", "cdcd");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  s = ''",
+            "  for i in ['a', 'b']:",
+            "    for i in ['c', 'd']: s = s + i",
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "cdcd");
   }
 
   @Test
   public void testForLoopMultipleVariables() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  s = ''",
-        "  for [i, j] in [[1, 2], [3, 4]]:",
-        "    s = s + str(i) + str(j) + '.'",
-        "  return s",
-        "s = foo()").testLookup("s", "12.34.");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  s = ''",
+            "  for [i, j] in [[1, 2], [3, 4]]:",
+            "    s = s + str(i) + str(j) + '.'",
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "12.34.");
   }
 
   @Test
@@ -980,7 +996,7 @@
 
   @Test
   public void testNoneAssignment() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp("def foo(x=None):", "  x = 1", "  x = None", "  return 2", "s = foo()")
         .testLookup("s", 2);
   }
@@ -999,7 +1015,7 @@
 
   @Test
   public void testJavaCalls() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.is_empty('a')")
         .testLookup("b", Boolean.FALSE);
@@ -1007,7 +1023,7 @@
 
   @Test
   public void testJavaCallsOnSubClass() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new MockSubClass())
         .setUp("b = mock.is_empty('a')")
         .testLookup("b", Boolean.FALSE);
@@ -1015,7 +1031,7 @@
 
   @Test
   public void testJavaCallsOnInterface() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new MockSubClass())
         .setUp("b = mock.is_empty_interface('a')")
         .testLookup("b", Boolean.FALSE);
@@ -1023,34 +1039,34 @@
 
   @Test
   public void testJavaCallsNotSkylarkCallable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError("'Mock' value has no field or method 'value'", "mock.value()");
   }
 
   @Test
   public void testNoOperatorIndex() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError("type 'Mock' has no operator [](int)", "mock[2]");
   }
 
   @Test
   public void testJavaCallsNoMethod() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError("'Mock' value has no field or method 'bad'", "mock.bad()");
   }
 
   @Test
   public void testJavaCallsNoMethodErrorMsg() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError("'int' value has no field or method 'bad'", "s = 3.bad('a', 'b', 'c')");
   }
 
   @Test
   public void testJavaCallWithKwargs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError(
             "'Mock' value has no field or method 'isEmpty'", "mock.isEmpty(str='abc')");
@@ -1058,7 +1074,7 @@
 
   @Test
   public void testStringListDictValues() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "def func(mock):",
@@ -1066,40 +1082,38 @@
             "    modified_list = v + ['extra_string']",
             "  return modified_list",
             "m = func(mock)")
-        .testLookup("m", StarlarkList.of(thread.mutability(), "b", "c", "extra_string"));
+        .testLookup("m", StarlarkList.of(null, "b", "c", "extra_string"));
   }
 
   @Test
   public void testProxyMethodsObject() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
-        .setUp(
-            "m = mock.proxy_methods_object()",
-            "b = m.with_params(1, True, named=True)")
+        .setUp("m = mock.proxy_methods_object()", "b = m.with_params(1, True, named=True)")
         .testLookup("b", "with_params(1, true, false, true, false, a)");
   }
 
   @Test
   public void testLegacyNamed() throws Exception {
-    new SkylarkTest("--incompatible_restrict_named_params=false")
+    new Scenario("--incompatible_restrict_named_params=false")
         .update("mock", new Mock())
         .setUp("b = mock.legacy_method(True, legacyNamed=True, named=True)")
         .testLookup("b", "legacy_method(true, true, true)");
 
-    new SkylarkTest("--incompatible_restrict_named_params=false")
+    new Scenario("--incompatible_restrict_named_params=false")
         .update("mock", new Mock())
         .setUp("b = mock.legacy_method(True, True, named=True)")
         .testLookup("b", "legacy_method(true, true, true)");
 
     // Verify legacyNamed also works with proxy method objects.
-    new SkylarkTest("--incompatible_restrict_named_params=false")
+    new Scenario("--incompatible_restrict_named_params=false")
         .update("mock", new Mock())
         .setUp(
             "m = mock.proxy_methods_object()",
             "b = m.legacy_method(True, legacyNamed=True, named=True)")
         .testLookup("b", "legacy_method(true, true, true)");
 
-    new SkylarkTest("--incompatible_restrict_named_params=false")
+    new Scenario("--incompatible_restrict_named_params=false")
         .update("mock", new Mock())
         .setUp("m = mock.proxy_methods_object()", "b = m.legacy_method(True, True, named=True)")
         .testLookup("b", "legacy_method(true, true, true)");
@@ -1113,7 +1127,7 @@
   public void testArgSpecifiedBothByNameAndPosition() throws Exception {
     // in with_params, 'posOrNamed' is positional parameter index 2. So by specifying both
     // posOrNamed by name and three positional parameters, there is a conflict.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "with_params() got multiple values for argument 'posOrNamed'",
@@ -1122,19 +1136,19 @@
 
   @Test
   public void testTooManyPositionalArgs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "with_params() accepts no more than 3 positional arguments but got 4",
             "mock.with_params(1, True, True, 'toomany', named=True)");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "with_params() accepts no more than 3 positional arguments but got 5",
             "mock.with_params(1, True, True, 'toomany', 'alsotoomany', named=True)");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "is_empty() accepts no more than 1 positional argument but got 2",
@@ -1143,68 +1157,68 @@
 
   @Test
   public void testJavaCallWithPositionalAndKwargs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True)")
         .testLookup("b", "with_params(1, true, false, true, false, a)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, multi=1)")
         .testLookup("b", "with_params(1, true, false, true, false, a, 1)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, multi='abc')")
         .testLookup("b", "with_params(1, true, false, true, false, a, abc)");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, multi=[1,2,3])")
         .testLookup("b", "with_params(1, true, false, true, false, a, [1, 2, 3])");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
             "with_params() missing 1 required named argument: named", "mock.with_params(1, True)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
             "with_params() missing 1 required named argument: named",
             "mock.with_params(1, True, True)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, True, named=True)")
         .testLookup("b", "with_params(1, true, true, true, false, a)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, posOrNamed=True)")
         .testLookup("b", "with_params(1, true, true, true, false, a)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, posOrNamed=True, optionalNamed=True)")
         .testLookup("b", "with_params(1, true, true, true, true, a)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
             "with_params() got unexpected keyword argument 'posornamed' (did you mean"
                 + " 'posOrNamed'?)",
             "mock.with_params(1, True, named=True, posornamed=True)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
             "with_params() got unexpected keyword argument 'n'",
             "mock.with_params(1, True, named=True, posOrNamed=True, n=2)");
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
             "in call to with_params(), parameter 'nonNoneable' cannot be None",
             "mock.with_params(1, True, True, named=True, optionalNamed=False, nonNoneable=None)");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("")
         .testIfExactError(
@@ -1214,7 +1228,7 @@
 
     // We do not enforce list item parameter type constraints.
     // Test for this behavior.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params(1, True, named=True, multi=['a', 'b'])")
         .testLookup("b", "with_params(1, true, false, true, false, a, [\"a\", \"b\"])");
@@ -1222,38 +1236,35 @@
 
   @Test
   public void testNoJavaCallsWithoutSkylark() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError("'int' value has no field or method 'to_string'", "s = 3.to_string()");
   }
 
   @Test
   public void testStructAccess() throws Exception {
-    new SkylarkTest()
-        .update("mock", new Mock())
-        .setUp("v = mock.struct_field")
-        .testLookup("v", "a");
+    new Scenario().update("mock", new Mock()).setUp("v = mock.struct_field").testLookup("v", "a");
   }
 
   @Test
   public void testStructAccessAsFuncallNonCallable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError("'string' object is not callable", "v = mock.struct_field()");
   }
 
   @Test
   public void testSelfCall() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock('bestmock')")
         .testLookup("v", "I'm a mock named bestmock");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("mockfunction = mock", "v = mockfunction('bestmock')")
         .testLookup("v", "I'm a mock named bestmock");
 
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfErrorContains(
             "in call to MockFn(), parameter 'pos' got value of type 'int', want 'string'",
@@ -1262,7 +1273,7 @@
 
   @Test
   public void testStructAccessAsFuncall() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock.struct_field_callable()")
         .testLookup("v", "foobar");
@@ -1288,7 +1299,7 @@
 
   @Test
   public void testJavaFunctionWithExtraInterpreterParams() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock.with_extra()")
         .testLookup("v", "with_extra(1)");
@@ -1296,7 +1307,7 @@
 
   @Test
   public void testStructFieldWithExtraInterpreterParams() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock.struct_field_with_extra")
         .testLookup("v", "struct_field_with_extra(true)");
@@ -1304,7 +1315,7 @@
 
   @Test
   public void testJavaFunctionWithParamsAndExtraInterpreterParams() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_params_and_extra(1, True, named=True)")
         .testLookup("b", "with_params_and_extra(1, true, false, true, false, a, 1)");
@@ -1312,13 +1323,13 @@
 
   @Test
   public void testJavaFunctionWithExtraArgsAndThread() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_args_and_thread(1, True, 'extraArg1', 'extraArg2', named=True)")
         .testLookup("b", "with_args_and_thread(1, true, true, args(extraArg1, extraArg2))");
 
     // Use an args list.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "myargs = ['extraArg2']",
@@ -1328,13 +1339,13 @@
 
   @Test
   public void testJavaFunctionWithExtraKwargs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_kwargs(True, extraKey1=True, named=True, extraKey2='x')")
         .testLookup("b", "with_kwargs(true, true, kwargs(extraKey1=true, extraKey2=x))");
 
     // Use a kwargs dict.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "mykwargs = {'extraKey2':'x', 'named':True}",
@@ -1345,14 +1356,14 @@
   @Test
   public void testJavaFunctionWithArgsAndKwargs() throws Exception {
     // Foo is used positionally
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_args_and_kwargs('foo', 'bar', 'baz', extraKey1=True, extraKey2='x')")
         .testLookup(
             "b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey1=true, extraKey2=x))");
 
     // Use an args list and a kwargs dict
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "mykwargs = {'extraKey1':True}",
@@ -1362,13 +1373,13 @@
             "b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey2=x, extraKey1=true))");
 
     // Foo is used by name
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_args_and_kwargs(foo='foo', extraKey1=True)")
         .testLookup("b", "with_args_and_kwargs(foo, args(), kwargs(extraKey1=true))");
 
     // Empty args and kwargs.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("b = mock.with_args_and_kwargs('foo')")
         .testLookup("b", "with_args_and_kwargs(foo, args(), kwargs())");
@@ -1377,7 +1388,7 @@
   @Test
   public void testProxyMethodsObjectWithArgsAndKwargs() throws Exception {
     // Foo is used positionally
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "m = mock.proxy_methods_object()",
@@ -1386,7 +1397,7 @@
             "b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey1=true, extraKey2=x))");
 
     // Use an args list and a kwargs dict
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "mykwargs = {'extraKey1':True}",
@@ -1397,7 +1408,7 @@
             "b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey2=x, extraKey1=true))");
 
     // Foo is used by name
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "m = mock.proxy_methods_object()",
@@ -1405,7 +1416,7 @@
         .testLookup("b", "with_args_and_kwargs(foo, args(), kwargs(extraKey1=true))");
 
     // Empty args and kwargs.
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("m = mock.proxy_methods_object()", "b = m.with_args_and_kwargs('foo')")
         .testLookup("b", "with_args_and_kwargs(foo, args(), kwargs())");
@@ -1413,13 +1424,13 @@
 
   @Test
   public void testStructAccessOfMethod() throws Exception {
-    new SkylarkTest().update("mock", new Mock()).testExpression("type(mock.function)", "function");
-    new SkylarkTest().update("mock", new Mock()).testExpression("mock.function()", "a");
+    new Scenario().update("mock", new Mock()).testExpression("type(mock.function)", "function");
+    new Scenario().update("mock", new Mock()).testExpression("mock.function()", "a");
   }
 
   @Test
   public void testStructAccessTypo() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new MockClassObject())
         .testIfExactError(
             "'MockClassObject' value has no field or method 'fild' (did you mean 'field'?)",
@@ -1428,7 +1439,7 @@
 
   @Test
   public void testStructAccessType_nonClassObject() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testIfExactError(
             "'Mock' value has no field or method 'sturct_field' (did you mean 'struct_field'?)",
@@ -1457,7 +1468,7 @@
 
   @Test
   public void testClassObjectAccess() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new MockClassObject())
         .setUp("v = mock.field")
         .testLookup("v", "a");
@@ -1465,7 +1476,7 @@
 
   @Test
   public void testUnionSet() throws Exception {
-    new SkylarkTest("--incompatible_depset_union=false")
+    new Scenario("--incompatible_depset_union=false")
         .testExpression("str(depset([1, 3]) | depset([1, 2]))", "depset([1, 2, 3])")
         .testExpression("str(depset([1, 2]) | [1, 3])", "depset([1, 2, 3])")
         .testIfExactError("unsupported binary operation: int | bool", "2 | False");
@@ -1473,7 +1484,7 @@
 
   @Test
   public void testSetIsNotIterable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains("not iterable", "list(depset(['a', 'b']))")
         .testIfErrorContains("not iterable", "max(depset([1, 2, 3]))")
         .testIfErrorContains(
@@ -1498,7 +1509,7 @@
 
   @Test
   public void testJavaFunctionReturnsNone() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock.nullfunc_working()")
         .testLookup("v", Starlark.NONE);
@@ -1506,7 +1517,7 @@
 
   @Test
   public void testVoidJavaFunctionReturnsNone() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp("v = mock.voidfunc()")
         .testLookup("v", Starlark.NONE);
@@ -1514,17 +1525,15 @@
 
   @Test
   public void testAugmentedAssignment() throws Exception {
-    new SkylarkTest().setUp("def f1(x):",
-        "  x += 1",
-        "  return x",
-        "",
-        "foo = f1(41)").testLookup("foo", 42);
+    new Scenario()
+        .setUp("def f1(x):", "  x += 1", "  return x", "", "foo = f1(41)")
+        .testLookup("foo", 42);
   }
 
   @Test
   public void testAugmentedAssignmentHasNoSideEffects() throws Exception {
     // Check object position.
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "counter = [0]",
             "value = [1, 2]",
@@ -1534,10 +1543,10 @@
             "  return value",
             "",
             "f()[1] += 1") // `f()` should be called only once here
-        .testLookup("counter", StarlarkList.of(thread.mutability(), 1));
+        .testLookup("counter", StarlarkList.of(null, 1));
 
     // Check key position.
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "counter = [0]",
             "value = [1, 2]",
@@ -1547,7 +1556,7 @@
             "  return 1",
             "",
             "value[f()] += 1") // `f()` should be called only once here
-        .testLookup("counter", StarlarkList.of(thread.mutability(), 1));
+        .testLookup("counter", StarlarkList.of(null, 1));
   }
 
   @Test
@@ -1571,7 +1580,7 @@
 
   @Test
   public void testAssignmentEvaluationOrder() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "ordinary = []",
             "augmented = []",
@@ -1587,31 +1596,33 @@
             "",
             "f(ordinary)[0] = g(ordinary)[1]",
             "f(augmented)[0] += g(augmented)[1]")
-        .testLookup(
-            "ordinary", StarlarkList.of(thread.mutability(), "g", "f")) // This order is consistent
-        .testLookup("augmented", StarlarkList.of(thread.mutability(), "f", "g")); // with Python
+        .testLookup("ordinary", StarlarkList.of(null, "g", "f")) // This order is consistent
+        .testLookup("augmented", StarlarkList.of(null, "f", "g")); // with Python
   }
 
   @Test
   public void testDictComprehensions_IterationOrder() throws Exception {
-    new SkylarkTest().setUp("def foo():",
-        "  d = {x : x for x in ['c', 'a', 'b']}",
-        "  s = ''",
-        "  for a in d:",
-        "    s += a",
-        "  return s",
-        "s = foo()").testLookup("s", "cab");
+    new Scenario()
+        .setUp(
+            "def foo():",
+            "  d = {x : x for x in ['c', 'a', 'b']}",
+            "  s = ''",
+            "  for a in d:",
+            "    s += a",
+            "  return s",
+            "s = foo()")
+        .testLookup("s", "cab");
   }
 
   @Test
   public void testDotExpressionOnNonStructObject() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError("'string' value has no field or method 'field'", "x = 'a'.field");
   }
 
   @Test
   public void testPlusEqualsOnListMutating() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def func():",
             "  l1 = [1, 2]",
@@ -1622,7 +1633,7 @@
         .testLookup("lists", "([1, 2, 3, 4], [1, 2, 3, 4])");
 
     // The same but with += after an IndexExpression
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def func():",
             "  l = [1, 2]",
@@ -1635,7 +1646,7 @@
 
   @Test
   public void testPlusEqualsOnTuple() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def func():",
             "  t1 = (1, 2)",
@@ -1648,9 +1659,9 @@
 
   @Test
   public void testPlusOnDictDeprecated() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains("unsupported binary operation: dict + dict", "{1: 2} + {3: 4}");
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "unsupported binary operation: dict + dict",
             "def func():",
@@ -1661,93 +1672,96 @@
 
   @Test
   public void testDictAssignmentAsLValue() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = {'a' : 1}",
-        "  d['b'] = 2",
-        "  return d",
-        "d = func()").testLookup("d", ImmutableMap.of("a", 1, "b", 2));
+    new Scenario()
+        .setUp("def func():", "  d = {'a' : 1}", "  d['b'] = 2", "  return d", "d = func()")
+        .testLookup("d", ImmutableMap.of("a", 1, "b", 2));
   }
 
   @Test
   public void testNestedDictAssignmentAsLValue() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = {'a' : 1}",
-        "  e = {'d': d}",
-        "  e['d']['b'] = 2",
-        "  return e",
-        "e = func()").testLookup("e", ImmutableMap.of("d", ImmutableMap.of("a", 1, "b", 2)));
+    new Scenario()
+        .setUp(
+            "def func():",
+            "  d = {'a' : 1}",
+            "  e = {'d': d}",
+            "  e['d']['b'] = 2",
+            "  return e",
+            "e = func()")
+        .testLookup("e", ImmutableMap.of("d", ImmutableMap.of("a", 1, "b", 2)));
   }
 
   @Test
   public void testListAssignmentAsLValue() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  a = [1, 2]",
-        "  a[1] = 3",
-        "  a[-2] = 4",
-        "  return a",
-        "a = str(func())").testLookup("a", "[4, 3]");
+    new Scenario()
+        .setUp(
+            "def func():",
+            "  a = [1, 2]",
+            "  a[1] = 3",
+            "  a[-2] = 4",
+            "  return a",
+            "a = str(func())")
+        .testLookup("a", "[4, 3]");
   }
 
   @Test
   public void testNestedListAssignmentAsLValue() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = [1, 2]",
-        "  e = [3, d]",
-        "  e[1][1] = 4",
-        "  return e",
-        "e = str(func())").testLookup("e", "[3, [1, 4]]");
+    new Scenario()
+        .setUp(
+            "def func():",
+            "  d = [1, 2]",
+            "  e = [3, d]",
+            "  e[1][1] = 4",
+            "  return e",
+            "e = str(func())")
+        .testLookup("e", "[3, [1, 4]]");
   }
 
   @Test
   public void testDictTupleAssignmentAsLValue() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = {'a' : 1}",
-        "  d['b'], d['c'] = 2, 3",
-        "  return d",
-        "d = func()").testLookup("d", ImmutableMap.of("a", 1, "b", 2, "c", 3));
+    new Scenario()
+        .setUp(
+            "def func():", "  d = {'a' : 1}", "  d['b'], d['c'] = 2, 3", "  return d", "d = func()")
+        .testLookup("d", ImmutableMap.of("a", 1, "b", 2, "c", 3));
   }
 
   @Test
   public void testDictItemPlusEqual() throws Exception {
-    new SkylarkTest().setUp("def func():",
-        "  d = {'a' : 2}",
-        "  d['a'] += 3",
-        "  return d",
-        "d = func()").testLookup("d", ImmutableMap.of("a", 5));
+    new Scenario()
+        .setUp("def func():", "  d = {'a' : 2}", "  d['a'] += 3", "  return d", "d = func()")
+        .testLookup("d", ImmutableMap.of("a", 5));
   }
 
   @Test
   public void testDictAssignmentAsLValueSideEffects() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp("def func(d):", "  d['b'] = 2", "d = {'a' : 1}", "func(d)")
         .testLookup("d", Dict.of((Mutability) null, "a", 1, "b", 2));
   }
 
   @Test
   public void testAssignmentToListInDictSideEffect() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp("l = [1, 2]", "d = {0: l}", "d[0].append(3)")
         .testLookup("l", StarlarkList.of(null, 1, 2, 3));
   }
 
   @Test
   public void testUserFunctionKeywordArgs() throws Exception {
-    new SkylarkTest().setUp("def foo(a, b, c):",
-        "  return a + b + c", "s = foo(1, c=2, b=3)")
+    new Scenario()
+        .setUp("def foo(a, b, c):", "  return a + b + c", "s = foo(1, c=2, b=3)")
         .testLookup("s", 6);
   }
 
   @Test
   public void testFunctionCallOrdering() throws Exception {
-    new SkylarkTest().setUp("def func(): return foo() * 2",
-         "def foo(): return 2",
-         "x = func()")
-         .testLookup("x", 4);
+    new Scenario()
+        .setUp("def func(): return foo() * 2", "def foo(): return 2", "x = func()")
+        .testLookup("x", 4);
   }
 
   @Test
   public void testFunctionCallBadOrdering() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "global variable 'foo' is referenced before assignment.",
             "def func(): return foo() * 2",
@@ -1757,7 +1771,7 @@
 
   @Test
   public void testLocalVariableDefinedBelow() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp(
             "def beforeEven(li):", // returns the value before the first even number
             "    for i in li:",
@@ -1771,7 +1785,7 @@
 
   @Test
   public void testShadowisNotInitialized() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             /* error message */ "local variable 'gl' is referenced before assignment",
             "gl = 5",
@@ -1783,7 +1797,7 @@
 
   @Test
   public void testShadowBuiltin() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "global variable 'len' is referenced before assignment",
             "x = len('abc')",
@@ -1793,7 +1807,7 @@
 
   @Test
   public void testFunctionCallRecursion() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "function 'f' called recursively",
             "def main():",
@@ -1817,7 +1831,7 @@
 
   @Test
   public void testNoneTrueFalseInSkylark() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp("a = None", "b = True", "c = False")
         .testLookup("a", Starlark.NONE)
         .testLookup("b", Boolean.TRUE)
@@ -1826,10 +1840,13 @@
 
   @Test
   public void testHasattrMethods() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
-        .setUp("a = hasattr(mock, 'struct_field')", "b = hasattr(mock, 'function')",
-            "c = hasattr(mock, 'is_empty')", "d = hasattr('str', 'replace')",
+        .setUp(
+            "a = hasattr(mock, 'struct_field')",
+            "b = hasattr(mock, 'function')",
+            "c = hasattr(mock, 'is_empty')",
+            "d = hasattr('str', 'replace')",
             "e = hasattr(mock, 'other')\n")
         .testLookup("a", Boolean.TRUE)
         .testLookup("b", Boolean.TRUE)
@@ -1840,7 +1857,7 @@
 
   @Test
   public void testGetattrMethods() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .setUp(
             "a = str(getattr(mock, 'struct_field', 'no'))",
@@ -1857,44 +1874,45 @@
 
   @Test
   public void testListAnTupleConcatenationDoesNotWorkInSkylark() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfExactError("unsupported binary operation: list + tuple", "[1, 2] + (3, 4)");
   }
 
   @Test
   public void testCannotCreateMixedListInSkylark() throws Exception {
-    new SkylarkTest().testExactOrder("['a', 'b', 1, 2]", "a", "b", 1, 2);
+    new Scenario().testExactOrder("['a', 'b', 1, 2]", "a", "b", 1, 2);
   }
 
   @Test
   public void testCannotConcatListInSkylarkWithDifferentGenericTypes() throws Exception {
-    new SkylarkTest().testExactOrder("[1, 2] + ['a', 'b']", 1, 2, "a", "b");
+    new Scenario().testExactOrder("[1, 2] + ['a', 'b']", 1, 2, "a", "b");
   }
 
   @Test
   public void testConcatEmptyListWithNonEmptyWorks() throws Exception {
-    new SkylarkTest().testExactOrder("[] + ['a', 'b']", "a", "b");
+    new Scenario().testExactOrder("[] + ['a', 'b']", "a", "b");
   }
 
   @Test
   public void testFormatStringWithTuple() throws Exception {
-    new SkylarkTest().setUp("v = '%s%s' % ('a', 1)").testLookup("v", "a1");
+    new Scenario().setUp("v = '%s%s' % ('a', 1)").testLookup("v", "a1");
   }
 
   @Test
   public void testSingletonTuple() throws Exception {
-    new SkylarkTest().testExactOrder("(1,)", 1);
+    new Scenario().testExactOrder("(1,)", 1);
   }
 
   @Test
   public void testDirFindsClassObjectFields() throws Exception {
-    new SkylarkTest().update("mock", new MockClassObject())
+    new Scenario()
+        .update("mock", new MockClassObject())
         .testExactOrder("dir(mock)", "field", "nset");
   }
 
   @Test
   public void testDirFindsJavaObjectStructFieldsAndMethods() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new Mock())
         .testExactOrder(
             "dir(mock)",
@@ -1925,7 +1943,7 @@
 
   @Test
   public void testStrNativeInfo() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new NativeInfoMock())
         .testEval(
             "str(mock)",
@@ -1935,7 +1953,7 @@
 
   @Test
   public void testNativeInfoAttrs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("mock", new NativeInfoMock())
         .testEval(
             "dir(mock)",
@@ -1959,85 +1977,19 @@
 
   @Test
   public void testPrintBadKwargs() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "print() got unexpected keyword argument 'end'", "print(end='x', other='y')");
   }
 
-  // Override tests in EvaluationTest incompatible with Skylark
-
-  @SuppressWarnings("unchecked")
-  @Override
-  @Test
-  public void testConcatLists() throws Exception {
-    new SkylarkTest().testExactOrder("[1,2] + [3,4]", 1, 2, 3, 4).testExactOrder("(1,2)", 1, 2)
-        .testExactOrder("(1,2) + (3,4)", 1, 2, 3, 4);
-
-    // TODO(fwe): cannot be handled by current testing suite
-    // list
-    Object x = eval("[1,2] + [3,4]");
-    assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
-
-    // tuple
-    x = eval("(1,2)");
-    assertThat((Iterable<Object>) x).containsExactly(1, 2).inOrder();
-    assertThat(x).isInstanceOf(Tuple.class);
-
-    x = eval("(1,2) + (3,4)");
-    assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
-    assertThat(x).isInstanceOf(Tuple.class);
-  }
-
-  @Override
-  @Test
-  public void testListConcatenation() throws Exception {}
-
-  @Override
-  @Test
-  public void testListComprehensionsMultipleVariablesFail() throws Exception {
-    new SkylarkTest()
-        .testIfErrorContains(
-            "assignment length mismatch: left-hand side has length 3, but right-hand side "
-                + "evaluates to value of length 2",
-            "def foo (): return [x + y for x, y, z in [(1, 2), (3, 4)]]",
-            "foo()");
-
-    new SkylarkTest()
-        .testIfErrorContains(
-            "type 'int' is not iterable", "def bar (): return [x + y for x, y in (1, 2)]", "bar()");
-
-    new SkylarkTest()
-        .testIfErrorContains(
-            "assignment length mismatch: left-hand side has length 3, but right-hand side "
-                + "evaluates to value of length 2",
-            "[x + y for x, y, z in [(1, 2), (3, 4)]]");
-
-    new SkylarkTest()
-        .testIfErrorContains("type 'int' is not iterable", "[x2 + y2 for x2, y2 in (1, 2)]");
-
-    new SkylarkTest()
-        // returns [2] in Python, it's an error in Skylark
-        .testIfErrorContains("must have at least one item", "[2 for [] in [()]]");
-  }
-
-  @Override
-  @Test
-  public void testNotCallInt() throws Exception {
-    new SkylarkTest()
-        .setUp("sum = 123456")
-        .testLookup("sum", 123456)
-        .testIfExactError("'int' object is not callable", "sum(1, 2, 3, 4, 5, 6)")
-        .testExpression("sum", 123456);
-  }
-
   @Test
   public void testConditionalExpressionAtToplevel() throws Exception {
-    new SkylarkTest().setUp("x = 1 if 2 else 3").testLookup("x", 1);
+    new Scenario().setUp("x = 1 if 2 else 3").testLookup("x", 1);
   }
 
   @Test
   public void testConditionalExpressionInFunction() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .setUp("def foo(a, b, c): return a+b if c else a-b\n")
         .testExpression("foo(23, 5, 0)", 18);
   }
@@ -2118,7 +2070,7 @@
 
   @Test
   public void testStructFieldDefinedOnlyInValues() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .setUp("v = val.values_only_field")
         .testLookup("v", "fromValues");
@@ -2126,7 +2078,7 @@
 
   @Test
   public void testStructMethodDefinedOnlyInValues() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .setUp("v = val.values_only_method()")
         .testLookup("v", "fromValues");
@@ -2134,7 +2086,7 @@
 
   @Test
   public void testStructFieldDefinedOnlyInSkylarkCallable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .setUp("v = val.callable_only_field")
         .testLookup("v", "fromSkylarkCallable");
@@ -2142,7 +2094,7 @@
 
   @Test
   public void testStructMethodDefinedOnlyInSkylarkCallable() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .setUp("v = val.callable_only_method()")
         .testLookup("v", "fromSkylarkCallable");
@@ -2153,7 +2105,7 @@
   public void testStructMethodDefinedInValuesAndSkylarkCallable() throws Exception {
     // This test exercises the resolution of ambiguity between @SkylarkCallable-annotated
     // fields and those reported by ClassObject.getValue.
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .setUp("v = val.collision_method()")
         .testLookup("v", "fromSkylarkCallable");
@@ -2161,7 +2113,7 @@
 
   @Test
   public void testStructFieldNotDefined() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .testIfExactError(
             // TODO(bazel-team): This should probably list callable_only_method as well.
@@ -2173,7 +2125,7 @@
 
   @Test
   public void testStructMethodNotDefined() throws Exception {
-    new SkylarkTest()
+    new Scenario()
         .update("val", new SkylarkClassObjectWithSkylarkCallables())
         .testIfExactError(
             "'struct_with_skylark_callables' value has no field or method 'nonexistent_method'\n"
@@ -2210,7 +2162,7 @@
 
     AnalysisFailureInfo info = AnalysisFailureInfo.forAnalysisFailures(ImmutableList.of(cause));
 
-    new SkylarkTest()
+    new Scenario()
         .update("val", info)
         .setUp(
             "causes = val.causes",
@@ -2221,76 +2173,9 @@
   }
 
   @Test
-  // TODO(adonovan): move to Validation tests.
-  public void testExperimentalFlagGuardedValue() throws Exception {
-    // This test uses an arbitrary experimental flag to verify this functionality. If this
-    // experimental flag were to go away, this test may be updated to use any experimental flag.
-    // The flag itself is unimportant to the test.
-    FlagGuardedValue val =
-        FlagGuardedValue.onlyWhenExperimentalFlagIsTrue(
-            FlagIdentifier.EXPERIMENTAL_BUILD_SETTING_API, "foo");
-    String errorMessage =
-        "GlobalSymbol is experimental and thus unavailable with the current "
-            + "flags. It may be enabled by setting --experimental_build_setting_api";
-
-
-    new SkylarkTest(ImmutableMap.of("GlobalSymbol", val), "--experimental_build_setting_api=true")
-        .setUp("var = GlobalSymbol")
-        .testLookup("var", "foo");
-
-    new SkylarkTest(ImmutableMap.of("GlobalSymbol", val), "--experimental_build_setting_api=false")
-        .testIfErrorContains(errorMessage, "var = GlobalSymbol");
-
-    new SkylarkTest(ImmutableMap.of("GlobalSymbol", val), "--experimental_build_setting_api=false")
-        .testIfErrorContains(errorMessage, "def my_function():", "  var = GlobalSymbol");
-
-    new SkylarkTest(ImmutableMap.of("GlobalSymbol", val), "--experimental_build_setting_api=false")
-        .setUp("GlobalSymbol = 'other'", "var = GlobalSymbol")
-        .testLookup("var", "other");
-  }
-
-  @Test
-  public void testIncompatibleFlagGuardedValue() throws Exception {
-    // This test uses an arbitrary incompatible flag to verify this functionality. If this
-    // incompatible flag were to go away, this test may be updated to use any incompatible flag.
-    // The flag itself is unimportant to the test.
-    FlagGuardedValue val = FlagGuardedValue.onlyWhenIncompatibleFlagIsFalse(
-        FlagIdentifier.INCOMPATIBLE_NO_TARGET_OUTPUT_GROUP,
-        "foo");
-    String errorMessage = "GlobalSymbol is deprecated and will be removed soon. It may be "
-        + "temporarily re-enabled by setting --incompatible_no_target_output_group=false";
-
-    new SkylarkTest(
-            ImmutableMap.of("GlobalSymbol", val),
-            "--incompatible_no_target_output_group=false")
-        .setUp("var = GlobalSymbol")
-        .testLookup("var", "foo");
-
-    new SkylarkTest(
-            ImmutableMap.of("GlobalSymbol", val),
-            "--incompatible_no_target_output_group=true")
-        .testIfErrorContains(errorMessage,
-            "var = GlobalSymbol");
-
-    new SkylarkTest(
-            ImmutableMap.of("GlobalSymbol", val),
-            "--incompatible_no_target_output_group=true")
-        .testIfErrorContains(errorMessage,
-            "def my_function():",
-            "  var = GlobalSymbol");
-
-    new SkylarkTest(
-            ImmutableMap.of("GlobalSymbol", val),
-            "--incompatible_no_target_output_group=true")
-        .setUp("GlobalSymbol = 'other'",
-            "var = GlobalSymbol")
-        .testLookup("var", "other");
-  }
-
-  @Test
   public void testFunctionEvaluatedBeforeArguments() throws Exception {
     // ''.nonesuch must be evaluated (and fail) before f().
-    new SkylarkTest()
+    new Scenario()
         .testIfErrorContains(
             "'string' value has no field or method 'nonesuch'",
             "def f(): x = 1//0",
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
index 89c6a02..12720de 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
@@ -318,10 +318,9 @@
   public void testGetSkylarkType_GivesExpectedClassesForListsAndTuples() throws Exception {
     Class<?> emptyTupleClass = Tuple.empty().getClass();
     Class<?> tupleClass = Tuple.of(1, "a", "b").getClass();
-    Class<?> mutableListClass =
-        StarlarkList.copyOf(thread.mutability(), Tuple.of(1, 2, 3)).getClass();
+    Class<?> listClass = StarlarkList.copyOf(null, Tuple.of(1, 2, 3)).getClass();
 
-    assertThat(EvalUtils.getSkylarkType(mutableListClass)).isEqualTo(StarlarkList.class);
+    assertThat(EvalUtils.getSkylarkType(listClass)).isEqualTo(StarlarkList.class);
     assertThat(EvalUtils.getSkylarkType(emptyTupleClass)).isEqualTo(Tuple.class);
     assertThat(EvalUtils.getSkylarkType(tupleClass)).isEqualTo(Tuple.class);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFlagGuardingTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFlagGuardingTest.java
index e84cc2b..57317cf 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFlagGuardingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkFlagGuardingTest.java
@@ -18,8 +18,6 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics.FlagIdentifier;
 import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
-import com.google.devtools.build.lib.testutil.TestMode;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -31,11 +29,6 @@
 @RunWith(JUnit4.class)
 public final class StarlarkFlagGuardingTest extends EvaluationTestCase {
 
-  @Before
-  public final void setup() throws Exception {
-    setMode(TestMode.SKYLARK);
-  }
-
   /** Mock containing exposed methods for flag-guarding tests. */
   @SkylarkModule(name = "Mock", doc = "")
   public static class Mock implements StarlarkValue {
@@ -133,23 +126,23 @@
 
   @Test
   public void testPositionalsOnlyGuardedMethod() throws Exception {
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testEval(
             "mock.positionals_only_method(1, True, 3)", "'positionals_only_method(1, true, 3)'");
 
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testIfErrorContains(
             "in call to positionals_only_method(), parameter 'b' got value of type 'int', want"
                 + " 'bool'",
             "mock.positionals_only_method(1, 3)");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testEval("mock.positionals_only_method(1, 3)", "'positionals_only_method(1, false, 3)'");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testIfErrorContains(
             "in call to positionals_only_method(), parameter 'c' got value of type 'bool', want"
@@ -159,22 +152,22 @@
 
   @Test
   public void testKeywordOnlyGuardedMethod() throws Exception {
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testEval(
             "mock.keywords_only_method(a=1, b=True, c=3)", "'keywords_only_method(1, true, 3)'");
 
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testIfErrorContains(
             "keywords_only_method() missing 1 required named argument: b",
             "mock.keywords_only_method(a=1, c=3)");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testEval("mock.keywords_only_method(a=1, c=3)", "'keywords_only_method(1, false, 3)'");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testIfErrorContains(
             "parameter 'b' is experimental and thus unavailable with the current "
@@ -186,13 +179,13 @@
   @Test
   public void testMixedParamsMethod() throws Exception {
     // def mixed_params_method(a, b, c = ?, d = ?)
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testEval(
             "mock.mixed_params_method(1, True, c=3, d=True)",
             "'mixed_params_method(1, true, 3, true)'");
 
-    new SkylarkTest("--experimental_build_setting_api=true")
+    new Scenario("--experimental_build_setting_api=true")
         .update("mock", new Mock())
         .testIfErrorContains(
             // Missing named arguments (d) are not reported
@@ -201,18 +194,18 @@
             "mock.mixed_params_method(1, c=3)");
 
     // def mixed_params_method(a, b disabled = False, c disabled = 3, d = ?)
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testEval(
             "mock.mixed_params_method(1, d=True)", "'mixed_params_method(1, false, 3, true)'");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testIfErrorContains(
             "mixed_params_method() accepts no more than 1 positional argument but got 2",
             "mock.mixed_params_method(1, True, d=True)");
 
-    new SkylarkTest("--experimental_build_setting_api=false")
+    new Scenario("--experimental_build_setting_api=false")
         .update("mock", new Mock())
         .testIfErrorContains(
             "mixed_params_method() accepts no more than 1 positional argument but got 2",
@@ -221,27 +214,85 @@
 
   @Test
   public void testKeywordsMultipleFlags() throws Exception {
-    new SkylarkTest("--experimental_build_setting_api=true", "--incompatible_no_attr_license=false")
+    new Scenario("--experimental_build_setting_api=true", "--incompatible_no_attr_license=false")
         .update("mock", new Mock())
         .testEval(
             "mock.keywords_multiple_flags(a=42, b=True, c=0)",
             "'keywords_multiple_flags(42, true, 0)'");
 
-    new SkylarkTest("--experimental_build_setting_api=true", "--incompatible_no_attr_license=false")
+    new Scenario("--experimental_build_setting_api=true", "--incompatible_no_attr_license=false")
         .update("mock", new Mock())
         .testIfErrorContains(
             "keywords_multiple_flags() missing 2 required named arguments: b, c",
             "mock.keywords_multiple_flags(a=42)");
 
-    new SkylarkTest("--experimental_build_setting_api=false", "--incompatible_no_attr_license=true")
+    new Scenario("--experimental_build_setting_api=false", "--incompatible_no_attr_license=true")
         .update("mock", new Mock())
         .testEval("mock.keywords_multiple_flags(a=42)", "'keywords_multiple_flags(42, false, 3)'");
 
-    new SkylarkTest("--experimental_build_setting_api=false", "--incompatible_no_attr_license=true")
+    new Scenario("--experimental_build_setting_api=false", "--incompatible_no_attr_license=true")
         .update("mock", new Mock())
         .testIfErrorContains(
             "parameter 'b' is deprecated and will be removed soon. It may be "
                 + "temporarily re-enabled by setting --incompatible_no_attr_license=false",
             "mock.keywords_multiple_flags(a=42, b=True, c=0)");
   }
+
+  @Test
+  public void testExperimentalFlagGuardedValue() throws Exception {
+    // This test uses an arbitrary experimental flag to verify this functionality. If this
+    // experimental flag were to go away, this test may be updated to use any experimental flag.
+    // The flag itself is unimportant to the test.
+    predeclare(
+        "GlobalSymbol",
+        FlagGuardedValue.onlyWhenExperimentalFlagIsTrue(
+            FlagIdentifier.EXPERIMENTAL_BUILD_SETTING_API, "foo"));
+
+    String errorMessage =
+        "GlobalSymbol is experimental and thus unavailable with the current "
+            + "flags. It may be enabled by setting --experimental_build_setting_api";
+
+    new Scenario("--experimental_build_setting_api=true")
+        .setUp("var = GlobalSymbol")
+        .testLookup("var", "foo");
+
+    new Scenario("--experimental_build_setting_api=false")
+        .testIfErrorContains(errorMessage, "var = GlobalSymbol");
+
+    new Scenario("--experimental_build_setting_api=false")
+        .testIfErrorContains(errorMessage, "def my_function():", "  var = GlobalSymbol");
+
+    new Scenario("--experimental_build_setting_api=false")
+        .setUp("GlobalSymbol = 'other'", "var = GlobalSymbol")
+        .testLookup("var", "other");
+  }
+
+  @Test
+  public void testIncompatibleFlagGuardedValue() throws Exception {
+    // This test uses an arbitrary incompatible flag to verify this functionality. If this
+    // incompatible flag were to go away, this test may be updated to use any incompatible flag.
+    // The flag itself is unimportant to the test.
+    predeclare(
+        "GlobalSymbol",
+        FlagGuardedValue.onlyWhenIncompatibleFlagIsFalse(
+            FlagIdentifier.INCOMPATIBLE_NO_TARGET_OUTPUT_GROUP, "foo"));
+
+    String errorMessage =
+        "GlobalSymbol is deprecated and will be removed soon. It may be "
+            + "temporarily re-enabled by setting --incompatible_no_target_output_group=false";
+
+    new Scenario("--incompatible_no_target_output_group=false")
+        .setUp("var = GlobalSymbol")
+        .testLookup("var", "foo");
+
+    new Scenario("--incompatible_no_target_output_group=true")
+        .testIfErrorContains(errorMessage, "var = GlobalSymbol");
+
+    new Scenario("--incompatible_no_target_output_group=true")
+        .testIfErrorContains(errorMessage, "def my_function():", "  var = GlobalSymbol");
+
+    new Scenario("--incompatible_no_target_output_group=true")
+        .setUp("GlobalSymbol = 'other'", "var = GlobalSymbol")
+        .testLookup("var", "other");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
index d8806c5..12fb0d7 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/StarlarkThreadTest.java
@@ -172,7 +172,7 @@
 
   @Test
   public void testBuiltinsCanBeShadowed() throws Exception {
-    StarlarkThread thread = newStarlarkThreadWithSkylarkOptions();
+    StarlarkThread thread = newStarlarkThread();
     EvalUtils.exec(ParserInput.fromLines("True = 123"), thread);
     assertThat(thread.getGlobals().lookup("True")).isEqualTo(123);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
index 7a1bfbc..10c391c 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/util/EvaluationTestCase.java
@@ -17,23 +17,24 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.skylark.SkylarkModules; // a bad dependency
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventCollector;
 import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
 import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
-import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.StarlarkSemanticsOptions;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.Expression;
+import com.google.devtools.build.lib.syntax.Module;
 import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.ParserInput;
-import com.google.devtools.build.lib.syntax.StarlarkFile;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.syntax.SyntaxError;
-import com.google.devtools.build.lib.syntax.ValidationEnvironment;
-import com.google.devtools.build.lib.testutil.TestMode;
-import java.util.ArrayList;
+import com.google.devtools.common.options.Options;
+import com.google.devtools.common.options.OptionsParsingException;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -46,71 +47,54 @@
 public class EvaluationTestCase {
   private EventCollectionApparatus eventCollectionApparatus =
       new EventCollectionApparatus(EventKind.ALL_EVENTS);
-  private TestMode testMode = TestMode.SKYLARK;
-  protected StarlarkThread thread;
-  protected Mutability mutability = Mutability.create("test");
+
+  private StarlarkSemantics semantics = StarlarkSemantics.DEFAULT_SEMANTICS;
+  private final Map<String, Object> extraPredeclared = new HashMap<>();
+  private StarlarkThread thread;
 
   @Before
-  public final void initialize() throws Exception {
+  public final void initialize() {
+    // TODO(adonovan): clean up the lazy initialization of thread when we disentangle
+    // Module from it. Only the module need exist early; the thread can be created
+    // immediately before execution
     thread = newStarlarkThread();
   }
 
-  /**
-   * Creates a new StarlarkThread suitable for the test case. Subclasses may override it to fit
-   * their purpose and e.g. call newBuildStarlarkThread or newStarlarkThread; or they may play with
-   * the testMode to run tests in either or both kinds of StarlarkThread. Note that all
-   * StarlarkThread-s may share the same Mutability, so don't close it.
-   *
-   * @return a fresh StarlarkThread.
-   */
-  public StarlarkThread newStarlarkThread() throws Exception {
-    return newStarlarkThreadWithSkylarkOptions();
-  }
-
-  protected StarlarkThread newStarlarkThreadWithSkylarkOptions(String... skylarkOptions)
-      throws Exception {
-    return newStarlarkThreadWithBuiltinsAndSkylarkOptions(ImmutableMap.of(), skylarkOptions);
-  }
-
-  protected StarlarkThread newStarlarkThreadWithBuiltinsAndSkylarkOptions(
-      Map<String, Object> builtins, String... skylarkOptions) throws Exception {
-    if (testMode == null) {
-      throw new IllegalArgumentException(
-          "TestMode is null. Please set a Testmode via setMode() or set the "
-              + "StarlarkThread manually by overriding newStarlarkThread()");
-    }
-    return testMode.createStarlarkThread(
-        StarlarkThread.makeDebugPrintHandler(getEventHandler()), builtins, skylarkOptions);
+  // Adds a binding to the predeclared environment.
+  protected final void predeclare(String name, Object value) {
+    extraPredeclared.put(name, value);
   }
 
   /**
-   * Sets the specified {@code TestMode} and tries to create the appropriate {@code StarlarkThread}
-   *
-   * @param testMode
-   * @throws Exception
+   * Returns a new thread using the semantics set by setSemantics(), the predeclared environment of
+   * SkylarkModules and prior calls to predeclared(), and a new mutability. Overridden by
+   * subclasses.
    */
-  protected void setMode(TestMode testMode, String... skylarkOptions) throws Exception {
-    this.testMode = testMode;
-    thread = newStarlarkThreadWithSkylarkOptions(skylarkOptions);
+  public StarlarkThread newStarlarkThread() {
+    ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();
+    SkylarkModules.addSkylarkGlobalsToBuilder(envBuilder); // TODO(adonovan): break bad dependency
+    envBuilder.putAll(extraPredeclared);
+    Module module = Module.createForBuiltins(envBuilder.build());
+
+    StarlarkThread thread =
+        StarlarkThread.builder(Mutability.create("test"))
+            .setGlobals(module)
+            .setSemantics(semantics)
+            .build();
+    thread.setPrintHandler(StarlarkThread.makeDebugPrintHandler(getEventHandler()));
+    return thread;
   }
 
-  protected void setMode(TestMode testMode, Map<String, Object> builtins,
-      String... skylarkOptions) throws Exception {
-    this.testMode = testMode;
-    thread = newStarlarkThreadWithBuiltinsAndSkylarkOptions(builtins, skylarkOptions);
-  }
+  /**
+   * Parses the semantics flags and updates the semantics used for subsequent evaluations. Also
+   * reinitializes the thread.
+   */
+  protected final void setSemantics(String... options) throws OptionsParsingException {
+    this.semantics =
+        Options.parse(StarlarkSemanticsOptions.class, options).getOptions().toSkylarkSemantics();
 
-  protected void enableSkylarkMode(Map<String, Object> builtins,
-      String... skylarkOptions) throws Exception {
-    setMode(TestMode.SKYLARK, builtins, skylarkOptions);
-  }
-
-  protected void enableSkylarkMode(String... skylarkOptions) throws Exception {
-    setMode(TestMode.SKYLARK, skylarkOptions);
-  }
-
-  protected void enableBuildMode(String... skylarkOptions) throws Exception {
-    setMode(TestMode.BUILD, skylarkOptions);
+    // Re-initialize the thread with the new semantics. See note at initialize.
+    thread = newStarlarkThread();
   }
 
   public ExtendedEventHandler getEventHandler() {
@@ -148,29 +132,9 @@
   }
 
   /** Joins the lines, parses them as a file, and executes it. */
-  // TODO(adonovan): this function does too much:
-  // - two modes, BUILD vs Skylark.
-  // - parse + validate + BUILD dialect checks + execute.
-  // Break the tests down into tests of just the scanner, parser, validator, build dialect checks,
-  // or execution, and assert that all passes except the one of interest succeed.
-  // All BUILD-dialect stuff belongs in bazel proper (lib.packages), not here.
-  public final void exec(String... lines) throws Exception {
+  public final void exec(String... lines) throws SyntaxError, EvalException, InterruptedException {
     ParserInput input = ParserInput.fromLines(lines);
-    StarlarkFile file = StarlarkFile.parse(input);
-    ValidationEnvironment.validateFile(
-        file, thread.getGlobals(), thread.getSemantics(), testMode == TestMode.BUILD);
-    if (testMode == TestMode.SKYLARK) { // .bzl and other dialects
-      if (!file.ok()) {
-        throw new SyntaxError(file.errors());
-      }
-    } else {
-      // For BUILD mode, validation events are reported but don't (yet)
-      // prevent execution. We also apply BUILD dialect syntax checks.
-      Event.replayEventsOn(getEventHandler(), file.errors());
-      List<String> globs = new ArrayList<>(); // unused
-      PackageFactory.checkBuildSyntax(file, globs, globs, new HashMap<>(), getEventHandler());
-    }
-    EvalUtils.exec(file, thread);
+    EvalUtils.exec(input, thread);
   }
 
   public void checkEvalError(String msg, String... input) throws Exception {
@@ -231,36 +195,42 @@
     return this;
   }
 
-  /**
-   * Encapsulates a separate test which can be executed by a {@code TestMode}
-   */
+  /** Encapsulates a separate test which can be executed by a Scenario. */
   protected interface Testable {
-    public void run() throws Exception;
+    void run() throws Exception;
   }
 
   /**
-   * Base class for test cases that run in specific modes (e.g. Build and/or Skylark)
+   * A test scenario (a script of steps). Beware: Scenario is an inner class that mutates its
+   * enclosing EvaluationTestCase as it executes the script.
    */
-  protected abstract class ModalTestCase {
-    private final SetupActions setup;
+  public final class Scenario {
+    private final SetupActions setup = new SetupActions();
+    private final String[] skylarkOptions;
 
-    protected ModalTestCase() {
-      setup = new SetupActions();
+    public Scenario(String... skylarkOptions) {
+      this.skylarkOptions = skylarkOptions;
+    }
+
+    private void run(Testable testable) throws Exception {
+      setSemantics(skylarkOptions);
+      testable.run();
     }
 
     /** Allows the execution of several statements before each following test. */
-    public ModalTestCase setUp(String... lines) {
+    public Scenario setUp(String... lines) {
       setup.registerExec(lines);
       return this;
     }
 
     /**
      * Allows the update of the specified variable before each following test
+     *
      * @param name The name of the variable that should be updated
      * @param value The new value of the variable
-     * @return This {@code ModalTestCase}
+     * @return This {@code Scenario}
      */
-    public ModalTestCase update(String name, Object value) {
+    public Scenario update(String name, Object value) {
       setup.registerUpdate(name, value);
       return this;
     }
@@ -270,41 +240,40 @@
      *
      * @param src The source expression to be evaluated
      * @param expectedEvalString The expression of the expected result
-     * @return This {@code ModalTestCase}
+     * @return This {@code Scenario}
      * @throws Exception
      */
-    public ModalTestCase testEval(String src, String expectedEvalString) throws Exception {
+    public Scenario testEval(String src, String expectedEvalString) throws Exception {
       runTest(createComparisonTestable(src, expectedEvalString, true));
       return this;
     }
 
     /** Evaluates an expression and compares its result to the expected object. */
-    public ModalTestCase testExpression(String src, Object expected) throws Exception {
+    public Scenario testExpression(String src, Object expected) throws Exception {
       runTest(createComparisonTestable(src, expected, false));
       return this;
     }
 
     /** Evaluates an expression and compares its result to the ordered list of expected objects. */
-    public ModalTestCase testExactOrder(String src, Object... items) throws Exception {
+    public Scenario testExactOrder(String src, Object... items) throws Exception {
       runTest(collectionTestable(src, items));
       return this;
     }
 
     /** Evaluates an expression and checks whether it fails with the expected error. */
-    public ModalTestCase testIfExactError(String expectedError, String... lines) throws Exception {
+    public Scenario testIfExactError(String expectedError, String... lines) throws Exception {
       runTest(errorTestable(true, expectedError, lines));
       return this;
     }
 
     /** Evaluates the expresson and checks whether it fails with the expected error. */
-    public ModalTestCase testIfErrorContains(String expectedError, String... lines)
-        throws Exception {
+    public Scenario testIfErrorContains(String expectedError, String... lines) throws Exception {
       runTest(errorTestable(false, expectedError, lines));
       return this;
     }
 
     /** Looks up the value of the specified variable and compares it to the expected value. */
-    public ModalTestCase testLookup(String name, Object expected) throws Exception {
+    public Scenario testLookup(String name, Object expected) throws Exception {
       runTest(createLookUpTestable(name, expected));
       return this;
     }
@@ -394,8 +363,6 @@
     protected void runTest(Testable testable) throws Exception {
       run(new TestableDecorator(setup, testable));
     }
-
-    protected abstract void run(Testable testable) throws Exception;
   }
 
   /**
@@ -467,75 +434,4 @@
       }
     }
   }
-
-  /**
-   * A class that executes each separate test in both modes (Build and Skylark)
-   */
-  protected class BothModesTest extends ModalTestCase {
-    private final String[] skylarkOptions;
-
-    public BothModesTest(String... skylarkOptions) {
-      this.skylarkOptions = skylarkOptions;
-    }
-
-    /**
-     * Executes the given Testable in both Build and Skylark mode
-     */
-    @Override
-    protected void run(Testable testable) throws Exception {
-      enableSkylarkMode(skylarkOptions);
-      try {
-        testable.run();
-      } catch (Exception e) {
-        throw new Exception("While in Skylark mode", e);
-      }
-
-      enableBuildMode(skylarkOptions);
-      try {
-        testable.run();
-      } catch (Exception e) {
-        throw new Exception("While in Build mode", e);
-      }
-    }
-  }
-
-  /**
-   * A class that runs all tests in Build mode
-   */
-  protected class BuildTest extends ModalTestCase {
-    private final String[] skylarkOptions;
-
-    public BuildTest(String... skylarkOptions) {
-      this.skylarkOptions = skylarkOptions;
-    }
-
-    @Override
-    protected void run(Testable testable) throws Exception {
-      enableBuildMode(skylarkOptions);
-      testable.run();
-    }
-  }
-
-  /**
-   * A class that runs all tests in Skylark mode
-   */
-  protected class SkylarkTest extends ModalTestCase {
-    private final String[] skylarkOptions;
-    private final Map<String, Object> builtins;
-
-    public SkylarkTest(String... skylarkOptions) {
-      this(ImmutableMap.of(), skylarkOptions);
-    }
-
-    public SkylarkTest(Map<String, Object> builtins, String... skylarkOptions) {
-      this.builtins = builtins;
-      this.skylarkOptions = skylarkOptions;
-    }
-
-    @Override
-    protected void run(Testable testable) throws Exception {
-      enableSkylarkMode(builtins, skylarkOptions);
-      testable.run();
-    }
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java b/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java
deleted file mode 100644
index 16f4a59..0000000
--- a/src/test/java/com/google/devtools/build/lib/testutil/TestMode.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2015 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 com.google.devtools.build.lib.testutil;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.devtools.build.lib.analysis.skylark.SkylarkModules;
-import com.google.devtools.build.lib.packages.StarlarkSemanticsOptions;
-import com.google.devtools.build.lib.syntax.Module;
-import com.google.devtools.build.lib.syntax.Mutability;
-import com.google.devtools.build.lib.syntax.StarlarkSemantics;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
-import com.google.devtools.common.options.Options;
-import com.google.devtools.common.options.OptionsParsingException;
-import java.util.Map;
-
-/**
- * Describes a particular testing mode by determining how the appropriate {@code StarlarkThread} has
- * to be created
- */
-public abstract class TestMode {
-
-  private static StarlarkSemantics parseSkylarkSemantics(String... options)
-      throws OptionsParsingException {
-    return Options.parse(StarlarkSemanticsOptions.class, options).getOptions().toSkylarkSemantics();
-  }
-
-  public static final TestMode BUILD =
-      new TestMode() {
-        @Override
-        public StarlarkThread createStarlarkThread(
-            StarlarkThread.PrintHandler printHandler,
-            Map<String, Object> builtins,
-            String... skylarkOptions)
-            throws Exception {
-          StarlarkThread thread =
-              StarlarkThread.builder(Mutability.create("build test"))
-                  .setGlobals(createModule(builtins))
-                  .setSemantics(TestMode.parseSkylarkSemantics(skylarkOptions))
-                  .build();
-          thread.setPrintHandler(printHandler);
-          return thread;
-        }
-      };
-
-  public static final TestMode SKYLARK =
-      new TestMode() {
-        @Override
-        public StarlarkThread createStarlarkThread(
-            StarlarkThread.PrintHandler printHandler,
-            Map<String, Object> builtins,
-            String... skylarkOptions)
-            throws Exception {
-          StarlarkThread thread =
-              StarlarkThread.builder(Mutability.create("skylark test"))
-                  .setGlobals(createModule(builtins))
-                  .setSemantics(TestMode.parseSkylarkSemantics(skylarkOptions))
-                  .build();
-          thread.setPrintHandler(printHandler);
-          return thread;
-        }
-      };
-
-  private static Module createModule(Map<String, Object> builtins) {
-    ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();
-
-    SkylarkModules.addSkylarkGlobalsToBuilder(envBuilder);
-    envBuilder.putAll(builtins);
-    return Module.createForBuiltins(envBuilder.build());
-  }
-
-  public abstract StarlarkThread createStarlarkThread(
-      StarlarkThread.PrintHandler printHandler,
-      Map<String, Object> builtins,
-      String... skylarkOptions)
-      throws Exception;
-}