Rename Skylark to Starlark in src/test/java/com/google/devtools/build/lib/skylark

RELNOTES: None.
PiperOrigin-RevId: 324017771
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/BUILD b/src/test/java/com/google/devtools/build/lib/starlark/BUILD
new file mode 100644
index 0000000..bc25685
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/BUILD
@@ -0,0 +1,98 @@
+load("@rules_java//java:defs.bzl", "java_test")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["**"]) + [
+        "//src/test/java/com/google/devtools/build/lib/starlark/util:srcs",
+    ],
+    visibility = ["//src:__subpackages__"],
+)
+
+java_test(
+    name = "StarlarkTests",
+    timeout = "long",
+    srcs = glob(["*.java"]),
+    shard_count = 25,
+    test_class = "com.google.devtools.build.lib.AllTests",
+    runtime_deps = [
+        "//src/test/java/com/google/devtools/build/lib:test_runner",
+    ],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/actions:action_lookup_key",
+        "//src/main/java/com/google/devtools/build/lib/actions:artifacts",
+        "//src/main/java/com/google/devtools/build/lib/analysis:actions/parameter_file_write_action",
+        "//src/main/java/com/google/devtools/build/lib/analysis:actions/substitution",
+        "//src/main/java/com/google/devtools/build/lib/analysis:actions/template_expansion_action",
+        "//src/main/java/com/google/devtools/build/lib/analysis:actions_provider",
+        "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster",
+        "//src/main/java/com/google/devtools/build/lib/analysis:config/host_transition",
+        "//src/main/java/com/google/devtools/build/lib/analysis:config/starlark_defined_config_transition",
+        "//src/main/java/com/google/devtools/build/lib/analysis:config/transitions/no_transition",
+        "//src/main/java/com/google/devtools/build/lib/analysis:configured_target",
+        "//src/main/java/com/google/devtools/build/lib/analysis:file_provider",
+        "//src/main/java/com/google/devtools/build/lib/analysis:resolved_toolchain_context",
+        "//src/main/java/com/google/devtools/build/lib/analysis:starlark/args",
+        "//src/main/java/com/google/devtools/build/lib/analysis:test/analysis_failure",
+        "//src/main/java/com/google/devtools/build/lib/analysis:test/analysis_failure_info",
+        "//src/main/java/com/google/devtools/build/lib/analysis:test/analysis_test_result_info",
+        "//src/main/java/com/google/devtools/build/lib/analysis:test/instrumented_files_info",
+        "//src/main/java/com/google/devtools/build/lib/analysis:toolchain_collection",
+        "//src/main/java/com/google/devtools/build/lib/analysis:transitive_info_collection",
+        "//src/main/java/com/google/devtools/build/lib/analysis:view_creation_failed_exception",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
+        "//src/main/java/com/google/devtools/build/lib/events",
+        "//src/main/java/com/google/devtools/build/lib/packages",
+        "//src/main/java/com/google/devtools/build/lib/packages:build_type",
+        "//src/main/java/com/google/devtools/build/lib/packages:type",
+        "//src/main/java/com/google/devtools/build/lib/rules/cpp",
+        "//src/main/java/com/google/devtools/build/lib/rules/java:java-compilation",
+        "//src/main/java/com/google/devtools/build/lib/rules/objc",
+        "//src/main/java/com/google/devtools/build/lib/rules/python",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:aspect_value_key",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:configured_target_and_data",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster",
+        "//src/main/java/com/google/devtools/build/lib/util",
+        "//src/main/java/com/google/devtools/build/lib/util:classpath",
+        "//src/main/java/com/google/devtools/build/lib/util:filetype",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/java/net/starlark/java/annot",
+        "//src/test/java/com/google/devtools/build/lib/actions/util",
+        "//src/test/java/com/google/devtools/build/lib/analysis/util",
+        "//src/test/java/com/google/devtools/build/lib/packages:testutil",
+        "//src/test/java/com/google/devtools/build/lib/starlark/util",
+        "//src/test/java/com/google/devtools/build/lib/syntax/util",
+        "//src/test/java/com/google/devtools/build/lib/testutil",
+        "//src/test/java/com/google/devtools/build/lib/testutil:JunitUtils",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+test_suite(
+    name = "windows_tests",
+    tags = [
+        "-no_windows",
+        "-slow",
+    ],
+)
+
+test_suite(
+    name = "all_windows_tests",
+    tests = [
+        ":windows_tests",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkActionProviderTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkActionProviderTest.java
new file mode 100644
index 0000000..15f6f76
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkActionProviderTest.java
@@ -0,0 +1,171 @@
+// Copyright 2018 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisResult;
+import com.google.devtools.build.lib.analysis.ConfiguredAspect;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.syntax.Dict;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.Sequence;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the Starlark-accessible actions provider on rule configured targets. */
+@RunWith(JUnit4.class)
+public class StarlarkActionProviderTest extends AnalysisTestCase {
+
+  @Test
+  public void aspectGetsActionProviderForNativeRule() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "foo = provider()",
+        "def _impl(target, ctx):",
+        "   return [foo(actions = target.actions)]",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file(
+        "test/BUILD",
+        "genrule(",
+        "   name = 'xxx',",
+        "   cmd = 'echo \"hello\" > $@',",
+        "   outs = ['mygen.out']",
+        ")");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    StarlarkProvider.Key fooKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "foo");
+
+    StructImpl fooProvider = (StructImpl) configuredAspect.get(fooKey);
+    assertThat(fooProvider.getValue("actions")).isNotNull();
+    @SuppressWarnings("unchecked")
+    Sequence<ActionAnalysisMetadata> actions =
+        (Sequence<ActionAnalysisMetadata>) fooProvider.getValue("actions");
+    assertThat(actions).isNotEmpty();
+
+    ActionAnalysisMetadata action = actions.get(0);
+    assertThat(action.getMnemonic()).isEqualTo("Genrule");
+    assertThat(action).isInstanceOf(AbstractAction.class);
+    assertThat(((AbstractAction) action).getExecutionInfo()).isNotNull();
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void aspectGetsActionProviderForStarlarkRule() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "foo = provider()",
+        "def _impl(target, ctx):",
+        "   mnemonics = [a.mnemonic for a in target.actions]",
+        "   envs = [a.env for a in target.actions]",
+        "   execution_info = [a.execution_info for a in target.actions]",
+        "   inputs = [a.inputs.to_list() for a in target.actions]",
+        "   outputs = [a.outputs.to_list() for a in target.actions]",
+        "   argv = [a.argv for a in target.actions]",
+        "   return [foo(",
+        "       actions = target.actions,",
+        "       mnemonics = mnemonics,",
+        "       envs = envs,",
+        "       execution_info = execution_info,",
+        "       inputs = inputs,",
+        "       outputs = outputs,",
+        "       argv = argv",
+        "    )]",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file(
+        "test/rule.bzl",
+        "def impl(ctx):",
+        "  output_file0 = ctx.actions.declare_file('myfile0')",
+        "  output_file1 = ctx.actions.declare_file('myfile1')",
+        "  executable = ctx.actions.declare_file('executable')",
+        "  ctx.actions.run(outputs=[output_file0], executable=executable,",
+        "      mnemonic='MyAction0', env={'foo':'bar', 'pet':'puppy'})",
+        "  ctx.actions.run_shell(outputs=[executable, output_file1],",
+        "      command='fakecmd', mnemonic='MyAction1', env={'pet':'bunny'})",
+        "  return None",
+        "my_rule = rule(impl)");
+    scratch.file(
+        "test/BUILD", "load('//test:rule.bzl', 'my_rule')", "my_rule(", "   name = 'xxx',", ")");
+
+    useConfiguration("--experimental_google_legacy_api");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    StarlarkProvider.Key fooKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "foo");
+    StructImpl fooProvider = (StructImpl) configuredAspect.get(fooKey);
+    assertThat(fooProvider.getValue("actions")).isNotNull();
+
+    Sequence<ActionAnalysisMetadata> actions =
+        (Sequence<ActionAnalysisMetadata>) fooProvider.getValue("actions");
+    assertThat(actions).hasSize(2);
+
+    Sequence<String> mnemonics = (Sequence<String>) fooProvider.getValue("mnemonics");
+    assertThat(mnemonics).containsExactly("MyAction0", "MyAction1");
+
+    Sequence<Dict<String, String>> envs =
+        (Sequence<Dict<String, String>>) fooProvider.getValue("envs");
+    assertThat(envs)
+        .containsExactly(
+            Dict.of((Mutability) null, "foo", "bar", "pet", "puppy"),
+            Dict.of((Mutability) null, "pet", "bunny"));
+
+    Sequence<Dict<String, String>> executionInfo =
+        (Sequence<Dict<String, String>>) fooProvider.getValue("execution_info");
+    assertThat(executionInfo).isNotNull();
+
+    Sequence<Sequence<Artifact>> inputs =
+        (Sequence<Sequence<Artifact>>) fooProvider.getValue("inputs");
+    assertThat(flattenArtifactNames(inputs)).containsExactly("executable");
+
+    Sequence<Sequence<Artifact>> outputs =
+        (Sequence<Sequence<Artifact>>) fooProvider.getValue("outputs");
+    assertThat(flattenArtifactNames(outputs)).containsExactly("myfile0", "executable", "myfile1");
+
+    Sequence<Sequence<String>> argv = (Sequence<Sequence<String>>) fooProvider.getValue("argv");
+    assertThat(argv.get(0)).hasSize(1);
+    assertThat(argv.get(0).get(0)).endsWith("executable");
+    assertThat(argv.get(1)).contains("fakecmd");
+  }
+
+  private static List<String> flattenArtifactNames(Sequence<Sequence<Artifact>> artifactLists) {
+    return artifactLists.stream()
+        .flatMap(artifacts -> artifacts.stream())
+        .map(artifact -> artifact.getFilename())
+        .collect(Collectors.toList());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAnnotationContractTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAnnotationContractTest.java
new file mode 100644
index 0000000..6bd9012
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAnnotationContractTest.java
@@ -0,0 +1,84 @@
+// Copyright 2018 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.starlark;
+
+import com.google.devtools.build.lib.util.Classpath;
+import java.lang.reflect.Method;
+import net.starlark.java.annot.StarlarkBuiltin;
+import net.starlark.java.annot.StarlarkInterfaceUtils;
+import net.starlark.java.annot.StarlarkMethod;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests that bazel usages of {@link StarlarkMethod} and {@link StarlarkBuiltin} abide by the
+ * contracts specified in their documentation.
+ *
+ * <p>Tests in this class use the java reflection API.
+ *
+ * <p>This verification *would* be done via annotation processor, but annotation processors in java
+ * don't have access to the full set of information that the java reflection API has.
+ */
+@RunWith(JUnit4.class)
+public class StarlarkAnnotationContractTest {
+
+  // Common prefix of packages in bazel that may have classes that implement or extend a
+  // Starlark type.
+  private static final String MODULES_PACKAGE_PREFIX = "com/google/devtools/build";
+
+  /**
+   * Verifies that every class in bazel that implements or extends a Starlark type has a clearly
+   * resolvable type.
+   *
+   * <p>If this test fails, it indicates the following error scenario:
+   *
+   * <p>Suppose class A is a subclass of both B and C, where B and C are annotated with {@link
+   * StarlarkBuiltin} annotations (and are thus considered "Starlark types"). If B is not a subclass
+   * of C (nor visa versa), then it's impossible to resolve whether A is of type B or if A is of
+   * type C. It's both! The way to resolve this is usually to have A be its own type (annotated with
+   * {@link StarlarkBuiltin}), and thus have the explicit type of A be semantically "B and C".
+   */
+  @Test
+  public void testResolvableStarlarkBuiltins() throws Exception {
+    for (Class<?> candidateClass : Classpath.findClasses(MODULES_PACKAGE_PREFIX)) {
+      StarlarkInterfaceUtils.getStarlarkBuiltin(candidateClass);
+    }
+  }
+
+  /**
+   * Verifies that no class or interface has a method annotated with {@link StarlarkMethod} unless
+   * that class or interface is annotated with either {@link StarlarkGlobalLibrary} or with {@link
+   * StarlarkBuiltin}.
+   */
+  @Test
+  public void testStarlarkCallableScope() throws Exception {
+    for (Class<?> candidateClass : Classpath.findClasses(MODULES_PACKAGE_PREFIX)) {
+      if (StarlarkInterfaceUtils.getStarlarkBuiltin(candidateClass) == null
+          && !StarlarkInterfaceUtils.hasStarlarkGlobalLibrary(candidateClass)) {
+        for (Method method : candidateClass.getMethods()) {
+          StarlarkMethod callable = StarlarkInterfaceUtils.getStarlarkMethod(method);
+          if (callable != null && method.getDeclaringClass() == candidateClass) {
+            throw new AssertionError(
+                String.format(
+                    "Class %s has a StarlarkMethod method %s but is neither a @StarlarkBuiltin"
+                        + " nor a @StarlarkGlobalLibrary",
+                    candidateClass, method.getName()));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
new file mode 100644
index 0000000..cd963d6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkDefinedAspectsTest.java
@@ -0,0 +1,3120 @@
+// 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.analysis.OutputGroupInfo.INTERNAL_SUFFIX;
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisResult;
+import com.google.devtools.build.lib.analysis.AspectValue;
+import com.google.devtools.build.lib.analysis.ConfiguredAspect;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.OutputGroupInfo;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.config.HostTransition;
+import com.google.devtools.build.lib.analysis.config.transitions.NoTransition;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.AspectDefinition;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.packages.util.MockObjcSupport;
+import com.google.devtools.build.lib.packages.util.MockProtoSupport;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration;
+import com.google.devtools.build.lib.rules.objc.ObjcProtoProvider;
+import com.google.devtools.build.lib.skyframe.AspectValueKey.AspectKey;
+import com.google.devtools.build.lib.syntax.Sequence;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for Starlark aspects */
+@RunWith(JUnit4.class)
+public class StarlarkDefinedAspectsTest extends AnalysisTestCase {
+  protected boolean keepGoing() {
+    return false;
+  }
+
+  private static final String LINE_SEPARATOR = System.lineSeparator();
+
+  @Before
+  public final void initializeToolsConfigMock() throws Exception {
+    // Required for tests including the objc_library rule.
+    MockObjcSupport.setup(mockToolsConfig);
+    // Required for tests including the proto_library rule.
+    MockProtoSupport.setup(mockToolsConfig);
+  }
+
+  @Test
+  public void simpleAspect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   print('This aspect does nothing')",
+        "   return struct()",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("//test:aspect.bzl%MyAspect(//test:xxx)");
+  }
+
+  @Test
+  public void aspectWithSingleDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "foo = provider()",
+        "def _impl(target, ctx):",
+        "   return foo()",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("//test:aspect.bzl%MyAspect(//test:xxx)");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    StarlarkProvider.Key fooKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "foo");
+
+    assertThat(configuredAspect.get(fooKey).getProvider().getKey()).isEqualTo(fooKey);
+  }
+
+  @Test
+  public void aspectWithDeclaredProviders() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "foo = provider()",
+        "bar = provider()",
+        "def _impl(target, ctx):",
+        "   return [foo(), bar()]",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("//test:aspect.bzl%MyAspect(//test:xxx)");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    StarlarkProvider.Key fooKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "foo");
+    StarlarkProvider.Key barKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "bar");
+
+    assertThat(configuredAspect.get(fooKey).getProvider().getKey()).isEqualTo(fooKey);
+    assertThat(configuredAspect.get(barKey).getProvider().getKey()).isEqualTo(barKey);
+  }
+
+  @Test
+  public void aspectWithDeclaredProvidersInAStruct() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "foo = provider()",
+        "bar = provider()",
+        "def _impl(target, ctx):",
+        "   return struct(foobar='foobar', providers=[foo(), bar()])",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("//test:aspect.bzl%MyAspect(//test:xxx)");
+
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    StarlarkProvider.Key fooKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "foo");
+    StarlarkProvider.Key barKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "bar");
+
+    assertThat(configuredAspect.get(fooKey).getProvider().getKey()).isEqualTo(fooKey);
+    assertThat(configuredAspect.get(barKey).getProvider().getKey()).isEqualTo(barKey);
+  }
+
+  private Iterable<String> getAspectDescriptions(AnalysisResult analysisResult) {
+    return Iterables.transform(
+        analysisResult.getAspectsMap().keySet(),
+        aspectKey ->
+            String.format("%s(%s)", aspectKey.getAspectClass().getName(), aspectKey.getLabel()));
+  }
+
+  @Test
+  public void aspectCommandLineLabel() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   print('This aspect does nothing')",
+        "   return struct()",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("//test:aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("//test:aspect.bzl%MyAspect(//test:xxx)");
+  }
+
+  @Test
+  public void aspectCommandLineRepoLabel() throws Exception {
+    scratch.overwriteFile(
+        "WORKSPACE",
+        scratch.readFile("WORKSPACE"),
+        "local_repository(name='local', path='local/repo')");
+    scratch.file("local/repo/WORKSPACE");
+    scratch.file(
+        "local/repo/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   print('This aspect does nothing')",
+        "   return struct()",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("local/repo/BUILD");
+
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("@local//:aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    assertThat(getAspectDescriptions(analysisResult))
+        .containsExactly("@local//:aspect.bzl%MyAspect(//test:xxx)");
+  }
+
+  private Iterable<String> getLabelsToBuild(AnalysisResult analysisResult) {
+    return Iterables.transform(
+        analysisResult.getTargetsToBuild(),
+        configuredTarget -> configuredTarget.getLabel().toString());
+  }
+
+  @Test
+  public void aspectAllowsFragmentsToBeSpecified() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   print('This aspect does nothing')",
+        "   return struct()",
+        "MyAspect = aspect(implementation=_impl, fragments=['java'], host_fragments=['cpp'])");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+
+    AspectKey key = Iterables.getOnlyElement(analysisResult.getAspectsMap().keySet());
+    AspectValue aspectValue =
+        (AspectValue) skyframeExecutor.getEvaluatorForTesting().getExistingValue(key);
+    AspectDefinition aspectDefinition = aspectValue.getAspect().getDefinition();
+    assertThat(
+            aspectDefinition
+                .getConfigurationFragmentPolicy()
+                .isLegalConfigurationFragment(JavaConfiguration.class, NoTransition.INSTANCE))
+        .isTrue();
+    assertThat(
+            aspectDefinition
+                .getConfigurationFragmentPolicy()
+                .isLegalConfigurationFragment(JavaConfiguration.class, HostTransition.INSTANCE))
+        .isFalse();
+    assertThat(
+            aspectDefinition
+                .getConfigurationFragmentPolicy()
+                .isLegalConfigurationFragment(CppConfiguration.class, NoTransition.INSTANCE))
+        .isFalse();
+    assertThat(
+            aspectDefinition
+                .getConfigurationFragmentPolicy()
+                .isLegalConfigurationFragment(CppConfiguration.class, HostTransition.INSTANCE))
+        .isTrue();
+  }
+
+  @Test
+  public void aspectPropagating() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   s = depset([target.label], transitive = [i.target_labels for i in ctx.rule.attr.deps])",
+        "   c = depset([ctx.rule.kind], transitive = [i.rule_kinds for i in ctx.rule.attr.deps])",
+        "   return struct(target_labels = s, rule_kinds = c)",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        "   attr_aspects=['deps'],",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "java_library(",
+        "     name = 'xxx',",
+        "     srcs = ['A.java'],",
+        "     deps = [':yyy'],",
+        ")");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    assertThat(configuredAspect).isNotNull();
+    Object names = configuredAspect.get("target_labels");
+    assertThat(names).isInstanceOf(Depset.class);
+    assertThat(
+            Iterables.transform(
+                ((Depset) names).toList(),
+                o -> {
+                  assertThat(o).isInstanceOf(Label.class);
+                  return o.toString();
+                }))
+        .containsExactly("//test:xxx", "//test:yyy");
+    Object ruleKinds = configuredAspect.get("rule_kinds");
+    assertThat(ruleKinds).isInstanceOf(Depset.class);
+    assertThat(((Depset) ruleKinds).toList()).containsExactly("java_library");
+  }
+
+  @Test
+  public void aspectsPropagatingForDefaultAndImplicit() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   s = []",
+        "   c = []",
+        "   a = ctx.rule.attr",
+        "   if getattr(a, '_defaultattr', None):",
+        "       s += [a._defaultattr.target_labels]",
+        "       c += [a._defaultattr.rule_kinds]",
+        "   if getattr(a, '_cc_toolchain', None):",
+        "       s += [a._cc_toolchain.target_labels]",
+        "       c += [a._cc_toolchain.rule_kinds]",
+        "   return struct(",
+        "       target_labels = depset([target.label], transitive = s),",
+        "       rule_kinds = depset([ctx.rule.kind], transitive = c))",
+        "",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "",
+        "my_rule = rule(implementation = _rule_impl,",
+        "   attrs = { '_defaultattr' : attr.label(default = Label('//test:xxx')) },",
+        ")",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        "   attr_aspects=['_defaultattr', '_cc_toolchain'],",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "cc_library(",
+        "     name = 'xxx',",
+        ")",
+        "my_rule(",
+        "     name = 'yyy',",
+        ")");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:yyy");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    assertThat(configuredAspect).isNotNull();
+    Object nameSet = configuredAspect.get("target_labels");
+    ImmutableList<String> names =
+        ImmutableList.copyOf(
+            Iterables.transform(
+                ((Depset) nameSet).toList(),
+                o -> {
+                  assertThat(o).isInstanceOf(Label.class);
+                  return ((Label) o).getName();
+                }));
+
+    assertThat(names).containsAtLeast("xxx", "yyy");
+    // Third is the C++ toolchain; its name changes between Blaze and Bazel.
+    assertThat(names).hasSize(3);
+  }
+
+  @Test
+  public void aspectsDirOnMergedTargets() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct(aspect_provider = 'data')",
+        "",
+        "p = provider()",
+        "MyAspect = aspect(implementation=_impl)",
+        "def _rule_impl(ctx):",
+        "   if ctx.attr.dep:",
+        "      return [p(dir = dir(ctx.attr.dep))]",
+        "   return [p()]",
+        "",
+        "my_rule = rule(implementation = _rule_impl,",
+        "   attrs = { 'dep' : attr.label(aspects = [MyAspect]) },",
+        ")");
+    StarlarkProvider.Key providerKey =
+        new StarlarkProvider.Key(Label.parseAbsoluteUnchecked("//test:aspect.bzl"), "p");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "my_rule(name = 'xxx',)",
+        "my_rule(name = 'yyy', dep = ':xxx')");
+    AnalysisResult analysisResult = update("//test:yyy");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+
+    StructImpl names = (StructImpl) target.get(providerKey);
+    assertThat((Iterable<?>) names.getValue("dir"))
+        .containsExactly(
+            "actions",
+            "aspect_provider",
+            "data_runfiles",
+            "default_runfiles",
+            "files",
+            "files_to_run",
+            "label",
+            "output_groups");
+  }
+
+  @Test
+  public void aspectWithOutputGroupsDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   f = target[OutputGroupInfo]._hidden_top_level" + INTERNAL_SUFFIX,
+        "   return [OutputGroupInfo(my_result = f)]",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        ")");
+    scratch.file(
+        "test/BUILD", "java_library(", "     name = 'xxx',", "     srcs = ['A.java'],", ")");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    OutputGroupInfo outputGroupInfo = OutputGroupInfo.get(configuredAspect);
+
+    assertThat(outputGroupInfo).isNotNull();
+    NestedSet<Artifact> names = outputGroupInfo.getOutputGroup("my_result");
+    assertThat(names.toList()).isNotEmpty();
+    NestedSet<Artifact> expectedSet =
+        OutputGroupInfo.get(getConfiguredTarget("//test:xxx"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    assertThat(names.toList()).containsExactlyElementsIn(expectedSet.toList());
+  }
+
+  @Test
+  public void aspectWithOutputGroupsAsListDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   g = target[OutputGroupInfo]._hidden_top_level" + INTERNAL_SUFFIX,
+        "   return [OutputGroupInfo(my_result=g.to_list())]",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        ")");
+    scratch.file(
+        "test/BUILD", "java_library(", "     name = 'xxx',", "     srcs = ['A.java'],", ")");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(
+            Iterables.transform(
+                analysisResult.getTargetsToBuild(),
+                configuredTarget -> configuredTarget.getLabel().toString()))
+        .containsExactly("//test:xxx");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    OutputGroupInfo outputGroupInfo = OutputGroupInfo.get(configuredAspect);
+    assertThat(outputGroupInfo).isNotNull();
+    NestedSet<Artifact> names = outputGroupInfo.getOutputGroup("my_result");
+    assertThat(names.toList()).isNotEmpty();
+    NestedSet<Artifact> expectedSet =
+        OutputGroupInfo.get(getConfiguredTarget("//test:xxx"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    assertThat(names.toList()).containsExactlyElementsIn(expectedSet.toList());
+  }
+
+  @Test
+  public void aspectsFromStarlarkRules() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   s = depset([target.label], transitive = [i.target_labels for i in ctx.rule.attr.deps])",
+        "   return struct(target_labels = s)",
+        "",
+        "def _rule_impl(ctx):",
+        "   s = depset(transitive = [i.target_labels for i in ctx.attr.attr])",
+        "   return struct(rule_deps = s)",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_aspect_impl,",
+        "   attr_aspects=['deps'],",
+        ")",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : ",
+        "             attr.label_list(mandatory=True, allow_files=True, aspects = [MyAspect]) },",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = [':yyy'],",
+        ")");
+
+    AnalysisResult analysisResult = update("//test:xxx");
+    assertThat(getLabelsToBuild(analysisResult)).containsExactly("//test:xxx");
+    ConfiguredTarget target = analysisResult.getTargetsToBuild().iterator().next();
+    Object names = target.get("rule_deps");
+    assertThat(names).isInstanceOf(Depset.class);
+    assertThat(
+            Iterables.transform(
+                ((Depset) names).toList(),
+                o -> {
+                  assertThat(o).isInstanceOf(Label.class);
+                  return o.toString();
+                }))
+        .containsExactly("//test:yyy");
+  }
+
+  @Test
+  public void aspectsNonExported() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   return []",
+        "",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "",
+        "def mk_aspect():",
+        "   return aspect(implementation=_aspect_impl)",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : attr.label_list(aspects = [mk_aspect()]) },",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = [':yyy'],",
+        ")");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult analysisResult = update("//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(analysisResult.hasError()).isTrue();
+    } catch (ViewCreationFailedException | TargetParsingException e) {
+      // expected
+    }
+
+    assertContainsEvent("ERROR /workspace/test/aspect.bzl:11:38");
+    assertContainsEvent("Aspects should be top-level values in extension files that define them.");
+  }
+
+  @Test
+  public void providerNonExported() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def mk_provider():",
+        "   return provider()",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : attr.label_list(providers = [mk_provider()]) },",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'my_rule')",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = [':yyy'],",
+        ")");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult analysisResult = update("//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(analysisResult.hasError()).isTrue();
+    } catch (ViewCreationFailedException | TargetParsingException e) {
+      // expected
+    }
+
+    assertContainsEvent("ERROR /workspace/test/rule.bzl:7:38");
+    assertContainsEvent(
+        "Providers should be top-level values in extension files that define them.");
+  }
+
+  @Test
+  public void aspectOnLabelAttr() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   return struct(aspect_data='foo')",
+        "",
+        "def _rule_impl(ctx):",
+        "   return struct(data=ctx.attr.attr.aspect_data)",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_aspect_impl,",
+        ")",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : ",
+        "             attr.label(aspects = [MyAspect]) },",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = ':yyy',",
+        ")");
+
+    AnalysisResult analysisResult = update("//test:xxx");
+    ConfiguredTarget target = analysisResult.getTargetsToBuild().iterator().next();
+    Object value = target.get("data");
+    assertThat(value).isEqualTo("foo");
+  }
+
+  @Test
+  public void labelKeyedStringDictAllowsAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   return struct(aspect_data=target.label.name)",
+        "",
+        "def _rule_impl(ctx):",
+        "   return struct(",
+        "       data=','.join(['{}:{}'.format(dep.aspect_data, val)",
+        "                      for dep, val in ctx.attr.attr.items()]))",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_aspect_impl,",
+        ")",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : ",
+        "             attr.label_keyed_string_dict(aspects = [MyAspect]) },",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "java_library(",
+        "     name = 'yyy',",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = {':yyy': 'zzz'},",
+        ")");
+
+    AnalysisResult analysisResult = update("//test:xxx");
+    ConfiguredTarget target = analysisResult.getTargetsToBuild().iterator().next();
+    Object value = target.get("data");
+    assertThat(value).isEqualTo("yyy:zzz");
+  }
+
+  @Test
+  public void aspectsDoNotAttachToFiles() throws Exception {
+    FileSystemUtils.appendIsoLatin1(
+        scratch.resolve("WORKSPACE"), "bind(name = 'yyy', actual = '//test:zzz.jar')");
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        "   attr_aspects=['deps'],",
+        ")");
+    scratch.file("test/zzz.jar");
+    scratch.file(
+        "test/BUILD",
+        "exports_files(['zzz.jar'])",
+        "java_library(",
+        "     name = 'xxx',",
+        "     srcs = ['A.java'],",
+        "     deps = ['//external:yyy'],",
+        ")");
+
+    AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(result.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectsDoNotAttachToTopLevelFiles() throws Exception {
+    FileSystemUtils.appendIsoLatin1(
+        scratch.resolve("WORKSPACE"), "bind(name = 'yyy', actual = '//test:zzz.jar')");
+    scratch.file(
+        "test/aspect.bzl",
+        "p = provider()",
+        "def _impl(target, ctx):",
+        "   return [p()]",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        "   attr_aspects=['deps'],",
+        ")");
+    scratch.file("test/zzz.jar");
+    scratch.file(
+        "test/BUILD",
+        "exports_files(['zzz.jar'])",
+        "java_library(",
+        "     name = 'xxx',",
+        "     srcs = ['A.java'],",
+        "     deps = ['//external:yyy'],",
+        ")");
+
+    AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:zzz.jar");
+    assertThat(result.hasError()).isFalse();
+    assertThat(
+            Iterables.getOnlyElement(result.getAspectsMap().values())
+                .getProviders()
+                .getProviderCount())
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void aspectFailingExecution() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return 1 // 0",
+        "",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:1:13: in "
+            + "//test:aspect.bzl%MyAspect aspect on java_library rule //test:xxx: \n"
+            + "Traceback (most recent call last):"
+            + LINE_SEPARATOR
+            + "\tFile \"/workspace/test/BUILD\", line 1"
+            + LINE_SEPARATOR
+            + "\t\t//test:aspect.bzl%MyAspect(...)"
+            + LINE_SEPARATOR
+            + "\tFile \"/workspace/test/aspect.bzl\", line 2, in _impl"
+            + LINE_SEPARATOR
+            + "\t\t1 // 0"
+            + LINE_SEPARATOR
+            + "integer division by zero");
+  }
+
+  @Test
+  public void aspectFailingReturnsNotAStruct() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return 0",
+        "",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "Aspect implementation should return a struct, a list, or a provider "
+            + "instance, but got int");
+  }
+
+  @Test
+  public void aspectFailingOrphanArtifacts() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "  ctx.actions.declare_file('missing_in_action.txt')",
+        "  return struct()",
+        "",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:1:13: in "
+            + "//test:aspect.bzl%MyAspect aspect on java_library rule //test:xxx: \n"
+            + "\n"
+            + "\n"
+            + "The following files have no generating action:\n"
+            + "test/missing_in_action.txt\n");
+  }
+
+  @Test
+  public void aspectSkippingOrphanArtifactsWithLocation() throws Exception {
+    scratch.file(
+        "simple/print.bzl",
+        "def _print_expanded_location_impl(target, ctx):",
+        "    return struct(result=ctx.expand_location(ctx.rule.attr.cmd, []))",
+        "",
+        "print_expanded_location = aspect(",
+        "    implementation = _print_expanded_location_impl,",
+        ")");
+    scratch.file(
+        "simple/BUILD",
+        "filegroup(",
+        "    name = \"files\",",
+        "    srcs = [\"afile\"],",
+        ")",
+        "",
+        "genrule(",
+        "    name = \"concat_all_files\",",
+        "    srcs = [\":files\"],",
+        "    outs = [\"concatenated.txt\"],",
+        "    cmd = \"$(location :files)\"",
+        ")");
+
+    reporter.removeHandler(failFastHandler);
+    AnalysisResult analysisResult =
+        update(
+            ImmutableList.of("//simple:print.bzl%print_expanded_location"),
+            "//simple:concat_all_files");
+    assertThat(analysisResult.hasError()).isFalse();
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    String result = (String) configuredAspect.get("result");
+
+    assertThat(result).isEqualTo("simple/afile");
+  }
+
+  @Test
+  public void topLevelAspectIsNotAnAspect() throws Exception {
+    scratch.file("test/aspect.bzl", "MyAspect = 4");
+    scratch.file("test/BUILD", "java_library(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("MyAspect from //test:aspect.bzl is not an aspect");
+  }
+
+  @Test
+  public void duplicateOutputGroups() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "  f = ctx.actions.declare_file('f.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return struct(output_groups = { 'duplicate' : depset([f]) })",
+        "",
+        "MyAspect = aspect(implementation=_impl)",
+        "def _rule_impl(ctx):",
+        "  g = ctx.actions.declare_file('g.txt')",
+        "  ctx.actions.write(g, 'g')",
+        "  return struct(output_groups = { 'duplicate' : depset([g]) })",
+        "my_rule = rule(_rule_impl)",
+        "def _noop(ctx):",
+        "  pass",
+        "rbase = rule(_noop, attrs = { 'dep' : attr.label(aspects = [MyAspect]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule', 'rbase')",
+        "my_rule(name = 'xxx')",
+        "rbase(name = 'yyy', dep = ':xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update("//test:yyy");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("ERROR /workspace/test/BUILD:3:6: Output group duplicate provided twice");
+  }
+
+  @Test
+  public void outputGroupsFromOneAspect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target, ctx):",
+        "  f = ctx.actions.declare_file(target.label.name + '_a1.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return struct(output_groups = { 'a1_group' : depset([f]) })",
+        "",
+        "a1 = aspect(implementation=_a1_impl, attr_aspects = ['dep'])",
+        "def _rule_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct()",
+        "  og = {k:ctx.attr.dep.output_groups[k] for k in ctx.attr.dep.output_groups}",
+        "  return struct(output_groups = og)",
+        "my_rule1 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a1]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule1')",
+        "my_rule1(name = 'base')",
+        "my_rule1(name = 'xxx', dep = ':base')");
+
+    AnalysisResult analysisResult = update("//test:xxx");
+    OutputGroupInfo outputGroupInfo =
+        OutputGroupInfo.get(Iterables.getOnlyElement(analysisResult.getTargetsToBuild()));
+    assertThat(getOutputGroupContents(outputGroupInfo, "a1_group"))
+        .containsExactly("test/base_a1.txt");
+  }
+
+  @Test
+  public void outputGroupsDeclaredProviderFromOneAspect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target, ctx):",
+        "  f = ctx.actions.declare_file(target.label.name + '_a1.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return [OutputGroupInfo(a1_group = depset([f]))]",
+        "",
+        "a1 = aspect(implementation=_a1_impl, attr_aspects = ['dep'])",
+        "def _rule_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct()",
+        "  return [OutputGroupInfo(a1_group = ctx.attr.dep[OutputGroupInfo].a1_group)]",
+        "my_rule1 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a1]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule1')",
+        "my_rule1(name = 'base')",
+        "my_rule1(name = 'xxx', dep = ':base')");
+
+    AnalysisResult analysisResult = update("//test:xxx");
+    OutputGroupInfo outputGroupInfo =
+        OutputGroupInfo.get(Iterables.getOnlyElement(analysisResult.getTargetsToBuild()));
+    assertThat(getOutputGroupContents(outputGroupInfo, "a1_group"))
+        .containsExactly("test/base_a1.txt");
+  }
+
+  @Test
+  public void outputGroupsFromTwoAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target, ctx):",
+        "  f = ctx.actions.declare_file(target.label.name + '_a1.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return struct(output_groups = { 'a1_group' : depset([f]) })",
+        "",
+        "a1 = aspect(implementation=_a1_impl, attr_aspects = ['dep'])",
+        "def _rule_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct()",
+        "  og = {k:ctx.attr.dep.output_groups[k] for k in ctx.attr.dep.output_groups}",
+        "  return struct(output_groups = og)",
+        "my_rule1 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a1]) })",
+        "def _a2_impl(target, ctx):",
+        "  g = ctx.actions.declare_file(target.label.name + '_a2.txt')",
+        "  ctx.actions.write(g, 'f')",
+        "  return struct(output_groups = { 'a2_group' : depset([g]) })",
+        "",
+        "a2 = aspect(implementation=_a2_impl, attr_aspects = ['dep'])",
+        "my_rule2 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a2]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule1', 'my_rule2')",
+        "my_rule1(name = 'base')",
+        "my_rule1(name = 'xxx', dep = ':base')",
+        "my_rule2(name = 'yyy', dep = ':xxx')");
+
+    AnalysisResult analysisResult = update("//test:yyy");
+    OutputGroupInfo outputGroupInfo =
+        OutputGroupInfo.get(Iterables.getOnlyElement(analysisResult.getTargetsToBuild()));
+    assertThat(getOutputGroupContents(outputGroupInfo, "a1_group"))
+        .containsExactly("test/base_a1.txt");
+    assertThat(getOutputGroupContents(outputGroupInfo, "a2_group"))
+        .containsExactly("test/xxx_a2.txt");
+  }
+
+  @Test
+  public void outputGroupsDeclaredProvidersFromTwoAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target, ctx):",
+        "  f = ctx.actions.declare_file(target.label.name + '_a1.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return [OutputGroupInfo(a1_group = depset([f]))]",
+        "",
+        "a1 = aspect(implementation=_a1_impl, attr_aspects = ['dep'])",
+        "def _rule_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct()",
+        "  og = dict()",
+        "  dep_og = ctx.attr.dep[OutputGroupInfo]",
+        "  if hasattr(dep_og, 'a1_group'):",
+        "     og['a1_group'] = dep_og.a1_group",
+        "  if hasattr(dep_og, 'a2_group'):",
+        "     og['a2_group'] = dep_og.a2_group",
+        "  return [OutputGroupInfo(**og)]",
+        "my_rule1 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a1]) })",
+        "def _a2_impl(target, ctx):",
+        "  g = ctx.actions.declare_file(target.label.name + '_a2.txt')",
+        "  ctx.actions.write(g, 'f')",
+        "  return [OutputGroupInfo(a2_group = depset([g]))]",
+        "",
+        "a2 = aspect(implementation=_a2_impl, attr_aspects = ['dep'])",
+        "my_rule2 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a2]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule1', 'my_rule2')",
+        "my_rule1(name = 'base')",
+        "my_rule1(name = 'xxx', dep = ':base')",
+        "my_rule2(name = 'yyy', dep = ':xxx')");
+
+    AnalysisResult analysisResult = update("//test:yyy");
+    OutputGroupInfo outputGroupInfo =
+        OutputGroupInfo.get(Iterables.getOnlyElement(analysisResult.getTargetsToBuild()));
+    assertThat(getOutputGroupContents(outputGroupInfo, "a1_group"))
+        .containsExactly("test/base_a1.txt");
+    assertThat(getOutputGroupContents(outputGroupInfo, "a2_group"))
+        .containsExactly("test/xxx_a2.txt");
+  }
+
+  @Test
+  public void duplicateOutputGroupsFromTwoAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target, ctx):",
+        "  f = ctx.actions.declare_file(target.label.name + '_a1.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return struct(output_groups = { 'a1_group' : depset([f]) })",
+        "",
+        "a1 = aspect(implementation=_a1_impl, attr_aspects = ['dep'])",
+        "def _rule_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct()",
+        "  og = {k:ctx.attr.dep.output_groups[k] for k in ctx.attr.dep.output_groups}",
+        "  return struct(output_groups = og)",
+        "my_rule1 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a1]) })",
+        "def _a2_impl(target, ctx):",
+        "  g = ctx.actions.declare_file(target.label.name + '_a2.txt')",
+        "  ctx.actions.write(g, 'f')",
+        "  return struct(output_groups = { 'a1_group' : depset([g]) })",
+        "",
+        "a2 = aspect(implementation=_a2_impl, attr_aspects = ['dep'])",
+        "my_rule2 = rule(_rule_impl, attrs = { 'dep' : attr.label(aspects = [a2]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule1', 'my_rule2')",
+        "my_rule1(name = 'base')",
+        "my_rule1(name = 'xxx', dep = ':base')",
+        "my_rule2(name = 'yyy', dep = ':xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult analysisResult = update("//test:yyy");
+      assertThat(analysisResult.hasError()).isTrue();
+      assertThat(keepGoing()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expected.
+    }
+    assertContainsEvent("ERROR /workspace/test/BUILD:3:9: Output group a1_group provided twice");
+  }
+
+  private static Iterable<String> getOutputGroupContents(
+      OutputGroupInfo outputGroupInfo, String groupName) {
+    return Iterables.transform(
+        outputGroupInfo.getOutputGroup(groupName).toList(), Artifact::getRootRelativePathString);
+  }
+
+  @Test
+  public void duplicateStarlarkProviders() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "  return struct(duplicate = 'x')",
+        "",
+        "MyAspect = aspect(implementation=_impl)",
+        "def _rule_impl(ctx):",
+        "  return struct(duplicate = 'y')",
+        "my_rule = rule(_rule_impl)",
+        "def _noop(ctx):",
+        "  pass",
+        "rbase = rule(_noop, attrs = { 'dep' : attr.label(aspects = [MyAspect]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'my_rule', 'rbase')",
+        "my_rule(name = 'xxx')",
+        "rbase(name = 'yyy', dep = ':xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update("//test:yyy");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("ERROR /workspace/test/BUILD:3:6: Provider duplicate provided twice");
+  }
+
+  @Test
+  public void topLevelAspectDoesNotExist() throws Exception {
+    scratch.file("test/aspect.bzl", "");
+    scratch.file("test/BUILD", "java_library(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("MyAspect is not exported from //test:aspect.bzl");
+  }
+
+  @Test
+  public void topLevelAspectDoesNotExist2() throws Exception {
+    scratch.file("test/BUILD", "java_library(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("cannot load '//test:aspect.bzl': no such file");
+  }
+
+  @Test
+  public void topLevelAspectDoesNotExistNoBuildFile() throws Exception {
+    scratch.file("test/BUILD", "java_library(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.of("foo/aspect.bzl%MyAspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect to fail.
+    }
+    assertContainsEvent("Every .bzl file must have a corresponding package");
+  }
+
+  @Test
+  public void aspectParametersUncovered() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectUncovered = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['aaa']) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectUncovered]) },",
+        ")");
+    scratch.file("test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (Exception e) {
+      // expect to fail.
+    }
+    assertContainsEvent( // "ERROR /workspace/test/aspect.bzl:9:11: "
+        "Aspect //test:aspect.bzl%MyAspectUncovered requires rule my_rule to specify attribute "
+            + "'my_attr' with type string.");
+  }
+
+  @Test
+  public void aspectParametersTypeMismatch() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectMismatch = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['aaa']) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectMismatch]),",
+        "              'my_attr' : attr.int() },",
+        ")");
+    scratch.file(
+        "test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx', my_attr = 4)");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (Exception e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "Aspect //test:aspect.bzl%MyAspectMismatch requires rule my_rule to specify attribute "
+            + "'my_attr' with type string.");
+  }
+
+  @Test
+  public void aspectParametersDontSupportSelect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectMismatch = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['aaa']) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectMismatch]),",
+        "              'my_attr' : attr.string() },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "my_rule(name = 'xxx', my_attr = select({'//conditions:default': 'foo'}))");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (Exception e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "//test:xxx: attribute 'my_attr' has a select() and aspect "
+            + "//test:aspect.bzl%MyAspectMismatch also declares '//test:xxx'. Aspect attributes "
+            + "don't currently support select().");
+  }
+
+  @Test
+  public void aspectParametersBadDefault() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectBadDefault = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['a'], default='b') },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectBadDefault]) },",
+        ")");
+    scratch.file("test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (Exception e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/aspect.bzl:5:28: "
+            + "Aspect parameter attribute 'my_attr' has a bad default value: has to be one of 'a' "
+            + "instead of 'b'");
+  }
+
+  @Test
+  public void aspectParametersBadValue() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectBadValue = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['a']) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectBadValue]),",
+        "              'my_attr' : attr.string() },",
+        ")");
+    scratch.file(
+        "test/BUILD", //
+        "load('//test:aspect.bzl', 'my_rule')",
+        "my_rule(name = 'xxx', my_attr='b')");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(result.hasError()).isTrue();
+    } catch (Exception e) {
+      // expect to fail.
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:2:8: //test:xxx: invalid value in 'my_attr' "
+            + "attribute: has to be one of 'a' instead of 'b'");
+  }
+
+  @Test
+  public void aspectParameters() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspect = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['aaa']) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspect]),",
+        "              'my_attr' : attr.string() },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "my_rule(name = 'xxx', my_attr = 'aaa')");
+
+    AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+    assertThat(result.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectParametersConfigurationField() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspect = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { '_my_attr' : attr.label(default=",
+        "             configuration_field(fragment='cpp', name = 'cc_toolchain')) },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspect]) },",
+        ")");
+    scratch.file("test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx')");
+
+    AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+    assertThat(result.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectParameterComputedDefault() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "def _defattr():",
+        "   return Label('//foo/bar:baz')",
+        "MyAspect = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { '_extra' : attr.label(default = _defattr) }",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspect]) },",
+        ")");
+    scratch.file("test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx')");
+    reporter.removeHandler(failFastHandler);
+
+    if (keepGoing()) {
+      AnalysisResult result = update("//test:xxx");
+      assertThat(result.hasError()).isTrue();
+    } else {
+      assertThrows(TargetParsingException.class, () -> update("//test:xxx"));
+    }
+    assertContainsEvent(
+        "Aspect attribute '_extra' (label) with computed default value is unsupported.");
+  }
+
+  @Test
+  public void aspectParametersOptional() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectOptParam = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['aaa'], default='aaa') },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectOptParam]) },",
+        ")");
+    scratch.file("test/BUILD", "load('//test:aspect.bzl', 'my_rule')", "my_rule(name = 'xxx')");
+
+    AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+    assertThat(result.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectParametersOptionalOverride() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   if (ctx.attr.my_attr == 'a'):",
+        "       fail('Rule is not overriding default, still has value ' + ctx.attr.my_attr)",
+        "   return struct()",
+        "def _rule_impl(ctx):",
+        "   return struct()",
+        "MyAspectOptOverride = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { 'my_attr' : attr.string(values=['a', 'b'], default='a') },",
+        ")",
+        "my_rule = rule(",
+        "    implementation=_rule_impl,",
+        "    attrs = { 'deps' : attr.label_list(aspects=[MyAspectOptOverride]),",
+        "              'my_attr' : attr.string() },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "my_rule(name = 'xxx', my_attr = 'b')");
+
+    AnalysisResult result = update(ImmutableList.<String>of(), "//test:xxx");
+    assertThat(result.hasError()).isFalse();
+  }
+
+  @Test
+  public void testMultipleExecutablesInTarget() throws Exception {
+    scratch.file(
+        "foo/extension.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   return struct()",
+        "my_aspect = aspect(_aspect_impl)",
+        "def _main_rule_impl(ctx):",
+        "   pass",
+        "my_rule = rule(_main_rule_impl,",
+        "   attrs = { ",
+        "      'exe1' : attr.label(executable = True, allow_files = True, cfg = 'host'),",
+        "      'exe2' : attr.label(executable = True, allow_files = True, cfg = 'host'),",
+        "   },",
+        ")");
+
+    scratch.file("foo/tool.sh", "#!/bin/bash");
+    scratch.file(
+        "foo/BUILD",
+        "load(':extension.bzl',  'my_rule')",
+        "my_rule(name = 'main', exe1 = ':tool.sh', exe2 = ':tool.sh')");
+    AnalysisResult analysisResultOfRule = update(ImmutableList.<String>of(), "//foo:main");
+    assertThat(analysisResultOfRule.hasError()).isFalse();
+
+    AnalysisResult analysisResultOfAspect =
+        update(ImmutableList.<String>of("/foo/extension.bzl%my_aspect"), "//foo:main");
+    assertThat(analysisResultOfAspect.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectFragmentAccessSuccess() throws Exception {
+    getConfiguredTargetForAspectFragment(
+        "ctx.fragments.java.strict_java_deps", "'java'", "", "", "");
+    assertNoEvents();
+  }
+
+  @Test
+  public void aspectHostFragmentAccessSuccess() throws Exception {
+    getConfiguredTargetForAspectFragment(
+        "ctx.host_fragments.java.strict_java_deps", "", "'java'", "", "");
+    assertNoEvents();
+  }
+
+  @Test
+  public void aspectFragmentAccessError() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    assertThrows(
+        ViewCreationFailedException.class,
+        () ->
+            getConfiguredTargetForAspectFragment(
+                "ctx.fragments.java.strict_java_deps", "'cpp'", "'java'", "'java'", ""));
+    assertContainsEvent(
+        "//test:aspect.bzl%MyAspect aspect on my_rule has to declare 'java' as a "
+            + "required fragment in target configuration in order to access it. Please update the "
+            + "'fragments' argument of the rule definition "
+            + "(for example: fragments = [\"java\"])");
+  }
+
+  @Test
+  public void aspectHostFragmentAccessError() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    assertThrows(
+        ViewCreationFailedException.class,
+        () ->
+            getConfiguredTargetForAspectFragment(
+                "ctx.host_fragments.java.java_strict_deps", "'java'", "'cpp'", "", "'java'"));
+    assertContainsEvent(
+        "//test:aspect.bzl%MyAspect aspect on my_rule has to declare 'java' as a "
+            + "required fragment in host configuration in order to access it. Please update the "
+            + "'host_fragments' argument of the rule definition "
+            + "(for example: host_fragments = [\"java\"])");
+  }
+
+  private ConfiguredTarget getConfiguredTargetForAspectFragment(
+      String fullFieldName,
+      String fragments,
+      String hostFragments,
+      String ruleFragments,
+      String ruleHostFragments)
+      throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   return struct(result = str(" + fullFieldName + "))",
+        "",
+        "def _rule_impl(ctx):",
+        "   return struct(stuff = '...')",
+        "",
+        "MyAspect = aspect(",
+        "   implementation=_aspect_impl,",
+        "   attr_aspects=['deps'],",
+        "   fragments=[" + fragments + "],",
+        "   host_fragments=[" + hostFragments + "],",
+        ")",
+        "my_rule = rule(",
+        "   implementation=_rule_impl,",
+        "   attrs = { 'attr' : ",
+        "             attr.label_list(mandatory=True, allow_files=True, aspects = [MyAspect]) },",
+        "   fragments=[" + ruleFragments + "],",
+        "   host_fragments=[" + ruleHostFragments + "],",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "exports_files(['zzz'])",
+        "my_rule(",
+        "     name = 'yyy',",
+        "     attr = ['zzz'],",
+        ")",
+        "my_rule(",
+        "     name = 'xxx',",
+        "     attr = ['yyy'],",
+        ")");
+
+    AnalysisResult result = update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    if (result.hasError()) {
+      assertThat(keepGoing()).isTrue();
+      throw new ViewCreationFailedException("Analysis failed");
+    }
+
+    return getConfiguredTarget("//test:xxx");
+  }
+
+  @Test
+  public void invalidateAspectOnBzlFileChange() throws Exception {
+    scratch.file("test/build_defs.bzl", aspectBzlFile("'deps'"));
+    scratch.file(
+        "test/BUILD",
+        "load(':build_defs.bzl', 'repro', 'repro_no_aspect')",
+        "repro_no_aspect(name = 'r0')",
+        "repro_no_aspect(name = 'r1', deps = [':r0'])",
+        "repro(name = 'r2', deps = [':r1'])");
+    buildTargetAndCheckRuleInfo("//test:r0", "//test:r1");
+
+    // Make aspect propagation list empty.
+    scratch.overwriteFile("test/build_defs.bzl", aspectBzlFile(""));
+
+    // The aspect should not propagate to //test:r0 anymore.
+    buildTargetAndCheckRuleInfo("//test:r1");
+  }
+
+  private void buildTargetAndCheckRuleInfo(String... expectedLabels) throws Exception {
+    AnalysisResult result = update(ImmutableList.<String>of(), "//test:r2");
+    ConfiguredTarget configuredTarget = result.getTargetsToBuild().iterator().next();
+    Depset ruleInfoValue = (Depset) configuredTarget.get("rule_info");
+    assertThat(ruleInfoValue.getSet(String.class).toList())
+        .containsExactlyElementsIn(expectedLabels);
+  }
+
+  private String[] aspectBzlFile(String attrAspects) {
+    return new String[] {
+      "def _repro_aspect_impl(target, ctx):",
+      "    s = depset([str(target.label)], transitive =",
+      "      [d.aspect_info for d in ctx.rule.attr.deps if hasattr(d, 'aspect_info')])",
+      "    return struct(aspect_info = s)",
+      "",
+      "_repro_aspect = aspect(",
+      "    _repro_aspect_impl,",
+      "    attr_aspects = [" + attrAspects + "],",
+      ")",
+      "",
+      "def repro_impl(ctx):",
+      "    s = depset(transitive = ",
+      "      [d.aspect_info for d in ctx.attr.deps if hasattr(d, 'aspect_info')])",
+      "    return struct(rule_info = s)",
+      "",
+      "def repro_no_aspect_impl(ctx):",
+      "    pass",
+      "",
+      "repro_no_aspect = rule(implementation = repro_no_aspect_impl,",
+      "             attrs = {",
+      "                       'deps': attr.label_list(",
+      "                             allow_files = True,",
+      "                       )",
+      "                      },",
+      ")",
+      "",
+      "repro = rule(implementation = repro_impl,",
+      "             attrs = {",
+      "                       'deps': attr.label_list(",
+      "                             allow_files = True,",
+      "                             aspects = [_repro_aspect],",
+      "                       )",
+      "                      },",
+      ")"
+    };
+  }
+
+  @Test
+  public void aspectOutputsToBinDirectory() throws Exception {
+    scratch.file(
+        "foo/extension.bzl",
+        "def _aspect_impl(target, ctx):",
+        "   file = ctx.actions.declare_file('aspect-output-' + target.label.name)",
+        "   ctx.actions.write(file, 'data')",
+        "   return struct(aspect_file = file)",
+        "my_aspect = aspect(_aspect_impl)",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "rule_bin_out = rule(_rule_impl, output_to_genfiles=False)",
+        "rule_gen_out = rule(_rule_impl, output_to_genfiles=True)",
+        "def _main_rule_impl(ctx):",
+        "   s = depset([d.aspect_file for d in ctx.attr.deps])",
+        "   return struct(aspect_files = s)",
+        "main_rule = rule(_main_rule_impl,",
+        "   attrs = { 'deps' : attr.label_list(aspects = [my_aspect]) },",
+        ")");
+
+    scratch.file(
+        "foo/BUILD",
+        "load(':extension.bzl', 'rule_bin_out', 'rule_gen_out', 'main_rule')",
+        "rule_bin_out(name = 'rbin')",
+        "rule_gen_out(name = 'rgen')",
+        "main_rule(name = 'main', deps = [':rbin', ':rgen'])");
+    AnalysisResult analysisResult = update(ImmutableList.<String>of(), "//foo:main");
+    ConfiguredTarget target = analysisResult.getTargetsToBuild().iterator().next();
+    NestedSet<Artifact> aspectFiles = ((Depset) target.get("aspect_files")).getSet(Artifact.class);
+    assertThat(Iterables.transform(aspectFiles.toList(), Artifact::getFilename))
+        .containsExactly("aspect-output-rbin", "aspect-output-rgen");
+    for (Artifact aspectFile : aspectFiles.toList()) {
+      String rootPath = aspectFile.getRoot().getExecPath().toString();
+      assertWithMessage("Artifact %s should not be in genfiles", aspectFile)
+          .that(rootPath)
+          .doesNotContain("genfiles");
+      assertWithMessage("Artifact %s should be in bin", aspectFile).that(rootPath).endsWith("bin");
+    }
+  }
+
+  @Test
+  public void toplevelAspectOnFile() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   print('This aspect does nothing')",
+        "   return struct()",
+        "MyAspect = aspect(implementation=_impl)");
+    scratch.file("test/BUILD", "exports_files(['file.txt'])");
+    scratch.file("test/file.txt", "");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:file.txt");
+    assertThat(analysisResult.hasError()).isFalse();
+    assertThat(
+            Iterables.getOnlyElement(analysisResult.getAspectsMap().values())
+                .getProviders()
+                .getProviderCount())
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void sharedAttributeDefinitionWithAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target,ctx):",
+        "  return struct()",
+        "my_aspect = aspect(implementation = _aspect_impl)",
+        "_ATTR = { 'deps' : attr.label_list(aspects = [my_aspect]) }",
+        "def _dummy_impl(ctx):",
+        "  pass",
+        "r1 = rule(_dummy_impl, attrs =  _ATTR)",
+        "r2 = rule(_dummy_impl, attrs =  _ATTR)");
+
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 't1')",
+        "r2(name = 't2', deps = [':t1'])");
+    AnalysisResult analysisResult = update("//test:t2");
+    assertThat(analysisResult.hasError()).isFalse();
+  }
+
+  @Test
+  public void multipleAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target,ctx):",
+        "  return struct()",
+        "my_aspect = aspect(implementation = _aspect_impl)",
+        "def _dummy_impl(ctx):",
+        "  pass",
+        "r1 = rule(_dummy_impl, ",
+        "          attrs = { 'deps' : attr.label_list(aspects = [my_aspect, my_aspect]) })");
+
+    scratch.file("test/BUILD", "load(':aspect.bzl', 'r1')", "r1(name = 't1')");
+    reporter.removeHandler(failFastHandler);
+    // This call succeeds if "--keep_going" was passed, which it does in the WithKeepGoing test
+    // suite. Otherwise, it fails and throws a TargetParsingException.
+    if (keepGoing()) {
+      AnalysisResult result = update("//test:r1");
+      assertThat(result.hasError()).isTrue();
+    } else {
+      assertThrows(TargetParsingException.class, () -> update("//test:r1"));
+    }
+    assertContainsEvent("aspect //test:aspect.bzl%my_aspect added more than once");
+  }
+
+  @Test
+  public void topLevelAspectsAndExtraActions() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target,ctx):",
+        "  f = ctx.actions.declare_file('dummy.txt')",
+        "  ctx.actions.run_shell(outputs = [f], command='echo xxx > $(location f)',",
+        "                        mnemonic='AspectAction')",
+        "  return struct()",
+        "my_aspect = aspect(implementation = _aspect_impl)");
+    scratch.file(
+        "test/BUILD",
+        "extra_action(",
+        "    name = 'xa',",
+        "    cmd = 'echo $(EXTRA_ACTION_FILE) > $(output file.xa)',",
+        "    out_templates = ['file.xa'],",
+        ")",
+        "action_listener(",
+        "    name = 'al',",
+        "    mnemonics = [ 'AspectAction' ],",
+        "    extra_actions = [ ':xa' ])",
+        "java_library(name = 'xxx')");
+    useConfiguration("--experimental_action_listener=//test:al");
+    AnalysisResult analysisResult =
+        update(ImmutableList.<String>of("test/aspect.bzl%my_aspect"), "//test:xxx");
+    assertThat(
+            Iterables.transform(
+                analysisResult.getTopLevelArtifactsToOwnerLabels().getArtifacts(),
+                Artifact::getFilename))
+        .contains("file.xa");
+  }
+
+  /** Regression test for b/137960630. */
+  @Test
+  public void topLevelAspectsAndExtraActionsWithConflict() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _aspect_impl(target, ctx):",
+        "  f = ctx.actions.declare_file('dummy.txt')",
+        "  ctx.actions.run_shell(outputs = [f], command='echo xxx > $(location f)',",
+        "                        mnemonic='AspectAction')",
+        "  return struct()",
+        "my_aspect = aspect(implementation = _aspect_impl)");
+    scratch.file(
+        "test/BUILD",
+        "extra_action(",
+        "    name = 'xa',",
+        "    cmd = 'echo $(EXTRA_ACTION_FILE) > $(output file.xa)',",
+        "    out_templates = ['file.xa'],",
+        ")",
+        "action_listener(",
+        "    name = 'al',",
+        "    mnemonics = ['AspectAction'],",
+        "    extra_actions = [':xa'],",
+        ")",
+        "java_library(name = 'xxx')",
+        "java_library(name = 'yyy')");
+    useConfiguration("--experimental_action_listener=//test:al");
+    reporter.removeHandler(failFastHandler); // We expect an error.
+
+    if (keepGoing()) {
+      AnalysisResult result =
+          update(ImmutableList.of("test/aspect.bzl%my_aspect"), "//test:xxx", "//test:yyy");
+      assertThat(result.hasError()).isTrue();
+    } else {
+      assertThrows(
+          ViewCreationFailedException.class,
+          () -> update(ImmutableList.of("test/aspect.bzl%my_aspect"), "//test:xxx", "//test:yyy"));
+    }
+    assertContainsEvent(
+        "file 'extra_actions/test/xa/test/file.xa' is generated by these conflicting actions");
+  }
+
+  @Test
+  public void aspectsPropagatingToAllAttributes() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   s = depset([target.label], transitive =",
+        "     [i.target_labels for i in ctx.rule.attr.runtime_deps]",
+        "     if hasattr(ctx.rule.attr, 'runtime_deps') else [])",
+        "   return struct(target_labels = s)",
+        "",
+        "MyAspect = aspect(",
+        "    implementation=_impl,",
+        "    attrs = { '_tool' : attr.label(default = Label('//test:tool')) },",
+        "    attr_aspects=['*'],",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "java_library(",
+        "    name = 'tool',",
+        ")",
+        "java_library(",
+        "     name = 'bar',",
+        "     runtime_deps = [':tool'],",
+        ")",
+        "java_library(",
+        "     name = 'foo',",
+        "     runtime_deps = [':bar'],",
+        ")");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:foo");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    assertThat(configuredAspect).isNotNull();
+    Object names = configuredAspect.get("target_labels");
+    assertThat(names).isInstanceOf(Depset.class);
+    assertThat(
+            Iterables.transform(
+                ((Depset) names).toList(),
+                o -> {
+                  assertThat(o).isInstanceOf(Label.class);
+                  return ((Label) o).getName();
+                }))
+        .containsExactly("foo", "bar", "tool");
+  }
+
+  /** Simple straightforward linear aspects-on-aspects. */
+  @Test
+  public void aspectOnAspectLinear() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = a1p(text = 'random'))",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "  value = []",
+        "  if hasattr(ctx.rule.attr.dep, 'a2p'):",
+        "     value += ctx.rule.attr.dep.a2p.value",
+        "  if hasattr(target, 'a1p'):",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=yes')",
+        "  else:",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=no')",
+        "  return struct(a2p = a2p(value = value))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 'r0')",
+        "r1(name = 'r1', dep = ':r0')",
+        "r2(name = 'r2', dep = ':r1')");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence) target.get("result");
+
+    // "yes" means that aspect a2 sees a1's providers.
+    assertThat(result)
+        .containsExactly(
+            "//test:r0[\"//test:aspect.bzl%a1\", \"//test:aspect.bzl%a2\"]=yes",
+            "//test:r1[\"//test:aspect.bzl%a2\"]=no");
+  }
+
+  /**
+   * Diamond case. rule r1 depends or r0 with aspect a1. rule r2 depends or r0 with aspect a2. rule
+   * rcollect depends on r1, r2 with aspect a3.
+   *
+   * <p>Aspect a3 should be applied twice to target r0: once in [a1, a3] sequence and once in [a2,
+   * a3] sequence.
+   */
+  @Test
+  public void aspectOnAspectDiamond() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = 'text from a1')",
+        "a1 = aspect(_a1_impl, attr_aspects = ['deps'], provides = ['a1p'])",
+        "",
+        "def _a2_impl(target,ctx):",
+        "  return struct(a2p = 'text from a2')",
+        "a2 = aspect(_a2_impl, attr_aspects = ['deps'], provides = ['a2p'])",
+        "",
+        "def _a3_impl(target,ctx):",
+        "  value = []",
+        "  f = ctx.actions.declare_file('a3.out')",
+        "  ctx.actions.write(f, 'text')",
+        "  for dep in ctx.rule.attr.deps:",
+        "     if hasattr(dep, 'a3p'):",
+        "         value += dep.a3p",
+        "  s = str(target.label) + str(ctx.aspect_ids) + '='",
+        "  if hasattr(target, 'a1p'):",
+        "     s += 'a1p'",
+        "  if hasattr(target, 'a2p'):",
+        "     s += 'a2p'",
+        "  value.append(s)",
+        "  return struct(a3p = value)",
+        "a3 = aspect(_a3_impl, attr_aspects = ['deps'],",
+        "            required_aspect_providers = [['a1p'], ['a2p']])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _rcollect_impl(ctx):",
+        "  value = []",
+        "  for dep in ctx.attr.deps:",
+        "     if hasattr(dep, 'a3p'):",
+        "         value += dep.a3p",
+        "  return struct(result = value)",
+        "r1 = rule(_r1_impl, attrs = { 'deps' : attr.label_list(aspects = [a1])})",
+        "r2 = rule(_r1_impl, attrs = { 'deps' : attr.label_list(aspects = [a2])})",
+        "rcollect = rule(_rcollect_impl, attrs = { 'deps' : attr.label_list(aspects = [a3])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2', 'rcollect')",
+        "r1(name = 'r0')",
+        "r1(name = 'r1', deps = [':r0'])",
+        "r2(name = 'r2', deps = [':r0'])",
+        "rcollect(name = 'rcollect', deps = [':r1', ':r2'])");
+    AnalysisResult analysisResult = update("//test:rcollect");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence) target.get("result");
+    assertThat(result)
+        .containsExactly(
+            "//test:r0[\"//test:aspect.bzl%a1\", \"//test:aspect.bzl%a3\"]=a1p",
+            "//test:r1[\"//test:aspect.bzl%a3\"]=",
+            "//test:r0[\"//test:aspect.bzl%a2\", \"//test:aspect.bzl%a3\"]=a2p",
+            "//test:r2[\"//test:aspect.bzl%a3\"]=");
+  }
+
+  /**
+   * Linear with duplicates. r2_1 depends on r0 with aspect a2. r1 depends on r2_1 with aspect a1.
+   * r2 depends on r1 with aspect a2.
+   *
+   * <p>a2 is not interested in a1. There should be just one instance of aspect a2 on r0, and is
+   * should *not* see a1.
+   */
+  @Test
+  public void aspectOnAspectLinearDuplicates() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = 'a1p')",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "  value = []",
+        "  if hasattr(ctx.rule.attr.dep, 'a2p'):",
+        "     value += ctx.rule.attr.dep.a2p.value",
+        "  if hasattr(target, 'a1p'):",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=yes')",
+        "  else:",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=no')",
+        "  return struct(a2p = a2p(value = value))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = [])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 'r0')",
+        "r2(name = 'r2_1', dep = ':r0')",
+        "r1(name = 'r1', dep = ':r2_1')",
+        "r2(name = 'r2', dep = ':r1')");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence) target.get("result");
+    // "yes" means that aspect a2 sees a1's providers.
+    assertThat(result)
+        .containsExactly(
+            "//test:r0[\"//test:aspect.bzl%a2\"]=no",
+            "//test:r1[\"//test:aspect.bzl%a2\"]=no", "//test:r2_1[\"//test:aspect.bzl%a2\"]=no");
+  }
+
+  /** Linear aspects-on-aspects with alias rule. */
+  @Test
+  public void aspectOnAspectLinearAlias() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = a1p(text = 'random'))",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "  value = []",
+        "  if hasattr(ctx.rule.attr.dep, 'a2p'):",
+        "     value += ctx.rule.attr.dep.a2p.value",
+        "  if hasattr(target, 'a1p'):",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=yes')",
+        "  else:",
+        "     value.append(str(target.label) + str(ctx.aspect_ids) + '=no')",
+        "  return struct(a2p = a2p(value = value))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 'r0')",
+        "alias(name = 'a0', actual = ':r0')",
+        "r1(name = 'r1', dep = ':a0')",
+        "r2(name = 'r2', dep = ':r1')");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence<?>) target.get("result");
+
+    // "yes" means that aspect a2 sees a1's providers.
+    assertThat(result)
+        .containsExactly(
+            "//test:r0[\"//test:aspect.bzl%a1\", \"//test:aspect.bzl%a2\"]=yes",
+            "//test:r1[\"//test:aspect.bzl%a2\"]=no");
+  }
+
+  @Test
+  public void aspectDescriptions() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _a_impl(target,ctx):",
+        "  s = str(target.label) + str(ctx.aspect_ids) + '='",
+        "  value = []",
+        "  if ctx.rule.attr.dep:",
+        "     d = ctx.rule.attr.dep",
+        "     this_id = ctx.aspect_ids[len(ctx.aspect_ids) - 1]",
+        "     s += str(d.label) + str(d.my_ids) + ',' + str(this_id in d.my_ids)",
+        "     value += ctx.rule.attr.dep.ap",
+        "  else:",
+        "     s += 'None'",
+        "  value.append(s)",
+        "  return struct(ap = value, my_ids = ctx.aspect_ids)",
+        "a = aspect(_a_impl, attr_aspects = ['dep'])",
+        "def _r_impl(ctx):",
+        "  if not ctx.attr.dep:",
+        "     return struct(result = [])",
+        "  return struct(result = ctx.attr.dep.ap)",
+        "r = rule(_r_impl, attrs = { 'dep' : attr.label(aspects = [a])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r')",
+        "r(name = 'r0')",
+        "r(name = 'r1', dep = ':r0')",
+        "r(name = 'r2', dep = ':r1')");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence<?>) target.get("result");
+
+    assertThat(result)
+        .containsExactly(
+            "//test:r0[\"//test:aspect.bzl%a\"]=None",
+            "//test:r1[\"//test:aspect.bzl%a\"]=//test:r0[\"//test:aspect.bzl%a\"],True");
+  }
+
+  @Test
+  public void attributesWithAspectsReused() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "my_aspect = aspect(_impl)",
+        "a_dict = { 'foo' : attr.label_list(aspects = [my_aspect]) }");
+
+    scratch.file(
+        "test/r1.bzl",
+        "load(':aspect.bzl', 'my_aspect', 'a_dict')",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "r1 = rule(_rule_impl, attrs = a_dict)");
+
+    scratch.file(
+        "test/r2.bzl",
+        "load(':aspect.bzl', 'my_aspect', 'a_dict')",
+        "def _rule_impl(ctx):",
+        "   pass",
+        "r2 = rule(_rule_impl, attrs = a_dict)");
+
+    scratch.file(
+        "test/BUILD",
+        "load(':r1.bzl', 'r1')",
+        "load(':r2.bzl', 'r2')",
+        "r1(name = 'x1')",
+        "r2(name = 'x2', foo = [':x1'])");
+    AnalysisResult analysisResult = update("//test:x2");
+    assertThat(analysisResult.hasError()).isFalse();
+  }
+
+  @Test
+  public void aspectAdvertisingProviders() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "   return struct()",
+        "my_aspect = aspect(_impl, provides = ['foo'])",
+        "a_dict = { 'foo' : attr.label_list(aspects = [my_aspect]) }");
+    scratch.file("test/BUILD", "java_library(name = 'xxx',)");
+
+    reporter.removeHandler(failFastHandler);
+    try {
+      AnalysisResult analysisResult =
+          update(ImmutableList.of("//test:aspect.bzl%my_aspect"), "//test:xxx");
+      assertThat(keepGoing()).isTrue();
+      assertThat(analysisResult.hasError()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expect exception
+    }
+    assertContainsEvent(
+        "Aspect '//test:aspect.bzl%my_aspect', applied to '//test:xxx', "
+            + "does not provide advertised provider 'foo'");
+  }
+
+  @Test
+  public void aspectOnAspectInconsistentVisibility() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = a1p(text = 'random'))",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "  return struct(a2p = a2p(value = 'random'))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 'r0')",
+        "r1(name = 'r1', dep = ':r0')",
+        "r2(name = 'r2', dep = ':r1')",
+        "r1(name = 'r1_1', dep = ':r2')",
+        "r2(name = 'r2_1', dep = ':r1_1')");
+    reporter.removeHandler(failFastHandler);
+
+    try {
+      AnalysisResult analysisResult = update("//test:r2_1");
+      assertThat(analysisResult.hasError()).isTrue();
+      assertThat(keepGoing()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expected
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:3:3: Aspect //test:aspect.bzl%a2 is"
+            + " applied twice, both before and after aspect //test:aspect.bzl%a1 "
+            + "(when propagating to //test:r1)");
+  }
+
+  @Test
+  public void aspectOnAspectInconsistentVisibilityIndirect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = a1p(text = 'random'))",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "  return struct(a2p = a2p(value = 'random'))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})",
+        "def _r0_impl(ctx):",
+        "  pass",
+        "r0 = rule(_r0_impl, attrs = { 'dep' : attr.label()})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r1', 'r2')",
+        "r0(name = 'r0')",
+        "r1(name = 'r1', dep = ':r0')",
+        "r2(name = 'r2', dep = ':r1')",
+        "r1(name = 'r1_1', dep = ':r2')",
+        "r2(name = 'r2_1', dep = ':r1_1')",
+        "r0(name = 'r0_2', dep = ':r2_1')");
+    reporter.removeHandler(failFastHandler);
+
+    try {
+      AnalysisResult analysisResult = update("//test:r0_2");
+      assertThat(analysisResult.hasError()).isTrue();
+      assertThat(keepGoing()).isTrue();
+    } catch (ViewCreationFailedException e) {
+      // expected
+    }
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:3:3: Aspect //test:aspect.bzl%a2 is"
+            + " applied twice, both before and after aspect //test:aspect.bzl%a1 "
+            + "(when propagating to //test:r1)");
+  }
+
+  /**
+   * Aspect a3 sees aspect a2, aspect a2 sees aspect a1, but a3 does not see a1. All three aspects
+   * should still propagate together.
+   */
+  @Test
+  public void aspectOnAspectOnAspect() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "p1 = provider()",
+        "def _a1_impl(target, ctx):",
+        "   return [p1()]",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = [p1])",
+        "p2 = provider()",
+        "def _a2_impl(target, ctx):",
+        "   value = True if p1 in target else False",
+        "   return [p2(has_p1 = value)]",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'],",
+        "   required_aspect_providers = [p1], provides = [p2])",
+        "p3 = provider()",
+        "def _a3_impl(target, ctx):",
+        "   list = []",
+        "   if ctx.rule.attr.dep:",
+        "     list = ctx.rule.attr.dep[p3].value",
+        "   my_value = str(target.label) +'=' + str(target[p2].has_p1 if p2 in target else False)",
+        "   return [p3(value = list + [my_value])]",
+        "a3 = aspect(_a3_impl, attr_aspects = ['dep'],",
+        "   required_aspect_providers = [p2])",
+        "def _r0_impl(ctx):",
+        "  pass",
+        "r0 = rule(_r0_impl, attrs = { 'dep' : attr.label()})",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  pass",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r1', 'r2')",
+        "r0(name = 'r0_1')",
+        "r0(name = 'r0_2', dep = ':r0_1')",
+        "r0(name = 'r0_3', dep = ':r0_2')",
+        "r1(name = 'r1_1', dep = ':r0_3')",
+        "r2(name = 'r2_1', dep = ':r1_1')");
+
+    AnalysisResult analysisResult = update(ImmutableList.of("//test:aspect.bzl%a3"), "//test:r2_1");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+    StarlarkProvider.Key p3 =
+        new StarlarkProvider.Key(Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "p3");
+    StructImpl p3Provider = (StructImpl) configuredAspect.get(p3);
+    assertThat((Sequence<?>) p3Provider.getValue("value"))
+        .containsExactly(
+            "//test:r0_1=True",
+            "//test:r0_2=True",
+            "//test:r0_3=True",
+            "//test:r1_1=False",
+            "//test:r2_1=False");
+  }
+
+  /**
+   * r0 is a dependency of r1 via two attributes, dep1 and dep2. r1 sends an aspect 'a' along dep1
+   * but not along dep2.
+   *
+   * <p>rcollect depends upon r1 and sends another aspect, 'collector', along its dep dependency.
+   * 'collector' wants to see aspect 'a' and propagates along dep1 and dep2. It should be applied
+   * both to r0 and to r0+a.
+   */
+  @Test
+  public void multipleDepsDifferentAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "PAspect = provider()",
+        "PCollector = provider()",
+        "def _aspect_impl(target, ctx):",
+        "   return [PAspect()]",
+        "a = aspect(_aspect_impl, attr_aspects = ['dep'], provides = [PAspect])",
+        "def _collector_impl(target, ctx):",
+        "   suffix = '+PAspect' if PAspect in target else ''",
+        "   result = [str(target.label)+suffix]",
+        "   for a in ['dep', 'dep1', 'dep2']:",
+        "     if hasattr(ctx.rule.attr, a):",
+        "        result += getattr(ctx.rule.attr, a)[PCollector].result",
+        "   return [PCollector(result=result)]",
+        "collector = aspect(_collector_impl, attr_aspects = ['*'], ",
+        "                   required_aspect_providers = [PAspect])",
+        "def _rimpl(ctx):",
+        "   pass",
+        "r0 = rule(_rimpl)",
+        "r1 = rule(_rimpl, ",
+        "          attrs = {",
+        "             'dep1' : attr.label(),",
+        "             'dep2' : attr.label(aspects = [a]),",
+        "          },",
+        ")",
+        "def _rcollect_impl(ctx):",
+        "    return [ctx.attr.dep[PCollector]]",
+        "rcollect = rule(_rcollect_impl,",
+        "                attrs = {",
+        "                  'dep' : attr.label(aspects = [collector]),",
+        "                })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r1', 'rcollect')",
+        "r0(name = 'r0')",
+        "r1(name = 'r1', dep1 = ':r0', dep2 = ':r0')",
+        "rcollect(name = 'rcollect', dep = ':r1')");
+
+    AnalysisResult analysisResult = update(ImmutableList.of(), "//test:rcollect");
+    ConfiguredTarget configuredTarget =
+        Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    StarlarkProvider.Key pCollector =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "PCollector");
+    StructImpl pCollectorProvider = (StructImpl) configuredTarget.get(pCollector);
+    assertThat((Sequence<?>) pCollectorProvider.getValue("result"))
+        .containsExactly("//test:r1", "//test:r0", "//test:r0+PAspect");
+  }
+
+  @Test
+  public void aspectSeesOtherAspectAttributes() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "PAspect = provider(fields = [])",
+        "PCollector = provider(fields = ['aspect_attr'])",
+        "def _a_impl(target, ctx):",
+        "  return [PAspect()]",
+        "a = aspect(_a_impl, ",
+        "           provides = [PAspect],",
+        "           attrs = {'_a_attr' : attr.label(default = '//test:foo')})",
+        "def _rcollect(target, ctx):",
+        "  if hasattr(ctx.rule.attr, '_a_attr'):",
+        "     return [PCollector(aspect_attr = ctx.rule.attr._a_attr.label)]",
+        "  if hasattr(ctx.rule.attr, 'dep'):",
+        "     return [ctx.rule.attr.dep[PCollector]]",
+        "  return [PCollector()]",
+        "acollect = aspect(_rcollect, attr_aspects = ['*'], required_aspect_providers = [PAspect])",
+        "def _rimpl(ctx):",
+        "  pass",
+        "r0 = rule(_rimpl)",
+        "r = rule(_rimpl, attrs = { 'dep' : attr.label(aspects = [a]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r')",
+        "r0(name = 'foo')",
+        "r0(name = 'bar')",
+        "r(name = 'baz', dep = ':bar')");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("//test:aspect.bzl%acollect"), "//test:baz");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+    StarlarkProvider.Key pCollector =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "PCollector");
+    StructImpl collector = (StructImpl) configuredAspect.get(pCollector);
+    assertThat(collector.getValue("aspect_attr"))
+        .isEqualTo(Label.parseAbsolute("//test:foo", ImmutableMap.of()));
+  }
+
+  @Test
+  public void ruleAttributesWinOverAspects() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "PAspect = provider(fields = [])",
+        "PCollector = provider(fields = ['attr_value'])",
+        "def _a_impl(target, ctx):",
+        "  return [PAspect()]",
+        "a = aspect(_a_impl, ",
+        "           provides = [PAspect],",
+        "           attrs = {'_same_attr' : attr.int(default = 239)})",
+        "def _rcollect(target, ctx):",
+        "  if hasattr(ctx.rule.attr, '_same_attr'):",
+        "     return [PCollector(attr_value = ctx.rule.attr._same_attr)]",
+        "  if hasattr(ctx.rule.attr, 'dep'):",
+        "     return [ctx.rule.attr.dep[PCollector]]",
+        "  return [PCollector()]",
+        "acollect = aspect(_rcollect, attr_aspects = ['*'], required_aspect_providers = [PAspect])",
+        "def _rimpl(ctx):",
+        "  pass",
+        "r0 = rule(_rimpl)",
+        "r = rule(_rimpl, ",
+        "          attrs = { ",
+        "                  'dep' : attr.label(aspects = [a]), ",
+        "                  '_same_attr' : attr.int(default = 30)",
+        "          })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r')",
+        "r0(name = 'foo')",
+        "r0(name = 'bar')",
+        "r(name = 'baz', dep = ':bar')");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("//test:aspect.bzl%acollect"), "//test:baz");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+    StarlarkProvider.Key pCollector =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "PCollector");
+    StructImpl collector = (StructImpl) configuredAspect.get(pCollector);
+    assertThat(collector.getValue("attr_value")).isEqualTo(30);
+  }
+
+  @Test
+  public void earlyAspectAttributesWin() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "PAspect1 = provider(fields = [])",
+        "PAspect2 = provider(fields = [])",
+        "PCollector = provider(fields = ['attr_value'])",
+        "def _a1_impl(target, ctx):",
+        "  return [PAspect1()]",
+        "def _a2_impl(target, ctx):",
+        "  return [PAspect2()]",
+        "a1 = aspect(_a1_impl, ",
+        "            provides = [PAspect1],",
+        "            attrs = {'_same_attr' : attr.int(default = 30)})",
+        "a2 = aspect(_a2_impl, ",
+        "            provides = [PAspect2],",
+        "            attrs = {'_same_attr' : attr.int(default = 239)})",
+        "def _rcollect(target, ctx):",
+        "  if hasattr(ctx.rule.attr, 'dep'):",
+        "     return [ctx.rule.attr.dep[PCollector]]",
+        "  if hasattr(ctx.rule.attr, '_same_attr'):",
+        "     return [PCollector(attr_value = ctx.rule.attr._same_attr)]",
+        "  fail('???')",
+        "  return [PCollector()]",
+        "acollect = aspect(_rcollect, attr_aspects = ['*'], ",
+        "                  required_aspect_providers = [[PAspect1], [PAspect2]])",
+        "def _rimpl(ctx):",
+        "  pass",
+        "r0 = rule(_rimpl)",
+        "r1 = rule(_rimpl, ",
+        "          attrs = { ",
+        "                  'dep' : attr.label(aspects = [a1]), ",
+        "          })",
+        "r2 = rule(_rimpl, ",
+        "          attrs = { ",
+        "                  'dep' : attr.label(aspects = [a2]), ",
+        "          })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r1', 'r2')",
+        "r0(name = 'bar')",
+        "r1(name = 'baz', dep = ':bar')",
+        "r2(name = 'quux', dep = ':baz')");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("//test:aspect.bzl%acollect"), "//test:quux");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+    StarlarkProvider.Key pCollector =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "PCollector");
+    StructImpl collector = (StructImpl) configuredAspect.get(pCollector);
+    assertThat(collector.getValue("attr_value")).isEqualTo(30);
+  }
+
+  @Test
+  public void aspectPropagatesOverOtherAspectAttributes() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "PAspect = provider(fields = [])",
+        "PCollector = provider(fields = ['visited'])",
+        "def _a_impl(target, ctx):",
+        "  return [PAspect()]",
+        "a = aspect(_a_impl, ",
+        "       provides = [PAspect],",
+        "       attrs = {'_a_attr' : attr.label(default = '//test:referenced_from_aspect_only')})",
+        "def _rcollect(target, ctx):",
+        "  transitive = []",
+        "  if hasattr(ctx.rule.attr, 'dep') and ctx.rule.attr.dep:",
+        "     transitive += [ctx.rule.attr.dep[PCollector].visited]",
+        "  if hasattr(ctx.rule.attr, '_a_attr') and ctx.rule.attr._a_attr:",
+        "     transitive += [ctx.rule.attr._a_attr[PCollector].visited] ",
+        "  visited = depset([target.label], transitive = transitive, )",
+        "  return [PCollector(visited = visited)]",
+        "acollect = aspect(_rcollect, attr_aspects = ['*'], required_aspect_providers = [PAspect])",
+        "def _rimpl(ctx):",
+        "  pass",
+        "r0 = rule(_rimpl)",
+        "r = rule(_rimpl, attrs = { 'dep' : attr.label(aspects = [a]) })");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r')",
+        "r0(name = 'referenced_from_aspect_only')",
+        "r0(name = 'bar')",
+        "r(name = 'baz', dep = ':bar')");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("//test:aspect.bzl%acollect"), "//test:baz");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+    StarlarkProvider.Key pCollector =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "PCollector");
+    StructImpl collector = (StructImpl) configuredAspect.get(pCollector);
+    assertThat(((Depset) collector.getValue("visited")).toList())
+        .containsExactly(
+            Label.parseAbsolute("//test:referenced_from_aspect_only", ImmutableMap.of()),
+            Label.parseAbsolute("//test:bar", ImmutableMap.of()),
+            Label.parseAbsolute("//test:baz", ImmutableMap.of()));
+  }
+
+  @Test
+  // This test verifies that aspects which are defined natively and exported for use in Starlark
+  // can be referenced at the top level using the --aspects flag. For ease of testing,
+  // apple_common.objc_proto_aspect is used as an example.
+  public void testTopLevelStarlarkObjcProtoAspect() throws Exception {
+    MockObjcSupport.setupObjcProtoLibrary(scratch);
+    scratch.file("test_starlark/BUILD");
+    scratch.file("x/data_filter.pbascii");
+    scratch.file(
+        "test_starlark/top_level_stub.bzl",
+        "top_level_aspect = apple_common.objc_proto_aspect",
+        "",
+        "def top_level_stub_impl(ctx):",
+        "  return struct()",
+        "top_level_stub = rule(",
+        "    top_level_stub_impl,",
+        "    attrs = {",
+        "        'deps': attr.label_list(),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file(
+        "x/BUILD",
+        "load('//objc_proto_library:objc_proto_library.bzl', 'objc_proto_library')",
+        "proto_library(",
+        "  name = 'protos',",
+        "  srcs = ['data.proto'],",
+        MockProtoSupport.MIGRATION_TAG,
+        ")",
+        "objc_proto_library(",
+        "  name = 'x',",
+        "  deps = [':protos'],",
+        "  portable_proto_filters = ['data_filter.pbascii'],",
+        ")");
+
+    scratch.file(
+        "bin/BUILD",
+        "load('//test_starlark:top_level_stub.bzl', 'top_level_stub')",
+        "top_level_stub(",
+        "  name = 'link_target',",
+        "  deps = ['//x:x'],",
+        ")");
+
+    useConfiguration(MockObjcSupport.requiredObjcCrosstoolFlags().toArray(new String[1]));
+    AnalysisResult analysisResult =
+        update(
+            ImmutableList.of("test_starlark/top_level_stub.bzl%top_level_aspect"),
+            "//bin:link_target");
+    ConfiguredAspect configuredAspect =
+        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
+
+    ObjcProtoProvider objcProtoProvider =
+        (ObjcProtoProvider) configuredAspect.get(ObjcProtoProvider.STARLARK_CONSTRUCTOR.getKey());
+    assertThat(objcProtoProvider).isNotNull();
+  }
+
+  @Test
+  public void testAspectActionProvider() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  ctx.actions.run_shell(",
+        "    outputs = [ctx.actions.declare_file('a1')],",
+        "    command = 'touch $@'",
+        "  )",
+        "  return struct(a1p=a1p())",
+        "a1 = aspect(_a1_impl, attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target, ctx):",
+        "  value = []",
+        "  if hasattr(ctx.rule.attr, 'dep') and hasattr(ctx.rule.attr.dep, 'a2p'):",
+        "     value += ctx.rule.attr.dep.a2p.value",
+        "  value += target.actions",
+        "  return struct(a2p = a2p(value = value))",
+        "a2 = aspect(_a2_impl, attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r0_impl(ctx):",
+        "  ctx.actions.run_shell(",
+        "    outputs = [ctx.actions.declare_file('r0')],",
+        "    command = 'touch $@'",
+        "  )",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep.a2p.value)",
+        "r0 = rule(_r0_impl)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r0', 'r1', 'r2')",
+        "r0(name = 'r0')",
+        "r1(name = 'r1', dep = ':r0')",
+        "r2(name = 'r2', dep = ':r1')");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    Sequence<?> result = (Sequence<?>) target.get("result");
+
+    // We should see both the action from the 'r0' rule, and the action from the 'a1' aspect
+    assertThat(result).hasSize(2);
+    assertThat(
+            result.stream()
+                .map(a -> ((Action) a).getPrimaryOutput().getExecPath().getBaseName())
+                .collect(toList()))
+        .containsExactly("r0", "a1");
+  }
+
+  @Test
+  public void testRuleAndAspectAttrConflict() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "MyInfo = provider()",
+        "def _impl(target, ctx):",
+        "   return [MyInfo(hidden_attr_label = str(ctx.attr._hiddenattr.label))]",
+        "",
+        "def _rule_impl(ctx):",
+        "   return []",
+        "",
+        "my_rule = rule(implementation = _rule_impl,",
+        "   attrs = { '_hiddenattr' : attr.label(default = Label('//test:xxx')) },",
+        ")",
+        "MyAspect = aspect(",
+        "   implementation=_impl,",
+        "   attrs = { '_hiddenattr' : attr.label(default = Label('//test:zzz')) },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:aspect.bzl', 'my_rule')",
+        "cc_library(",
+        "     name = 'xxx',",
+        ")",
+        "my_rule(",
+        "     name = 'yyy',",
+        ")",
+        "cc_library(",
+        "     name = 'zzz',",
+        ")");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:yyy");
+    ConfiguredAspect configuredAspect = analysisResult.getAspectsMap().values().iterator().next();
+    assertThat(configuredAspect).isNotNull();
+
+    StarlarkProvider.Key myInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:aspect.bzl", ImmutableMap.of()), "MyInfo");
+    StructImpl myInfo = (StructImpl) configuredAspect.get(myInfoKey);
+    assertThat(myInfo.getValue("hidden_attr_label")).isEqualTo("//test:zzz");
+  }
+
+  /** Simple straightforward linear aspects-on-aspects. */
+  @Test
+  public void aspectOnAspectAttrConflict() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "MyInfo = provider()",
+        "a1p = provider()",
+        "def _a1_impl(target,ctx):",
+        "  return struct(a1p = a1p(text = 'random'))",
+        "a1 = aspect(_a1_impl,",
+        "   attrs = { '_hiddenattr' : attr.label(default = Label('//test:xxx')) },",
+        "   attr_aspects = ['dep'], provides = ['a1p'])",
+        "a2p = provider()",
+        "def _a2_impl(target,ctx):",
+        "   return [MyInfo(hidden_attr_label = str(ctx.attr._hiddenattr.label))]",
+        "a2 = aspect(_a2_impl,",
+        "  attrs = { '_hiddenattr' : attr.label(default = Label('//test:zzz')) },",
+        "  attr_aspects = ['dep'], required_aspect_providers = ['a1p'])",
+        "def _r1_impl(ctx):",
+        "  pass",
+        "def _r2_impl(ctx):",
+        "  return struct(result = ctx.attr.dep[MyInfo].hidden_attr_label)",
+        "r1 = rule(_r1_impl, attrs = { 'dep' : attr.label(aspects = [a1])})",
+        "r2 = rule(_r2_impl, attrs = { 'dep' : attr.label(aspects = [a2])})");
+    scratch.file(
+        "test/BUILD",
+        "load(':aspect.bzl', 'r1', 'r2')",
+        "r1(name = 'r0')",
+        "r1(name = 'r1', dep = ':r0')",
+        "r2(name = 'r2', dep = ':r1')",
+        "cc_library(",
+        "     name = 'xxx',",
+        ")",
+        "cc_library(",
+        "     name = 'zzz',",
+        ")");
+    AnalysisResult analysisResult = update("//test:r2");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    String result = (String) target.get("result");
+
+    assertThat(result).isEqualTo("//test:zzz");
+  }
+
+  @Test
+  public void testAllCcLibraryAttrsAreValidTypes() throws Exception {
+    scratch.file(
+        "test/aspect.bzl",
+        "def _impl(target, ctx):",
+        "  for entry in dir(ctx.rule.attr):",
+        "    val = getattr(ctx.rule.attr, entry, None)",
+        "    # Only legitimate Starlark values can be passed to dir(), so this effectively",
+        "    # verifies val is an appropriate Starlark type.",
+        "    _test_dir = dir(val)",
+        "  return []",
+        "",
+        "MyAspect = aspect(",
+        "  implementation=_impl,",
+        ")");
+    scratch.file("test/BUILD", "cc_library(", "     name = 'xxx',", ")");
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/aspect.bzl%MyAspect"), "//test:xxx");
+    assertThat(analysisResult.getAspectsMap().values().iterator().next()).isNotNull();
+  }
+
+  @Test
+  public void testApplyToGeneratingRules() throws Exception {
+    // Create test rules:
+    // dep_rule: a rule which may depend on other dep_rule targets and may optionally create
+    //     an output file.
+    // root_{with,no}_files: a rule which depends on dep_rule targets and attaches an aspect.
+    //     The rule returns a RootInfo provider which contains two fields:
+    //        'from_aspect' : a list of all labels that the aspect propagated to
+    //        'non_aspect' : a list of all labels that information was obtained from without aspect
+    //     root_with_files uses an aspect with apply_to_generating_rules=True, and root_no_files
+    //     uses an aspect with apply_to_generating_rules=False.
+    scratch.file(
+        "test/lib.bzl",
+        "RootInfo = provider()",
+        "NonAspectInfo = provider()",
+        "FromAspectInfo = provider()",
+        "def _aspect_impl(target, ctx):",
+        "  dep_labels = []",
+        "  for dep in ctx.rule.attr.deps:",
+        "    if FromAspectInfo in dep:",
+        "      dep_labels += [dep[FromAspectInfo].labels]",
+        "  return FromAspectInfo(labels = depset(direct = [ctx.label], transitive = dep_labels))",
+        "",
+        "def _rule_impl(ctx):",
+        "  non_aspect = []",
+        "  from_aspect = []",
+        "  for dep in ctx.attr.deps:",
+        "    if NonAspectInfo in dep:",
+        "      non_aspect +=  dep[NonAspectInfo].labels.to_list()",
+        "    if FromAspectInfo in dep:",
+        "      from_aspect += dep[FromAspectInfo].labels.to_list()",
+        "  return RootInfo(from_aspect = from_aspect, non_aspect = non_aspect)",
+        "",
+        "def _dep_rule_impl(ctx):",
+        "  if ctx.outputs.output:",
+        "    ctx.actions.run_shell(outputs = [ctx.outputs.output], command = 'dont run me')",
+        "  dep_labels = []",
+        "  for dep in ctx.attr.deps:",
+        "    if NonAspectInfo in dep:",
+        "      dep_labels += [dep[NonAspectInfo].labels]",
+        "  return NonAspectInfo(labels = depset(direct = [ctx.label], transitive = dep_labels))",
+        "",
+        "aspect_with_files = aspect(",
+        "  implementation = _aspect_impl,",
+        "  attr_aspects = ['deps'],",
+        "  apply_to_generating_rules = True)",
+        "",
+        "aspect_no_files = aspect(",
+        "  implementation = _aspect_impl,",
+        "  attr_aspects = ['deps'],",
+        "  apply_to_generating_rules = False)",
+        "",
+        "root_with_files = rule(implementation = _rule_impl,",
+        "  attrs = {'deps' : attr.label_list(aspects = [aspect_with_files])})",
+        "",
+        "root_no_files = rule(implementation = _rule_impl,",
+        "  attrs = {'deps' : attr.label_list(aspects = [aspect_no_files])})",
+        "",
+        "dep_rule = rule(implementation = _dep_rule_impl,",
+        "  attrs = {'deps' : attr.label_list(allow_files = True), 'output' : attr.output()})");
+
+    // Create a target graph such that two graph roots each point to a common subgraph
+    // alpha -> beta_output -> charlie, where beta_output is a generated output file of target
+    // 'beta'.
+    scratch.file(
+        "test/BUILD",
+        "load('//test:lib.bzl', 'root_with_files', 'root_no_files', 'dep_rule')",
+        "",
+        "root_with_files(name = 'test_with_files', deps = [':alpha'])",
+        "root_no_files(name = 'test_no_files', deps = [':alpha'])",
+        "dep_rule(name = 'alpha', deps = [':beta_output'])",
+        "dep_rule(name = 'beta', deps = [':charlie'], output = 'beta_output')",
+        "dep_rule(name = 'charlie')");
+
+    StarlarkProvider.Key rootInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "RootInfo");
+
+    AnalysisResult analysisResultWithFiles = update("//test:test_with_files");
+    ConfiguredTarget targetWithFiles =
+        Iterables.getOnlyElement(analysisResultWithFiles.getTargetsToBuild());
+    StructImpl rootInfoWithFiles = (StructImpl) targetWithFiles.get(rootInfoKey);
+    // With apply_to_generating_rules=True, the aspect should have traversed :beta_output and
+    // applied to both :beta and :charlie.
+    assertThat(rootInfoWithFiles.getValue("from_aspect", Sequence.class))
+        .containsExactly(
+            Label.parseAbsolute("//test:charlie", ImmutableMap.of()),
+            Label.parseAbsolute("//test:beta", ImmutableMap.of()),
+            Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+    assertThat(rootInfoWithFiles.getValue("non_aspect", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+
+    AnalysisResult analysisResultNoFiles = update("//test:test_no_files");
+    ConfiguredTarget targetNoFiles =
+        Iterables.getOnlyElement(analysisResultNoFiles.getTargetsToBuild());
+    StructImpl rootInfoNoFiles = (StructImpl) targetNoFiles.get(rootInfoKey);
+    // With apply_to_generating_rules=False, the aspect should have only accessed :alpha, as it
+    // must have stopped before :beta_output.
+    assertThat(rootInfoNoFiles.getValue("from_aspect", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+    assertThat(rootInfoWithFiles.getValue("non_aspect", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+  }
+
+  private void setupAspectOnAspectTargetGraph(
+      boolean applyRootToGeneratingRules, boolean applyDepToGeneratingRules) throws Exception {
+    // RootAspectInfo.both_labels returns a list of target labels which
+    //     were evaluated as [root_aspect(dep_aspect(target))].
+    // RootAspectInfo.root_only_labels returns a list of target labels which
+    //     were evaluated as [root_aspect(target)].
+    // DepAspectInfo.labels returns a list of target labels which were evaluated by dep_aspect.
+    scratch.file(
+        "test/lib.bzl",
+        "RootAspectInfo = provider()",
+        "DepAspectInfo = provider()",
+        "def _root_aspect_impl(target, ctx):",
+        "  both_labels = []",
+        "  root_only_labels = []",
+        "  for dep in ctx.rule.attr.deps:",
+        "    if RootAspectInfo in dep:",
+        "      both_labels += dep[RootAspectInfo].both_labels",
+        "      root_only_labels += dep[RootAspectInfo].root_only_labels",
+        "      if DepAspectInfo in dep:",
+        "        both_labels += [dep.label]",
+        "      else:",
+        "        root_only_labels += [dep.label]",
+        "  return RootAspectInfo(both_labels = both_labels, root_only_labels = root_only_labels)",
+        "",
+        "def _dep_aspect_impl(target, ctx):",
+        "  dep_labels = [ctx.label]",
+        "  for dep in ctx.rule.attr.deps:",
+        "    if DepAspectInfo in dep:",
+        "      dep_labels += dep[DepAspectInfo].labels",
+        "  return DepAspectInfo(labels = dep_labels)",
+        "",
+        "def _root_rule_impl(ctx):",
+        "  return [ctx.attr.deps[0][RootAspectInfo], ctx.attr.deps[0][DepAspectInfo]]",
+        "",
+        "def _dep_with_aspect_rule_impl(ctx):",
+        "  return [ctx.attr.deps[0][DepAspectInfo]]",
+        "",
+        "def _dep_rule_impl(ctx):",
+        "  if ctx.outputs.output:",
+        "    ctx.actions.run_shell(outputs = [ctx.outputs.output], command = 'dont run me')",
+        "  return []",
+        "",
+        "root_aspect = aspect(",
+        "  implementation = _root_aspect_impl,",
+        "  attr_aspects = ['deps'],",
+        "  required_aspect_providers = [DepAspectInfo],",
+        "  apply_to_generating_rules = " + (applyRootToGeneratingRules ? "True" : "False") + ")",
+        "",
+        "dep_aspect = aspect(",
+        "  implementation = _dep_aspect_impl,",
+        "  attr_aspects = ['deps'],",
+        "  provides = [DepAspectInfo],",
+        "  apply_to_generating_rules = " + (applyDepToGeneratingRules ? "True" : "False") + ")",
+        "",
+        "root_rule = rule(implementation = _root_rule_impl,",
+        "  attrs = {'deps' : attr.label_list(aspects = [root_aspect])})",
+        "",
+        "dep_with_aspect_rule = rule(implementation = _dep_with_aspect_rule_impl,",
+        "  attrs = {'deps' : attr.label_list(aspects = [dep_aspect])})",
+        "",
+        "dep_rule = rule(implementation = _dep_rule_impl,",
+        "  attrs = {'deps' : attr.label_list(allow_files = True), 'output' : attr.output()})");
+
+    // Target graph: test -> aspect_propagating_target -> alpha -> beta_output
+    // beta_output is an output of target `beta`
+    // beta -> charlie
+    scratch.file(
+        "test/BUILD",
+        "load('//test:lib.bzl', 'root_rule', 'dep_with_aspect_rule', 'dep_rule')",
+        "",
+        "root_rule(name = 'test', deps = [':aspect_propagating_target'])",
+        "dep_with_aspect_rule(name = 'aspect_propagating_target', deps = [':alpha'])",
+        "dep_rule(name = 'alpha', deps = [':beta_output'])",
+        "dep_rule(name = 'beta', deps = [':charlie'], output = 'beta_output')",
+        "dep_rule(name = 'charlie')");
+  }
+
+  @Test
+  public void testAspectOnAspectApplyToGeneratingRules_bothPropagate() throws Exception {
+    setupAspectOnAspectTargetGraph(
+        /* applyRootToGeneratingRules= */ true, /* applyDepToGeneratingRules= */ true);
+
+    StarlarkProvider.Key rootInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "RootAspectInfo");
+    StarlarkProvider.Key depInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "DepAspectInfo");
+
+    AnalysisResult analysisResult = update("//test:test");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    StructImpl rootInfo = (StructImpl) target.get(rootInfoKey);
+    StructImpl depInfo = (StructImpl) target.get(depInfoKey);
+
+    assertThat(rootInfo.getValue("both_labels", Sequence.class))
+        .containsExactly(
+            Label.parseAbsolute("//test:alpha", ImmutableMap.of()),
+            Label.parseAbsolute("//test:beta_output", ImmutableMap.of()),
+            Label.parseAbsolute("//test:charlie", ImmutableMap.of()));
+    assertThat(rootInfo.getValue("root_only_labels", Sequence.class)).isEmpty();
+    assertThat(depInfo.getValue("labels", Sequence.class))
+        .containsExactly(
+            Label.parseAbsolute("//test:alpha", ImmutableMap.of()),
+            Label.parseAbsolute("//test:beta", ImmutableMap.of()),
+            Label.parseAbsolute("//test:charlie", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testAspectOnAspectApplyToGeneratingRules_neitherPropagate() throws Exception {
+    setupAspectOnAspectTargetGraph(
+        /* applyRootToGeneratingRules= */ false, /* applyDepToGeneratingRules= */ false);
+
+    StarlarkProvider.Key rootInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "RootAspectInfo");
+    StarlarkProvider.Key depInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "DepAspectInfo");
+
+    AnalysisResult analysisResult = update("//test:test");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    StructImpl rootInfo = (StructImpl) target.get(rootInfoKey);
+    StructImpl depInfo = (StructImpl) target.get(depInfoKey);
+
+    assertThat(rootInfo.getValue("both_labels", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+    assertThat(rootInfo.getValue("root_only_labels", Sequence.class)).isEmpty();
+    assertThat(depInfo.getValue("labels", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testAspectOnAspectApplyToGeneratingRules_rootOnly() throws Exception {
+    setupAspectOnAspectTargetGraph(
+        /* applyRootToGeneratingRules= */ true, /* applyDepToGeneratingRules= */ false);
+
+    StarlarkProvider.Key rootInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "RootAspectInfo");
+    StarlarkProvider.Key depInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "DepAspectInfo");
+
+    AnalysisResult analysisResult = update("//test:test");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    StructImpl rootInfo = (StructImpl) target.get(rootInfoKey);
+    StructImpl depInfo = (StructImpl) target.get(depInfoKey);
+
+    assertThat(rootInfo.getValue("both_labels", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+    assertThat(rootInfo.getValue("root_only_labels", Sequence.class))
+        .containsExactly(
+            Label.parseAbsolute("//test:beta_output", ImmutableMap.of()),
+            Label.parseAbsolute("//test:charlie", ImmutableMap.of()));
+    assertThat(depInfo.getValue("labels", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testAspectOnAspectApplyToGeneratingRules_depOnly() throws Exception {
+    setupAspectOnAspectTargetGraph(
+        /* applyRootToGeneratingRules= */ false, /* applyDepToGeneratingRules= */ true);
+
+    StarlarkProvider.Key rootInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "RootAspectInfo");
+    StarlarkProvider.Key depInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:lib.bzl", ImmutableMap.of()), "DepAspectInfo");
+
+    AnalysisResult analysisResult = update("//test:test");
+    ConfiguredTarget target = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    StructImpl rootInfo = (StructImpl) target.get(rootInfoKey);
+    StructImpl depInfo = (StructImpl) target.get(depInfoKey);
+
+    assertThat(rootInfo.getValue("both_labels", Sequence.class))
+        .containsExactly(Label.parseAbsolute("//test:alpha", ImmutableMap.of()));
+    assertThat(rootInfo.getValue("root_only_labels", Sequence.class)).isEmpty();
+    assertThat(depInfo.getValue("labels", Sequence.class))
+        .containsExactly(
+            Label.parseAbsolute("//test:alpha", ImmutableMap.of()),
+            Label.parseAbsolute("//test:beta", ImmutableMap.of()),
+            Label.parseAbsolute("//test:charlie", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testAspectActionsDontInheritExecProperties() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:defs.bzl', 'my_rule')",
+        "my_rule(",
+        "  name = 'my_target',",
+        "  deps = [':my_dep'],",
+        ")",
+        "cc_binary(",
+        "  name = 'my_dep',",
+        "  srcs = ['dep.cc'],",
+        "  exec_properties = {'mem': '16g'},",
+        ")");
+    scratch.file(
+        "test/defs.bzl",
+        "def _aspect_impl(target, ctx):",
+        "  f = ctx.actions.declare_file('f.txt')",
+        "  ctx.actions.write(f, 'f')",
+        "  return []",
+        "my_aspect = aspect(",
+        "  implementation = _aspect_impl,",
+        "  attr_aspects = ['deps'],",
+        ")",
+        "def _rule_impl(ctx):",
+        "  pass",
+        "my_rule = rule(",
+        "  implementation = _rule_impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(aspects = [my_aspect])",
+        "  },",
+        ")");
+    scratch.file("test/dep.cc", "int main() {return 0;}");
+
+    AnalysisResult analysisResult =
+        update(ImmutableList.of("test/defs.bzl%my_aspect"), "//test:my_target");
+    assertThat(analysisResult).isNotNull();
+    ActionOwner owner =
+        Iterables.getOnlyElement(
+                Iterables.getOnlyElement(analysisResult.getAspectsMap().values()).getActions())
+            .getOwner();
+    assertThat(owner.getExecProperties()).isEmpty();
+  }
+
+  /** StarlarkAspectTest with "keep going" flag */
+  @RunWith(JUnit4.class)
+  public static final class WithKeepGoing extends StarlarkDefinedAspectsTest {
+    @Override
+    protected FlagBuilder defaultFlags() {
+      return super.defaultFlags().with(Flag.KEEP_GOING);
+    }
+
+    @Override
+    protected boolean keepGoing() {
+      return true;
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
new file mode 100644
index 0000000..983bd02
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkIntegrationTest.java
@@ -0,0 +1,3485 @@
+// Copyright 2016 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.analysis.OutputGroupInfo.INTERNAL_SUFFIX;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.Correspondence;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.OutputGroupInfo;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.config.StarlarkDefinedConfigTransition;
+import com.google.devtools.build.lib.analysis.configuredtargets.FileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkAttributeTransitionProvider;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleTransitionProvider;
+import com.google.devtools.build.lib.analysis.test.AnalysisFailure;
+import com.google.devtools.build.lib.analysis.test.AnalysisFailureInfo;
+import com.google.devtools.build.lib.analysis.test.AnalysisTestResultInfo;
+import com.google.devtools.build.lib.analysis.test.InstrumentedFilesInfo;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.AttributeContainer;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.BuildSetting;
+import com.google.devtools.build.lib.packages.FunctionSplitTransitionAllowlist;
+import com.google.devtools.build.lib.packages.Provider;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.objc.ObjcProvider;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
+import com.google.devtools.build.lib.syntax.NoneType;
+import com.google.devtools.build.lib.syntax.Sequence;
+import com.google.devtools.build.lib.syntax.Starlark;
+import com.google.devtools.build.lib.syntax.StarlarkList;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for Starlark. */
+@RunWith(JUnit4.class)
+public class StarlarkIntegrationTest extends BuildViewTestCase {
+  protected boolean keepGoing() {
+    return false;
+  }
+
+  @Before
+  public void setupMyInfo() throws IOException {
+    scratch.file("myinfo/myinfo.bzl", "MyInfo = provider()");
+
+    scratch.file("myinfo/BUILD");
+  }
+
+  private StructImpl getMyInfoFromTarget(ConfiguredTarget configuredTarget) throws Exception {
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//myinfo:myinfo.bzl", ImmutableMap.of()), "MyInfo");
+    return (StructImpl) configuredTarget.get(key);
+  }
+
+  @Test
+  public void testRemoteLabelAsDefaultAttributeValue() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def _impl(ctx):",
+        "  pass",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'dep' : attr.label_list(default=[\"@r//:t\"]) })");
+
+    // We are only interested in whether the label string in the default value can be converted
+    // to a proper Label without an exception (see GitHub issue #1442).
+    // Consequently, we expect getTarget() to fail later since the repository does not exist.
+    checkError(
+        "test/starlark",
+        "the_rule",
+        "no such package '@r//': The repository '@r' could not be resolved",
+        "load('//test/starlark:extension.bzl', 'my_rule')",
+        "",
+        "my_rule(name='the_rule')");
+  }
+
+  @Test
+  public void testMainRepoLabelWorkspaceRoot() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  return [MyInfo(result = ctx.label.workspace_root)]",
+        "my_rule = rule(implementation = _impl, attrs = { })");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'my_rule')",
+        "my_rule(name='t')");
+
+    ConfiguredTarget myTarget = getConfiguredTarget("//test/starlark:t");
+    String result = (String) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void testExternalRepoLabelWorkspaceRoot_subdirRepoLayout() throws Exception {
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='/r')")
+            .build());
+
+    scratch.file("/r/WORKSPACE");
+    scratch.file(
+        "/r/test/starlark/extension.bzl",
+        "load('@//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  return [MyInfo(result = ctx.label.workspace_root)]",
+        "my_rule = rule(implementation = _impl, attrs = { })");
+    scratch.file(
+        "/r/BUILD", "load('//:test/starlark/extension.bzl', 'my_rule')", "my_rule(name='t')");
+
+    // Required since we have a new WORKSPACE file.
+    invalidatePackages(true);
+
+    ConfiguredTarget myTarget = getConfiguredTarget("@r//:t");
+    String result = (String) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result).isEqualTo("external/r");
+  }
+
+  @Test
+  public void testExternalRepoLabelWorkspaceRoot_siblingRepoLayout() throws Exception {
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='/r')")
+            .build());
+
+    scratch.file("/r/WORKSPACE");
+    scratch.file(
+        "/r/test/starlark/extension.bzl",
+        "load('@//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  return [MyInfo(result = ctx.label.workspace_root)]",
+        "my_rule = rule(implementation = _impl, attrs = { })");
+    scratch.file(
+        "/r/BUILD", "load('//:test/starlark/extension.bzl', 'my_rule')", "my_rule(name='t')");
+
+    // Required since we have a new WORKSPACE file.
+    invalidatePackages(true);
+
+    setStarlarkSemanticsOptions("--experimental_sibling_repository_layout");
+
+    ConfiguredTarget myTarget = getConfiguredTarget("@r//:t");
+    String result = (String) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result).isEqualTo("../r");
+  }
+
+  @Test
+  public void testSameMethodNames() throws Exception {
+    // The alias feature of load() may hide the fact that two methods in the stack trace have the
+    // same name. This is perfectly legal as long as these two methods are actually distinct.
+    // Consequently, no "Recursion was detected" error must be thrown.
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//test/starlark:other.bzl', other_impl = 'impl')",
+        "def impl(ctx):",
+        "  other_impl(ctx)",
+        "empty = rule(implementation = impl)");
+    scratch.file("test/starlark/other.bzl", "def impl(ctx):", "  print('This rule does nothing')");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'empty')",
+        "empty(name = 'test_target')");
+
+    getConfiguredTarget("//test/starlark:test_target");
+  }
+
+  private AttributeContainer getContainerForTarget(String targetName) throws Exception {
+    ConfiguredTargetAndData target = getConfiguredTargetAndData("//test/starlark:" + targetName);
+    return target.getTarget().getAssociatedRule().getAttributeContainer();
+  }
+
+  @Test
+  public void testMacroHasGeneratorAttributes() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def _impl(ctx):",
+        "  print('This rule does nothing')",
+        "",
+        "empty = rule(implementation = _impl)",
+        "no_macro = rule(implementation = _impl)",
+        "",
+        "def macro(name, visibility=None):",
+        "  empty(name = name, visibility=visibility)",
+        "def native_macro(name):",
+        "  native.cc_library(name = name + '_suffix')");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', macro_rule = 'macro', no_macro_rule = 'no_macro',",
+        "  native_macro_rule = 'native_macro')",
+        "macro_rule(name = 'macro_target')",
+        "no_macro_rule(name = 'no_macro_target')",
+        "native_macro_rule(name = 'native_macro_target')",
+        "cc_binary(name = 'cc_target', deps = ['cc_dep'])",
+        "cc_library(name = 'cc_dep')");
+
+    AttributeContainer withMacro = getContainerForTarget("macro_target");
+    assertThat(withMacro.getAttr("generator_name")).isEqualTo("macro_target");
+    assertThat(withMacro.getAttr("generator_function")).isEqualTo("macro");
+    assertThat(withMacro.getAttr("generator_location")).isEqualTo("test/starlark/BUILD:3:11");
+
+    // Attributes are only set when the rule was created by a macro
+    AttributeContainer noMacro = getContainerForTarget("no_macro_target");
+    assertThat(noMacro.getAttr("generator_name")).isEqualTo("");
+    assertThat(noMacro.getAttr("generator_function")).isEqualTo("");
+    assertThat(noMacro.getAttr("generator_location")).isEqualTo("");
+
+    AttributeContainer nativeMacro = getContainerForTarget("native_macro_target_suffix");
+    assertThat(nativeMacro.getAttr("generator_name")).isEqualTo("native_macro_target");
+    assertThat(nativeMacro.getAttr("generator_function")).isEqualTo("native_macro");
+    assertThat(nativeMacro.getAttr("generator_location")).isEqualTo("test/starlark/BUILD:5:18");
+
+    AttributeContainer ccTarget = getContainerForTarget("cc_target");
+    assertThat(ccTarget.getAttr("generator_name")).isEqualTo("");
+    assertThat(ccTarget.getAttr("generator_function")).isEqualTo("");
+    assertThat(ccTarget.getAttr("generator_location")).isEqualTo("");
+  }
+
+  @Test
+  public void sanityCheckUserDefinedTestRule() throws Exception {
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _impl(ctx):",
+        "  output = ctx.outputs.out",
+        "  ctx.actions.write(output = output, content = 'hello', is_executable=True)",
+        "  return [DefaultInfo(executable = output)]",
+        "",
+        "fake_test = rule(",
+        "  implementation = _impl,",
+        "  test=True,",
+        "  attrs = {'_xcode_config': attr.label(default = configuration_field(",
+        "  fragment = 'apple', name = \"xcode_config_label\"))},",
+        "  outputs = {\"out\": \"%{name}.txt\"})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'fake_test')",
+        "fake_test(name = 'test_name')");
+    getConfiguredTarget("//test/starlark:test_name");
+  }
+
+  @Test
+  public void testOutputGroupsDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  f = ctx.attr.dep[OutputGroupInfo]._hidden_top_level" + INTERNAL_SUFFIX,
+        "  return [MyInfo(result = f),",
+        "      OutputGroupInfo(my_group = f)]",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'dep' : attr.label() })");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl',  'my_rule')",
+        "cc_binary(name = 'lib', data = ['a.txt'])",
+        "my_rule(name='my', dep = ':lib')");
+    NestedSet<Artifact> hiddenTopLevelArtifacts =
+        OutputGroupInfo.get(getConfiguredTarget("//test/starlark:lib"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    ConfiguredTarget myTarget = getConfiguredTarget("//test/starlark:my");
+    Depset result = (Depset) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result.getSet(Artifact.class).toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(OutputGroupInfo.get(myTarget).getOutputGroup("my_group").toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+  }
+
+
+  @Test
+  public void testOutputGroupsAsDictionary() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  f = ctx.attr.dep.output_groups['_hidden_top_level" + INTERNAL_SUFFIX + "']",
+        "  has_key1 = '_hidden_top_level" + INTERNAL_SUFFIX + "' in ctx.attr.dep.output_groups",
+        "  has_key2 = 'foobar' in ctx.attr.dep.output_groups",
+        "  all_keys = [k for k in ctx.attr.dep.output_groups]",
+        "  return [MyInfo(result = f, ",
+        "                has_key1 = has_key1,",
+        "                has_key2 = has_key2,",
+        "                all_keys = all_keys),",
+        "      OutputGroupInfo(my_group = f)]",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'dep' : attr.label() })");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl',  'my_rule')",
+        "cc_binary(name = 'lib', data = ['a.txt'])",
+        "my_rule(name='my', dep = ':lib')");
+    NestedSet<Artifact> hiddenTopLevelArtifacts =
+        OutputGroupInfo.get(getConfiguredTarget("//test/starlark:lib"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    ConfiguredTarget myTarget = getConfiguredTarget("//test/starlark:my");
+    StructImpl myInfo = getMyInfoFromTarget(myTarget);
+    Depset result = (Depset) myInfo.getValue("result");
+    assertThat(result.getSet(Artifact.class).toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(OutputGroupInfo.get(myTarget).getOutputGroup("my_group").toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(myInfo.getValue("has_key1")).isEqualTo(Boolean.TRUE);
+    assertThat(myInfo.getValue("has_key2")).isEqualTo(Boolean.FALSE);
+    assertThat((Sequence) myInfo.getValue("all_keys"))
+        .containsExactly(
+            OutputGroupInfo.HIDDEN_TOP_LEVEL,
+            OutputGroupInfo.COMPILATION_PREREQUISITES,
+            OutputGroupInfo.FILES_TO_COMPILE,
+            OutputGroupInfo.TEMP_FILES);
+  }
+
+  @Test
+  public void testOutputGroupsAsDictionaryPipe() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  g = depset(ctx.attr.dep.output_groups['_hidden_top_level" + INTERNAL_SUFFIX + "'])",
+        "  return [MyInfo(result = g),",
+        "      OutputGroupInfo(my_group = g)]",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'dep' : attr.label() })");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl',  'my_rule')",
+        "cc_binary(name = 'lib', data = ['a.txt'])",
+        "my_rule(name='my', dep = ':lib')");
+    NestedSet<Artifact> hiddenTopLevelArtifacts =
+        OutputGroupInfo.get(getConfiguredTarget("//test/starlark:lib"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    ConfiguredTarget myTarget = getConfiguredTarget("//test/starlark:my");
+    Depset result = (Depset) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result.getSet(Artifact.class).toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(OutputGroupInfo.get(myTarget).getOutputGroup("my_group").toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+  }
+
+  @Test
+  public void testOutputGroupsDeclaredProviderWithList() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  f = ctx.attr.dep[OutputGroupInfo]._hidden_top_level" + INTERNAL_SUFFIX,
+        "  g = f.to_list()",
+        "  return [MyInfo(result = f),",
+        "      OutputGroupInfo(my_group = g, my_empty_group = [])]",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'dep' : attr.label() })");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl',  'my_rule')",
+        "cc_binary(name = 'lib', data = ['a.txt'])",
+        "my_rule(name='my', dep = ':lib')");
+    NestedSet<Artifact> hiddenTopLevelArtifacts =
+        OutputGroupInfo.get(getConfiguredTarget("//test/starlark:lib"))
+            .getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL);
+    ConfiguredTarget myTarget = getConfiguredTarget("//test/starlark:my");
+    Depset result = (Depset) getMyInfoFromTarget(myTarget).getValue("result");
+    assertThat(result.getSet(Artifact.class).toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(OutputGroupInfo.get(myTarget).getOutputGroup("my_group").toList())
+        .containsExactlyElementsIn(hiddenTopLevelArtifacts.toList());
+    assertThat(OutputGroupInfo.get(myTarget).getOutputGroup("my_empty_group").toList()).isEmpty();
+  }
+
+  @Test
+  public void testStackTraceErrorInFunction() throws Exception {
+    runStackTraceTest(
+        "str",
+        "\t\tstr.index(1)"
+            + System.lineSeparator()
+            + "in call to index(), parameter 'sub' got value of type 'int', want 'string'");
+  }
+
+  @Test
+  public void testStackTraceMissingMethod() throws Exception {
+    runStackTraceTest(
+        "None",
+        "\t\tNone.index"
+            + System.lineSeparator()
+            + "'NoneType' value has no field or method 'index'");
+  }
+
+  protected void runStackTraceTest(String object, String errorMessage) throws Exception {
+    reporter.removeHandler(failFastHandler);
+    String expectedTrace =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "Traceback (most recent call last):",
+                "\tFile \"/workspace/test/starlark/BUILD\", line 3",
+                "\t\tcustom_rule(name = 'cr')",
+                "\tFile \"/workspace/test/starlark/extension.bzl\", line 6, in custom_rule_impl",
+                "\t\tfoo()",
+                "\tFile \"/workspace/test/starlark/extension.bzl\", line 9, in foo",
+                "\t\tbar(2, 4)",
+                "\tFile \"/workspace/test/starlark/extension.bzl\", line 11, in bar",
+                "\t\tfirst(x, y, z)",
+                "\tFile \"/workspace/test/starlark/functions.bzl\", line 2, in first",
+                "\t\tsecond(a, b)",
+                "\tFile \"/workspace/test/starlark/functions.bzl\", line 5, in second",
+                "\t\tthird(\"legal\")",
+                "\tFile \"/workspace/test/starlark/functions.bzl\", line 7, in third",
+                errorMessage);
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//test/starlark:functions.bzl', 'first')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  ftb = depset(attr1)",
+        "  foo()",
+        "  return [MyInfo(provider_key = ftb)]",
+        "def foo():",
+        "  bar(2,4)",
+        "def bar(x,y,z=1):",
+        "  first(x,y, z)",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
+    scratch.file(
+        "test/starlark/functions.bzl",
+        "def first(a, b, c):",
+        "  second(a, b)",
+        "  third(b)",
+        "def second(a, b):",
+        "  third('legal')",
+        "def third(str):",
+        "  " + object + ".index(1)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    getConfiguredTarget("//test/starlark:cr");
+    assertContainsEvent(expectedTrace);
+  }
+
+  @Test
+  public void testFilesToBuild() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  ftb = depset(attr1)",
+        "  return [DefaultInfo(runfiles = ctx.runfiles(), files = ftb)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testRunfiles() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  rf = ctx.runfiles(files = attr1)",
+        "  return [DefaultInfo(runfiles = rf)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(RunfilesProvider.class).getDefaultRunfiles().getAllArtifacts()))
+        .containsExactly("a.txt");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(RunfilesProvider.class).getDataRunfiles().getAllArtifacts()))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testAccessRunfiles() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  runfiles = ctx.attr.x.default_runfiles.files",
+        "  return [DefaultInfo(files = runfiles)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'x': attr.label(allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "cc_library(name = 'lib', data = ['a.txt'])",
+        "custom_rule(name = 'cr1', x = ':lib')",
+        "custom_rule(name = 'cr2', x = 'b.txt')");
+
+    scratch.file("test/starlark/a.txt");
+    scratch.file("test/starlark/b.txt");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr1");
+    List<String> baseArtifactNames =
+        ActionsTestUtil.baseArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild());
+    assertThat(baseArtifactNames).containsExactly("a.txt");
+
+    target = getConfiguredTarget("//test/starlark:cr2");
+    baseArtifactNames =
+        ActionsTestUtil.baseArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild());
+    assertThat(baseArtifactNames).isEmpty();
+  }
+
+  @Test
+  public void testStatefulRunfiles() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  rf1 = ctx.runfiles(files = attr1)",
+        "  rf2 = ctx.runfiles()",
+        "  return [DefaultInfo(data_runfiles = rf1, default_runfiles = rf2)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory = True, allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    assertThat(target.getProvider(RunfilesProvider.class).getDefaultRunfiles().isEmpty()).isTrue();
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(RunfilesProvider.class).getDataRunfiles().getAllArtifacts()))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testExecutableGetsInRunfilesAndFilesToBuild() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  ctx.actions.write(output = ctx.outputs.executable, content = 'echo hello')",
+        "  rf = ctx.runfiles(ctx.files.data)",
+        "  return [DefaultInfo(runfiles = rf)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl, executable = True,",
+        "  attrs = {'data': attr.label_list(allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', data = [':a.txt'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(RunfilesProvider.class).getDefaultRunfiles().getAllArtifacts()))
+        .containsExactly("a.txt", "cr")
+        .inOrder();
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("cr");
+  }
+
+  @Test
+  public void testCannotSpecifyRunfilesWithDataOrDefaultRunfiles_struct() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  rf = ctx.runfiles()",
+        "  return struct(runfiles = rf, default_runfiles = rf)",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    checkError(
+        "test/starlark",
+        "cr",
+        "Cannot specify the provider 'runfiles' together with "
+            + "'data_runfiles' or 'default_runfiles'",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+  }
+
+  @Test
+  public void testCannotSpecifyRunfilesWithDataOrDefaultRunfiles_defaultInfo() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  rf = ctx.runfiles()",
+        "  return [DefaultInfo(runfiles = rf, default_runfiles = rf)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    checkError(
+        "test/starlark",
+        "cr",
+        "Cannot specify the provider 'runfiles' together with "
+            + "'data_runfiles' or 'default_runfiles'",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+  }
+
+  @Test
+  public void testDefaultInfoWithRunfilesConstructor() throws Exception {
+    scratch.file(
+        "pkg/BUILD",
+        "sh_binary(name = 'tryme',",
+        "          srcs = [':tryme.sh'],",
+        "          visibility = ['//visibility:public'],",
+        ")");
+
+    scratch.file(
+        "src/rulez.bzl",
+        "def  _impl(ctx):",
+        "   info = DefaultInfo(runfiles = ctx.runfiles(files=[ctx.executable.dep]))",
+        "   if info.default_runfiles.files.to_list()[0] != ctx.executable.dep:",
+        "       fail('expected runfile to be in info.default_runfiles')",
+        "   return [info]",
+        "r = rule(_impl,",
+        "         attrs = {",
+        "            'dep' : attr.label(executable = True, mandatory = True, cfg = 'host'),",
+        "         }",
+        ")");
+
+    scratch.file(
+        "src/BUILD", "load(':rulez.bzl', 'r')", "r(name = 'r_tools', dep = '//pkg:tryme')");
+
+    assertThat(getConfiguredTarget("//src:r_tools")).isNotNull();
+  }
+
+  @Test
+  public void testInstrumentedFilesProviderWithCodeCoverageDisabled() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(instrumented_files=struct(",
+        "      extensions = ['txt'],",
+        "      source_attributes = ['attr1'],",
+        "      dependency_attributes = ['attr2']))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "      'attr1': attr.label_list(mandatory = True, allow_files=True),",
+        "      'attr2': attr.label_list(mandatory = True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "java_library(name='jl', srcs = [':A.java'])",
+        "custom_rule(name = 'cr', attr1 = [':a.txt', ':a.random'], attr2 = [':jl'])");
+
+    useConfiguration("--nocollect_code_coverage");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    InstrumentedFilesInfo provider = target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR);
+    assertWithMessage("InstrumentedFilesInfo should be set.").that(provider).isNotNull();
+    assertThat(ActionsTestUtil.baseArtifactNames(provider.getInstrumentedFiles())).isEmpty();
+  }
+
+  @Test
+  public void testInstrumentedFilesProviderWithCodeCoverageEnabled() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(instrumented_files=struct(",
+        "      extensions = ['txt'],",
+        "      source_attributes = ['attr1'],",
+        "      dependency_attributes = ['attr2']))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "      'attr1': attr.label_list(mandatory = True, allow_files=True),",
+        "      'attr2': attr.label_list(mandatory = True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "java_library(name='jl', srcs = [':A.java'])",
+        "custom_rule(name = 'cr', attr1 = [':a.txt', ':a.random'], attr2 = [':jl'])");
+
+    useConfiguration("--collect_code_coverage");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    InstrumentedFilesInfo provider = target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR);
+    assertWithMessage("InstrumentedFilesInfo should be set.").that(provider).isNotNull();
+    assertThat(ActionsTestUtil.baseArtifactNames(provider.getInstrumentedFiles()))
+        .containsExactly("a.txt", "A.java");
+  }
+
+  @Test
+  public void testInstrumentedFilesInfo_coverageDisabled() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(instrumented_files=struct(",
+        "      extensions = ['txt'],",
+        "      source_attributes = ['attr1'],",
+        "      dependency_attributes = ['attr2']))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "      'attr1': attr.label_list(mandatory = True, allow_files=True),",
+        "      'attr2': attr.label_list(mandatory = True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "java_library(name='jl', srcs = [':A.java'])",
+        "custom_rule(name = 'cr', attr1 = [':a.txt', ':a.random'], attr2 = [':jl'])");
+
+    useConfiguration("--nocollect_code_coverage");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    InstrumentedFilesInfo provider = target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR);
+    assertWithMessage("InstrumentedFilesInfo should be set.").that(provider).isNotNull();
+    assertThat(ActionsTestUtil.baseArtifactNames(provider.getInstrumentedFiles())).isEmpty();
+  }
+
+  @Test
+  public void testInstrumentedFilesInfo_coverageEnabled() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "",
+        "def custom_rule_impl(ctx):",
+        "  return [coverage_common.instrumented_files_info(ctx,",
+        "      extensions = ['txt'],",
+        "      source_attributes = ['attr1'],",
+        "      dependency_attributes = ['attr2'])]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "      'attr1': attr.label_list(mandatory = True, allow_files=True),",
+        "      'attr2': attr.label_list(mandatory = True)})",
+        "",
+        "def test_rule_impl(ctx):",
+        "  return [MyInfo(",
+        // The point of this is to assert that these fields can be read in analysistest.
+        // Normally, this information wouldn't be forwarded via a different provider.
+        "    instrumented_files = ctx.attr.target[InstrumentedFilesInfo].instrumented_files,",
+        "    metadata_files = ctx.attr.target[InstrumentedFilesInfo].metadata_files)]",
+        "",
+        "test_rule = rule(implementation = test_rule_impl,",
+        "  attrs = {'target': attr.label(mandatory = True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule', 'test_rule')",
+        "",
+        "cc_library(name='cl', srcs = [':A.cc'])",
+        "custom_rule(name = 'cr', attr1 = [':a.txt', ':a.random'], attr2 = [':cl'])",
+        "test_rule(name = 'test', target = ':cr')");
+
+    useConfiguration("--collect_code_coverage");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:test");
+    StructImpl myInfo = getMyInfoFromTarget(target);
+    assertThat(
+            ActionsTestUtil.baseArtifactNames(
+                ((Depset) myInfo.getValue("instrumented_files")).getSet(Artifact.class)))
+        .containsExactly("a.txt", "A.cc");
+    assertThat(
+            ActionsTestUtil.baseArtifactNames(
+                ((Depset) myInfo.getValue("metadata_files")).getSet(Artifact.class)))
+        .containsExactly("A.gcno");
+  }
+
+  @Test
+  public void testTransitiveInfoProviders() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  ftb = depset(attr1)",
+        "  return [MyInfo(provider_key = ftb)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    RuleConfiguredTarget target = (RuleConfiguredTarget) getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(
+            ActionsTestUtil.baseArtifactNames(
+                ((Depset) getMyInfoFromTarget(target).getValue("provider_key"))
+                    .getSet(Artifact.class)))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testInstrumentedFilesForwardedFromDepsByDefaultExperimentFlag() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def wrapper_impl(ctx):",
+        // This wrapper doesn't configure InstrumentedFilesInfo.
+        "    return []",
+        "",
+        "wrapper = rule(implementation = wrapper_impl,",
+        "    attrs = {",
+        "        'srcs': attr.label_list(allow_files = True),",
+        "        'wrapped': attr.label(mandatory = True),",
+        "        'wrapped_list': attr.label_list(),",
+        // Host deps aren't forwarded by default, since they don't provide code/binaries executed
+        // at runtime.
+        "        'tool': attr.label(cfg = 'host', executable = True, mandatory = True),",
+        "    })");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'wrapper')",
+        "",
+        "cc_binary(name = 'tool', srcs = [':tool.cc'])",
+        "cc_binary(name = 'wrapped', srcs = [':wrapped.cc'])",
+        "cc_binary(name = 'wrapped_list', srcs = [':wrapped_list.cc'])",
+        "wrapper(",
+        "    name = 'wrapper',",
+        "    srcs = ['ignored.cc'],",
+        "    wrapped = ':wrapped',",
+        "    wrapped_list = [':wrapped_list'],",
+        "    tool = ':tool',",
+        ")",
+        "cc_binary(name = 'outer', data = [':wrapper'])");
+
+    // Current behavior is that nothing gets forwarded if IntstrumentedFilesInfo is not configured.
+    // That means that source files are not collected for the coverage manifest unless the entire
+    // dependency chain between the test and the source file explicitly configures coverage.
+    // New behavior is protected by --experimental_forward_instrumented_files_info_by_default.
+    useConfiguration("--collect_code_coverage");
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:outer");
+    InstrumentedFilesInfo provider = target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR);
+    assertWithMessage("InstrumentedFilesInfo should be set.").that(provider).isNotNull();
+    assertThat(ActionsTestUtil.baseArtifactNames(provider.getInstrumentedFiles())).isEmpty();
+
+    // Instead, the default behavior could be to forward InstrumentedFilesInfo from all
+    // dependencies. Coverage still needs to be configured for rules that handle source files for
+    // languages which support coverage instrumentation, but not every wrapper rule in the
+    // dependency chain needs to configure that for instrumentation to be correct.
+    useConfiguration(
+        "--collect_code_coverage", "--experimental_forward_instrumented_files_info_by_default");
+    target = getConfiguredTarget("//test/starlark:outer");
+    provider = target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR);
+    assertWithMessage("InstrumentedFilesInfo should be set.").that(provider).isNotNull();
+    assertThat(ActionsTestUtil.baseArtifactNames(provider.getInstrumentedFiles()))
+        .containsExactly("wrapped.cc", "wrapped_list.cc");
+  }
+
+  @Test
+  public void testMandatoryProviderMissing() throws Exception {
+    scratch.file("test/starlark/BUILD");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def rule_impl(ctx):",
+        "  return []",
+        "",
+        "dependent_rule = rule(implementation = rule_impl)",
+        "",
+        "main_rule = rule(implementation = rule_impl,",
+        "    attrs = {'dependencies': attr.label_list(providers = ['some_provider'],",
+        "        allow_files=True)})");
+
+    checkError(
+        "test",
+        "b",
+        "in dependencies attribute of main_rule rule //test:b: "
+            + "'//test:a' does not have mandatory providers: 'some_provider'",
+        "load('//test/starlark:extension.bzl', 'dependent_rule')",
+        "load('//test/starlark:extension.bzl', 'main_rule')",
+        "",
+        "dependent_rule(name = 'a')",
+        "main_rule(name = 'b', dependencies = [':a'])");
+  }
+
+  @Test
+  public void testSpecialMandatoryProviderMissing() throws Exception {
+    // Test that rules satisfy `providers = [...]` condition if a special provider that always
+    // exists for all rules is requested. Also check external rules.
+
+    FileSystemUtils.appendIsoLatin1(scratch.resolve("WORKSPACE"),
+        "bind(name = 'bar', actual = '//test/ext:bar')");
+    scratch.file(
+        "test/ext/BUILD",
+        "load('//test/starlark:extension.bzl', 'foobar')",
+        "",
+        "foobar(name = 'bar', visibility = ['//visibility:public'],)");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def rule_impl(ctx):",
+        "  pass",
+        "",
+        "foobar = rule(implementation = rule_impl)",
+        "main_rule = rule(implementation = rule_impl, attrs = {",
+        "    'deps': attr.label_list(providers = [",
+        "        'files', 'data_runfiles', 'default_runfiles',",
+        "        'files_to_run', 'output_groups',",
+        "    ])",
+        "})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load(':extension.bzl', 'foobar', 'main_rule')",
+        "",
+        "foobar(name = 'foo')",
+        "main_rule(name = 'main', deps = [':foo', '//external:bar'])");
+
+    invalidatePackages();
+    getConfiguredTarget("//test/starlark:main");
+  }
+
+  @Test
+  public void testActions() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  output = ctx.outputs.o",
+        "  ctx.actions.run_shell(",
+        "    inputs = attr1,",
+        "    outputs = [output],",
+        "    command = 'echo')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)},",
+        "  outputs = {'o': 'o.txt'})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    getConfiguredTarget("//test/starlark:cr");
+
+    FileConfiguredTarget target = getFileConfiguredTarget("//test/starlark:o.txt");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            getGeneratingAction(target.getArtifact()).getInputs()))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputFunction() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  files = [ctx.outputs.o]",
+        "  ctx.actions.run_shell(",
+        "    outputs = files,",
+        "    command = 'echo')",
+        "  ftb = depset(files)",
+        "  return [DefaultInfo(runfiles = ctx.runfiles(), files = ftb)]",
+        "",
+        "def output_func(name, public_attr, _private_attr):",
+        "  if _private_attr != None: return {}",
+        "  return {'o': name + '-' + public_attr + '.txt'}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'public_attr': attr.string(),",
+        "           '_private_attr': attr.label()},",
+        "  outputs = output_func)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', public_attr = 'bar')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(
+            ActionsTestUtil.baseArtifactNames(
+                target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("cr-bar.txt");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputFunctionDependingOnComputedAttribute() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  files = [ctx.outputs.o]",
+        "  ctx.actions.run_shell(",
+        "    outputs = files,",
+        "    command = 'echo')",
+        "  ftb = depset(files)",
+        "  return [DefaultInfo(runfiles = ctx.runfiles(), files = ftb)]",
+        "",
+        "def attr_func(public_attr):",
+        "  return public_attr",
+        "",
+        "def output_func(_private_attr):",
+        "  return {'o': _private_attr.name + '.txt'}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'public_attr': attr.label(),",
+        "           '_private_attr': attr.label(default = attr_func)},",
+        "  outputs = output_func)",
+        "",
+        "def empty_rule_impl(ctx):",
+        "  pass",
+        "",
+        "empty_rule = rule(implementation = empty_rule_impl)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule', 'empty_rule')",
+        "",
+        "empty_rule(name = 'foo')",
+        "custom_rule(name = 'cr', public_attr = '//test/starlark:foo')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("foo.txt");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputs() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  files = [ctx.outputs.lbl, ctx.outputs.list, ctx.outputs.str]",
+        "  print('==!=!=!=')",
+        "  print(files)",
+        "  ctx.actions.run_shell(",
+        "    outputs = files,",
+        "    command = 'echo')",
+        "  return [DefaultInfo(files = depset(files))]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "    'attr1': attr.label(allow_files=True),",
+        "    'attr2': attr.label_list(allow_files=True),",
+        "    'attr3': attr.string(),",
+        "  },",
+        "  outputs = {",
+        "    'lbl': '%{attr1}.a',",
+        "    'list': '%{attr2}.b',",
+        "    'str': '%{attr3}.c',",
+        "})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(",
+        "  name='cr',",
+        "  attr1='f1.txt',",
+        "  attr2=['f2.txt'],",
+        "  attr3='f3.txt',",
+        ")");
+
+    scratch.file("test/starlark/f1.txt");
+    scratch.file("test/starlark/f2.txt");
+    scratch.file("test/starlark/f3.txt");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("f1.a", "f2.b", "f3.txt.c");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputFunctionAndDefaultValue() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  ctx.actions.run_shell(",
+        "    outputs = [ctx.outputs.o],",
+        "    command = 'echo')",
+        "  return [DefaultInfo(runfiles = ctx.runfiles())]",
+        "",
+        "def output_func(attr1):",
+        "  return {'o': attr1 + '.txt'}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.string(default='bar')},",
+        "  outputs = output_func)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = None)");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("bar.txt");
+  }
+
+  @Test
+  public void testPrintProviderCollection() throws Exception {
+    scratch.file(
+        "test/starlark/rules.bzl",
+        "",
+        "FooInfo = provider()",
+        "BarInfo = provider()",
+        "",
+        "def _top_level_rule_impl(ctx):",
+        "  print('My Dep Providers:', ctx.attr.my_dep)",
+        "",
+        "def _dep_rule_impl(name):",
+        "  providers = [",
+        "      FooInfo(),",
+        "      BarInfo(),",
+        "  ]",
+        "  return providers",
+        "",
+        "top_level_rule = rule(",
+        "    implementation=_top_level_rule_impl,",
+        "    attrs={'my_dep':attr.label()}",
+        ")",
+        "",
+        "dep_rule = rule(",
+        "    implementation=_dep_rule_impl,",
+        ")");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:rules.bzl', 'top_level_rule', 'dep_rule')",
+        "",
+        "top_level_rule(name = 'tl', my_dep=':d')",
+        "",
+        "dep_rule(name = 'd')");
+
+    getConfiguredTarget("//test/starlark:tl");
+    assertContainsEvent(
+        "My Dep Providers: <target //test/starlark:d, keys:[FooInfo, BarInfo, OutputGroupInfo]>");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputFunctionPrints() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  print('implementation', ctx.label)",
+        "  files = [ctx.outputs.o]",
+        "  ctx.actions.run_shell(",
+        "    outputs = files,",
+        "    command = 'echo')",
+        "",
+        "def output_func(name):",
+        "  print('output function', name)",
+        "  return {'o': name + '.txt'}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  outputs = output_func)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+
+    getConfiguredTarget("//test/starlark:cr");
+    assertContainsEvent("output function cr");
+    assertContainsEvent("implementation //test/starlark:cr");
+  }
+
+  @Test
+  public void testNoOutputAttrDefault() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def custom_rule_impl(ctx):",
+        "  out_file = ctx.actions.declare_file(ctx.attr._o1.name)",
+        "  ctx.actions.write(output=out_file, content='hi')",
+        "  return [MyInfo(o1=ctx.attr._o1)]",
+        "",
+        "def output_fn():",
+        "  return Label('//test/starlark:foo.txt')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'_o1': attr.output(default = output_fn)})");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("got unexpected keyword argument 'default'");
+  }
+
+  @Test
+  public void testNoOutputListAttrDefault() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return []",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'outs': attr.output_list(default = [])})");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("got unexpected keyword argument 'default'");
+  }
+
+  @Test
+  public void testRuleClassNonMandatoryEmptyOutputs() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def custom_rule_impl(ctx):",
+        "  return [MyInfo(",
+        "      o1=ctx.outputs.o1,",
+        "      o2=ctx.outputs.o2)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'o1': attr.output(), 'o2': attr.output_list()})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+    StructImpl myInfo = getMyInfoFromTarget(target);
+    assertThat(myInfo.getValue("o1")).isEqualTo(Starlark.NONE);
+    assertThat(myInfo.getValue("o2")).isEqualTo(StarlarkList.empty());
+  }
+
+  @Test
+  public void testRuleClassImplicitAndExplicitOutputNamesCollide() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return []",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'o': attr.output_list()},",
+        "  outputs = {'o': '%{name}.txt'})");
+
+    checkError(
+        "test/starlark",
+        "cr",
+        "Multiple outputs with the same key: o",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', o = [':bar.txt'])");
+  }
+
+  @Test
+  public void testRuleClassDefaultFilesToBuild() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  files = [ctx.outputs.o]",
+        "  ctx.actions.run_shell(",
+        "    outputs = files,",
+        "    command = 'echo')",
+        "  ftb = depset(files)",
+        "  for i in ctx.outputs.out:",
+        "    ctx.actions.write(output=i, content='hi there')",
+        "",
+        "def output_func(attr1):",
+        "  return {'o': attr1 + '.txt'}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {",
+        "    'attr1': attr.string(),",
+        "    'out': attr.output_list()",
+        "  },",
+        "  outputs = output_func)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = 'bar', out=['other'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("bar.txt", "other")
+        .inOrder();
+  }
+
+  @Test
+  public void rulesReturningDeclaredProviders() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "my_provider = provider()",
+        "def _impl(ctx):",
+        "   return [my_provider(x = 1)]",
+        "my_rule = rule(_impl)"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':extension.bzl', 'my_rule')",
+        "my_rule(name = 'r')"
+    );
+
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:r");
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.create(configuredTarget.getLabel().getPackageIdentifier(), "extension.bzl"),
+            "my_provider");
+    StructImpl declaredProvider = (StructImpl) configuredTarget.get(key);
+    assertThat(declaredProvider).isNotNull();
+    assertThat(declaredProvider.getProvider().getKey()).isEqualTo(key);
+    assertThat(declaredProvider.getValue("x")).isEqualTo(1);
+  }
+
+  @Test
+  public void rulesReturningDeclaredProvidersCompatMode() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "my_provider = provider()",
+        "def _impl(ctx):",
+        "   return [my_provider(x = 1)]",
+        "my_rule = rule(_impl)");
+    scratch.file(
+        "test/BUILD",
+        "load(':extension.bzl', 'my_rule')",
+        "my_rule(name = 'r')"
+    );
+
+    ConfiguredTarget configuredTarget  = getConfiguredTarget("//test:r");
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.create(configuredTarget.getLabel().getPackageIdentifier(), "extension.bzl"),
+            "my_provider");
+    StructImpl declaredProvider = (StructImpl) configuredTarget.get(key);
+    assertThat(declaredProvider).isNotNull();
+    assertThat(declaredProvider.getProvider().getKey()).isEqualTo(key);
+    assertThat(declaredProvider.getValue("x")).isEqualTo(1);
+  }
+
+  @Test
+  public void testRuleReturningUnwrappedDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "my_provider = provider()",
+        "def _impl(ctx):",
+        "   return my_provider(x = 1)",
+        "my_rule = rule(_impl)"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':extension.bzl', 'my_rule')",
+        "my_rule(name = 'r')"
+    );
+
+    ConfiguredTarget configuredTarget  = getConfiguredTarget("//test:r");
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.create(configuredTarget.getLabel().getPackageIdentifier(), "extension.bzl"),
+            "my_provider");
+    StructImpl declaredProvider = (StructImpl) configuredTarget.get(key);
+    assertThat(declaredProvider).isNotNull();
+    assertThat(declaredProvider.getProvider().getKey()).isEqualTo(key);
+    assertThat(declaredProvider.getValue("x")).isEqualTo(1);
+  }
+
+  @Test
+  public void testConflictingProviderKeys_fromStruct_disallowed() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "my_provider = provider()",
+        "other_provider = provider()",
+        "def _impl(ctx):",
+        "   return [my_provider(x = 1), other_provider(), my_provider(x = 2)]",
+        "my_rule = rule(_impl)");
+
+    checkError(
+        "test",
+        "r",
+        "Multiple conflicting returned providers with key my_provider",
+        "load(':extension.bzl', 'my_rule')",
+        "my_rule(name = 'r')");
+  }
+
+  @Test
+  public void testConflictingProviderKeys_fromIterable_disallowed() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "my_provider = provider()",
+        "other_provider = provider()",
+        "def _impl(ctx):",
+        "   return [my_provider(x = 1), other_provider(), my_provider(x = 2)]",
+        "my_rule = rule(_impl)"
+    );
+
+    checkError(
+        "test",
+        "r",
+        "Multiple conflicting returned providers with key my_provider",
+        "load(':extension.bzl', 'my_rule')",
+        "my_rule(name = 'r')");
+  }
+
+  @Test
+  public void testRecursionDetection() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def _impl(ctx):",
+        "  _impl(ctx)",
+        "empty = rule(implementation = _impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'empty')",
+        "empty(name = 'test_target')");
+
+    getConfiguredTarget("//test/starlark:test_target");
+    assertContainsEvent("function '_impl' called recursively");
+  }
+
+  @Test
+  public void testBadCallbackFunction() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl", "def impl(): return 0", "", "custom_rule = rule(impl)");
+
+    checkError(
+        "test/starlark",
+        "cr",
+        "impl() does not accept positional arguments",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+  }
+
+  @Test
+  public void testRuleClassImplicitOutputFunctionBadAttr() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return None",
+        "",
+        "def output_func(bad_attr):",
+        "  return {'a': bad_attr}",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.string()},",
+        "  outputs = output_func)");
+
+    checkError(
+        "test/starlark",
+        "cr",
+        "Attribute 'bad_attr' either doesn't exist or uses a select()",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = 'bar')");
+  }
+
+  @Test
+  public void testHelperFunctionInRuleImplementation() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def helper_func(attr1):",
+        "  return depset(attr1)",
+        "",
+        "def custom_rule_impl(ctx):",
+        "  attr1 = ctx.files.attr1",
+        "  ftb = helper_func(attr1)",
+        "  return [DefaultInfo(runfiles = ctx.runfiles(), files = ftb)]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "  attrs = {'attr1': attr.label_list(mandatory=True, allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr', attr1 = [':a.txt'])");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:cr");
+
+    assertThat(target.getLabel().toString()).isEqualTo("//test/starlark:cr");
+    assertThat(
+        ActionsTestUtil.baseArtifactNames(
+            target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("a.txt");
+  }
+
+  @Test
+  public void testMultipleLoadsOfSameRule() throws Exception {
+    scratch.file("test/starlark/BUILD");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return None",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "     attrs = {'dep': attr.label_list(allow_files=True)})");
+
+    scratch.file(
+        "test/starlark1/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "custom_rule(name = 'cr1')");
+
+    scratch.file(
+        "test/starlark2/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "custom_rule(name = 'cr2', dep = ['//test/starlark1:cr1'])");
+
+    getConfiguredTarget("//test/starlark2:cr2");
+  }
+
+  @Test
+  public void testFunctionGeneratingRules() throws Exception {
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def impl(ctx): return None",
+        "def gen(): return rule(impl)",
+        "r = gen()",
+        "s = gen()");
+
+    scratch.file(
+        "test/starlark/BUILD", "load(':extension.bzl', 'r', 's')",
+        "r(name = 'r')", "s(name = 's')");
+
+    getConfiguredTarget("//test/starlark:r");
+    getConfiguredTarget("//test/starlark:s");
+  }
+
+  @Test
+  public void testLoadInStarlark() throws Exception {
+    scratch.file("test/starlark/implementation.bzl", "def custom_rule_impl(ctx):", "  return None");
+
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//test/starlark:implementation.bzl', 'custom_rule_impl')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "     attrs = {'dep': attr.label_list(allow_files=True)})");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "custom_rule(name = 'cr')");
+
+    getConfiguredTarget("//test/starlark:cr");
+  }
+
+  @Test
+  public void testRuleAliasing() throws Exception {
+    scratch.file(
+        "test/starlark/implementation.bzl",
+        "def impl(ctx): return []",
+        "custom_rule = rule(implementation = impl)");
+
+    scratch.file(
+        "test/starlark/ext.bzl",
+        "load('//test/starlark:implementation.bzl', 'custom_rule')",
+        "def impl(ctx): return []",
+        "custom_rule1 = rule(implementation = impl)",
+        "custom_rule2 = custom_rule1",
+        "custom_rule3 = custom_rule");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:ext.bzl', 'custom_rule1', 'custom_rule2', 'custom_rule3')",
+        "custom_rule4 = custom_rule3",
+        "custom_rule1(name = 'cr1')",
+        "custom_rule2(name = 'cr2')",
+        "custom_rule3(name = 'cr3')",
+        "custom_rule4(name = 'cr4')");
+
+    getConfiguredTarget("//test/starlark:cr1");
+    getConfiguredTarget("//test/starlark:cr2");
+    getConfiguredTarget("//test/starlark:cr3");
+    getConfiguredTarget("//test/starlark:cr4");
+  }
+
+  @Test
+  public void testRecursiveLoad() throws Exception {
+    scratch.file("test/starlark/ext2.bzl", "load('//test/starlark:ext1.bzl', 'symbol2')");
+
+    scratch.file("test/starlark/ext1.bzl", "load('//test/starlark:ext2.bzl', 'symbol1')");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:ext1.bzl', 'custom_rule')",
+        "genrule(name = 'rule')");
+
+    reporter.removeHandler(failFastHandler);
+    assertThrows(BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:rule"));
+    assertContainsEvent(
+        "cycle detected in extension files: \n"
+            + "    test/starlark/BUILD\n"
+            + ".-> //test/starlark:ext1.bzl\n"
+            + "|   //test/starlark:ext2.bzl\n"
+            + "`-- //test/starlark:ext1.bzl");
+  }
+
+  @Test
+  public void testRecursiveLoad2() throws Exception {
+    scratch.file("test/starlark/ext1.bzl", "load('//test/starlark:ext2.bzl', 'symbol2')");
+    scratch.file("test/starlark/ext2.bzl", "load('//test/starlark:ext3.bzl', 'symbol3')");
+    scratch.file("test/starlark/ext3.bzl", "load('//test/starlark:ext4.bzl', 'symbol4')");
+    scratch.file("test/starlark/ext4.bzl", "load('//test/starlark:ext2.bzl', 'symbol2')");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:ext1.bzl', 'custom_rule')",
+        "genrule(name = 'rule')");
+
+    reporter.removeHandler(failFastHandler);
+    assertThrows(BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:rule"));
+    assertContainsEvent(
+        "cycle detected in extension files: \n"
+            + "    test/starlark/BUILD\n"
+            + "    //test/starlark:ext1.bzl\n"
+            + ".-> //test/starlark:ext2.bzl\n"
+            + "|   //test/starlark:ext3.bzl\n"
+            + "|   //test/starlark:ext4.bzl\n"
+            + "`-- //test/starlark:ext2.bzl");
+  }
+
+  @Test
+  public void testLoadSymbolTypo() throws Exception {
+    scratch.file("test/starlark/ext1.bzl", "myvariable = 2");
+
+    scratch.file("test/starlark/BUILD", "load('//test/starlark:ext1.bzl', 'myvariables')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:test_target");
+    assertContainsEvent(
+        "file '//test/starlark:ext1.bzl' does not contain symbol 'myvariables' "
+            + "(did you mean 'myvariable'?)");
+  }
+
+  @Test
+  public void testOutputsObjectOrphanExecutableReportError() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   o = ctx.outputs.executable",
+        "   return [DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:xxx");
+    assertContainsEvent("ERROR /workspace/test/BUILD:2:8: in my_rule rule //test:xxx: ");
+    assertContainsEvent("The following files have no generating action:");
+    assertContainsEvent("test/xxx");
+  }
+
+  @Test
+  public void testCustomExecutableUsed() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   o = ctx.actions.declare_file('x.sh')",
+        "   ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+        "   return [DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+    Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+    assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+  }
+
+  @Test
+  public void testCustomAndDefaultExecutableReportsError() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   e = ctx.outputs.executable",
+        "   o = ctx.actions.declare_file('x.sh')",
+        "   ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+        "   return [DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:xxx");
+    assertContainsEvent("ERROR /workspace/test/BUILD:2:8: in my_rule rule //test:xxx: ");
+    assertContainsEvent(
+        "/workspace/test/rule.bzl:5:23: The rule 'my_rule' both accesses "
+            + "'ctx.outputs.executable' and provides a different executable 'test/x.sh'. "
+            + "Do not use 'ctx.output.executable'.");
+  }
+
+
+  @Test
+  public void testCustomExecutableStrNoEffect() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   o = ctx.actions.declare_file('x.sh')",
+        "   ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+        "   print(str(ctx.outputs))",
+        "   return [DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+    Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+    assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+  }
+
+  @Test
+  public void testCustomExecutableDirNoEffect() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   o = ctx.actions.declare_file('x.sh')",
+        "   ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+        "   print(dir(ctx.outputs))",
+        "   return [DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+    Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+    assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+  }
+
+  @Test
+  public void testOutputsObjectInDifferentRuleInaccessible() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "PInfo = provider(fields = ['outputs'])",
+        "def _impl(ctx):",
+        "   o = ctx.actions.declare_file('x.sh')",
+        "   ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+        "   return [PInfo(outputs = ctx.outputs), DefaultInfo(executable = o)]",
+        "my_rule = rule(_impl, executable = True)",
+        "def _dep_impl(ctx):",
+        "   o = ctx.attr.dep[PInfo].outputs.executable",
+        "   pass",
+        "my_dep_rule = rule(_dep_impl, attrs = { 'dep' : attr.label() })"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule', 'my_dep_rule')",
+        "my_rule(name = 'xxx')",
+        "my_dep_rule(name = 'yyy', dep = ':xxx')"
+    );
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:yyy");
+    assertContainsEvent("ERROR /workspace/test/BUILD:3:12: in my_dep_rule rule //test:yyy: ");
+    assertContainsEvent("File \"/workspace/test/rule.bzl\", line 8, in _dep_impl");
+    assertContainsEvent("ctx.attr.dep[PInfo].outputs.executable");
+    assertContainsEvent("cannot access outputs of rule '//test:xxx' outside "
+        + "of its own rule implementation function");
+  }
+
+  @Test
+  public void testOutputsObjectStringRepresentation() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "PInfo = provider(fields = ['outputs', 's'])",
+        "def _impl(ctx):",
+        "   ctx.actions.write(ctx.outputs.executable, 'echo Stuff', is_executable = True)",
+        "   ctx.actions.write(ctx.outputs.other, 'Other')",
+        "   return [PInfo(outputs = ctx.outputs, s = str(ctx.outputs))]",
+        "my_rule = rule(_impl, executable = True, outputs = { 'other' : '%{name}.other' })",
+        "def _dep_impl(ctx):",
+        "   return [PInfo(s = str(ctx.attr.dep[PInfo].outputs))]",
+        "my_dep_rule = rule(_dep_impl, attrs = { 'dep' : attr.label() })"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule', 'my_dep_rule')",
+        "my_rule(name = 'xxx')",
+        "my_dep_rule(name = 'yyy', dep = ':xxx')"
+    );
+
+    StarlarkProvider.Key pInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:rule.bzl", ImmutableMap.of()), "PInfo");
+
+    ConfiguredTarget targetXXX = getConfiguredTarget("//test:xxx");
+    StructImpl structXXX = (StructImpl) targetXXX.get(pInfoKey);
+
+    assertThat(structXXX.getValue("s"))
+        .isEqualTo(
+            "ctx.outputs(executable = <generated file test/xxx>, "
+                + "other = <generated file test/xxx.other>)");
+
+    ConfiguredTarget targetYYY = getConfiguredTarget("//test:yyy");
+    StructImpl structYYY = (StructImpl) targetYYY.get(pInfoKey);
+    assertThat(structYYY.getValue("s"))
+        .isEqualTo("ctx.outputs(for //test:xxx)");
+  }
+
+  @Test
+  public void testExecutableRuleWithNoExecutableReportsError() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "   pass",
+        "my_rule = rule(_impl, executable = True)"
+    );
+
+    scratch.file(
+        "test/BUILD",
+        "load(':rule.bzl', 'my_rule')",
+        "my_rule(name = 'xxx')"
+    );
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:xxx");
+    assertContainsEvent("ERROR /workspace/test/BUILD:2:8: in my_rule rule //test:xxx: ");
+    assertContainsEvent("/rule.bzl:1:5: The rule 'my_rule' is executable. "
+        + "It needs to create an executable File and pass it as the 'executable' "
+        + "parameter to the DefaultInfo it returns.");
+  }
+
+  @Test
+  public void testExecutableFromDifferentRuleIsForbidden() throws Exception {
+    scratch.file(
+        "pkg/BUILD",
+        "sh_binary(name = 'tryme',",
+        "          srcs = [':tryme.sh'],",
+        "          visibility = ['//visibility:public'],",
+        ")");
+
+    scratch.file(
+        "src/rulez.bzl",
+        "def  _impl(ctx):",
+        "   return [DefaultInfo(executable = ctx.executable.runme,",
+        "                       files = depset([ctx.executable.runme]),",
+        "          )]",
+        "r = rule(_impl,",
+        "         executable = True,",
+        "         attrs = {",
+        "            'runme' : attr.label(executable = True, mandatory = True, cfg = 'host'),",
+        "         }",
+        ")");
+
+    scratch.file(
+        "src/BUILD", "load(':rulez.bzl', 'r')", "r(name = 'r_tools', runme = '//pkg:tryme')");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//src:r_tools");
+    assertContainsEvent(
+        "/workspace/src/rulez.bzl:2:23: 'executable' provided by an executable"
+            + " rule 'r' should be created by the same rule.");
+  }
+
+  @Test
+  public void testFileAndDirectory() throws Exception {
+    scratch.file(
+        "ext.bzl",
+        "def _extrule(ctx):",
+        "  dir = ctx.actions.declare_directory('foo/bar/baz')",
+        "  ctx.actions.run_shell(",
+        "      outputs = [dir],",
+        "      command = 'mkdir -p ' + dir.path + ' && echo wtf > ' + dir.path + '/wtf.txt')",
+        "",
+        "extrule = rule(",
+        "    _extrule,",
+        "    outputs = {",
+        "      'out': 'foo/bar/baz',",
+        "    },",
+        ")");
+    scratch.file(
+        "BUILD", //
+        "load(':ext.bzl', 'extrule')",
+        "",
+        "extrule(",
+        "    name = 'test'",
+        ")");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//:test");
+    assertContainsEvent("ERROR /workspace/BUILD:3:8: in extrule rule //:test:");
+    assertContainsEvent("he following directories were also declared as files:");
+    assertContainsEvent("foo/bar/baz");
+  }
+
+  @Test
+  public void testEnvironmentConstraintsFromStarlarkRule() throws Exception {
+    scratch.file(
+        "buildenv/foo/BUILD",
+        "environment_group(name = 'env_group',",
+        "    defaults = [':default'],",
+        "    environments = ['default', 'other'])",
+        "environment(name = 'default')",
+        "environment(name = 'other')");
+    // The example Starlark rule explicitly provides the MyProvider provider as a regression test
+    // for a bug where a Starlark rule with unsatisfied constraints but explicit providers would
+    // result in Bazel throwing a null pointer exception.
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "MyProvider = provider()",
+        "",
+        "def _impl(ctx):",
+        "  return [MyProvider(foo = 'bar')]",
+        "my_rule = rule(implementation = _impl,",
+        "    attrs = { 'deps' : attr.label_list() },",
+        "    provides = [MyProvider])");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl',  'my_rule')",
+        "java_library(name = 'dep', srcs = ['a.java'], restricted_to = ['//buildenv/foo:other'])",
+        "my_rule(name='my', deps = [':dep'])");
+
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test/starlark:my")).isNull();
+    assertContainsEvent(
+        "//test/starlark:dep doesn't support expected environment: //buildenv/foo:default");
+  }
+
+  @Test
+  public void testAnalysisFailureInfo() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "   fail('This Is My Failure Message')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'r')");
+
+    useConfiguration("--allow_analysis_failures=true");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:r");
+    AnalysisFailureInfo info =
+        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
+    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
+    assertThat(failure.getMessage()).contains("This Is My Failure Message");
+    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
+  }
+
+  @Test
+  public void testAnalysisFailureInfo_forTest() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "   fail('This Is My Failure Message')",
+        "",
+        "custom_test = rule(implementation = custom_rule_impl,",
+        "    test = True)");
+
+    scratch.file(
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
+
+    useConfiguration("--allow_analysis_failures=true");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:r");
+    AnalysisFailureInfo info =
+        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
+    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
+    assertThat(failure.getMessage()).contains("This Is My Failure Message");
+    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
+  }
+
+  @Test
+  public void testAnalysisFailureInfoWithOutput() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "   fail('This Is My Failure Message')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl,",
+        "    outputs = {'my_output': '%{name}.txt'})");
+
+    scratch.file(
+        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");
+
+    useConfiguration("--allow_analysis_failures=true");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:r");
+    AnalysisFailureInfo info =
+        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
+    AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
+    assertThat(failure.getMessage()).contains("This Is My Failure Message");
+    assertThat(failure.getLabel()).isEqualTo(Label.parseAbsoluteUnchecked("//test:r"));
+  }
+
+  @Test
+  public void testTransitiveAnalysisFailureInfo() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "   fail('This Is My Failure Message')",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)",
+        "",
+        "def depending_rule_impl(ctx):",
+        "   return []",
+        "",
+        "depending_rule = rule(implementation = depending_rule_impl,",
+        "     attrs = {'deps' : attr.label_list()})");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule', 'depending_rule')",
+        "",
+        "custom_rule(name = 'one')",
+        "custom_rule(name = 'two')",
+        "depending_rule(name = 'failures_are_direct_deps',",
+        "    deps = [':one', ':two'])",
+        "depending_rule(name = 'failures_are_indirect_deps',",
+        "    deps = [':failures_are_direct_deps'])");
+
+    useConfiguration("--allow_analysis_failures=true");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:failures_are_indirect_deps");
+    AnalysisFailureInfo info =
+        (AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
+
+    Correspondence<AnalysisFailure, AnalysisFailure> correspondence =
+        Correspondence.from(
+            (actual, expected) ->
+                actual.getLabel().equals(expected.getLabel())
+                    && actual.getMessage().contains(expected.getMessage()),
+            "is equivalent to");
+
+    AnalysisFailure expectedOne =
+        new AnalysisFailure(
+            Label.parseAbsoluteUnchecked("//test:one"), "This Is My Failure Message");
+    AnalysisFailure expectedTwo =
+        new AnalysisFailure(
+            Label.parseAbsoluteUnchecked("//test:two"), "This Is My Failure Message");
+
+    assertThat(info.getCauses().getSet(AnalysisFailure.class).toList())
+        .comparingElementsUsing(correspondence)
+        .containsExactly(expectedOne, expectedTwo);
+  }
+
+  @Test
+  public void testTestResultInfo() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'r')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:r");
+    AnalysisTestResultInfo info =
+        (AnalysisTestResultInfo) target.get(AnalysisTestResultInfo.STARLARK_CONSTRUCTOR.getKey());
+    assertThat(info.getSuccess()).isTrue();
+    assertThat(info.getMessage()).isEqualTo("message contents");
+  }
+
+  @Test
+  public void testAnalysisTestRuleWithActionRegistration() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  out_file = ctx.actions.declare_file('file.txt')",
+        "  ctx.actions.write(output=out_file, content='hi')",
+        "",
+        "custom_test = rule(implementation = custom_rule_impl, analysis_test = True)");
+
+    scratch.file(
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent(
+        "implementation function of a rule with analysis_test=true may not register actions");
+  }
+
+  @Test
+  public void testAnalysisTestRuleWithFlag() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "",
+        "custom_test = rule(implementation = custom_rule_impl, analysis_test = True)");
+
+    scratch.file(
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test:r");
+    AnalysisTestResultInfo info =
+        (AnalysisTestResultInfo) target.get(AnalysisTestResultInfo.STARLARK_CONSTRUCTOR.getKey());
+    assertThat(info.getSuccess()).isTrue();
+    assertThat(info.getMessage()).isEqualTo("message contents");
+
+    // TODO(cparsons): Verify implicit action registration via AnalysisTestResultInfo.
+  }
+
+  @Test
+  public void testAnalysisTestTransitionOnAnalysisTest() throws Exception {
+    useConfiguration("--copt=yeehaw");
+
+    scratch.file(
+        "test/extension.bzl",
+        "MyInfo = provider()",
+        "MyDep = provider()",
+        "",
+        "def outer_rule_impl(ctx):",
+        "  return [MyInfo(copts = ctx.fragments.cpp.copts),",
+        "          MyDep(info = ctx.attr.dep[0][MyInfo]),",
+        "          AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def inner_rule_impl(ctx):",
+        "  return [MyInfo(copts = ctx.fragments.cpp.copts)]",
+        "",
+        "my_transition = analysis_test_transition(",
+        "    settings = {",
+        "        '//command_line_option:copt' : ['cowabunga'] }",
+        ")",
+        "inner_rule = rule(implementation = inner_rule_impl,",
+        "                  fragments = ['cpp'])",
+        "outer_rule_test = rule(",
+        "  implementation = outer_rule_impl,",
+        "  fragments = ['cpp'],",
+        "  analysis_test = True,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'inner_rule', 'outer_rule_test')",
+        "",
+        "inner_rule(name = 'inner')",
+        "outer_rule_test(name = 'r', dep = ':inner')");
+
+    StarlarkProvider.Key myInfoKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:extension.bzl", ImmutableMap.of()), "MyInfo");
+    StarlarkProvider.Key myDepKey =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//test:extension.bzl", ImmutableMap.of()), "MyDep");
+
+    ConfiguredTarget outerTarget = getConfiguredTarget("//test:r");
+    StructImpl outerInfo = (StructImpl) outerTarget.get(myInfoKey);
+    StructImpl outerDepInfo = (StructImpl) outerTarget.get(myDepKey);
+    StructImpl innerInfo = (StructImpl) outerDepInfo.getValue("info");
+
+    assertThat((Sequence) outerInfo.getValue("copts")).containsExactly("yeehaw");
+    assertThat((Sequence) innerInfo.getValue("copts")).containsExactly("cowabunga");
+  }
+
+  @Test
+  public void testAnalysisTestTransitionOnNonAnalysisTest() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return []",
+        "my_transition = analysis_test_transition(",
+        "    settings = {",
+        "        '//command_line_option:test_arg' : ['yeehaw'] }",
+        ")",
+        "",
+        "custom_rule = rule(",
+        "  implementation = custom_rule_impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent(
+        "Only rule definitions with analysis_test=True may have attributes "
+            + "with analysis_test_transition transitions");
+  }
+
+  @Test
+  public void testBuildSettingRule_flag() throws Exception {
+    scratch.file("test/rules.bzl",
+        "def _impl(ctx): return None",
+        "build_setting_rule = rule(_impl, build_setting = config.string(flag=True))");
+    scratch.file("test/BUILD",
+    "load('//test:rules.bzl', 'build_setting_rule')",
+    "build_setting_rule(name = 'my_build_setting', build_setting_default = 'default')");
+
+    BuildSetting buildSetting =
+        getTarget("//test:my_build_setting")
+            .getAssociatedRule()
+            .getRuleClassObject()
+            .getBuildSetting();
+
+    assertThat(buildSetting.getType()).isEqualTo(Type.STRING);
+    assertThat(buildSetting.isFlag()).isTrue();
+  }
+
+  @Test
+  public void testBuildSettingRule_settingByDefault() throws Exception {
+    scratch.file("test/rules.bzl",
+        "def _impl(ctx): return None",
+        "build_setting_rule = rule(_impl, build_setting = config.string())");
+    scratch.file("test/BUILD",
+        "load('//test:rules.bzl', 'build_setting_rule')",
+        "build_setting_rule(name = 'my_build_setting', build_setting_default = 'default')");
+
+    BuildSetting buildSetting =
+        getTarget("//test:my_build_setting")
+            .getAssociatedRule()
+            .getRuleClassObject()
+            .getBuildSetting();
+
+    assertThat(buildSetting.getType()).isEqualTo(Type.STRING);
+    assertThat(buildSetting.isFlag()).isFalse();
+  }
+
+  @Test
+  public void testBuildSettingRule_settingByFlagParameter() throws Exception {
+    scratch.file("test/rules.bzl",
+        "def _impl(ctx): return None",
+        "build_setting_rule = rule(_impl, build_setting = config.string(flag=False))");
+    scratch.file("test/BUILD",
+        "load('//test:rules.bzl', 'build_setting_rule')",
+        "build_setting_rule(name = 'my_build_setting', build_setting_default = 'default')");
+
+    BuildSetting buildSetting =
+        getTarget("//test:my_build_setting")
+            .getAssociatedRule()
+            .getRuleClassObject()
+            .getBuildSetting();
+
+    assertThat(buildSetting.getType()).isEqualTo(Type.STRING);
+    assertThat(buildSetting.isFlag()).isFalse();
+  }
+
+
+  @Test
+  public void testBuildSettingRule_noDefault() throws Exception {
+    scratch.file("test/rules.bzl",
+        "def _impl(ctx): return None",
+        "build_setting_rule = rule(_impl, build_setting = config.string())");
+    scratch.file("test/BUILD",
+        "load('//test:rules.bzl', 'build_setting_rule')",
+        "build_setting_rule(name = 'my_build_setting')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_build_setting");
+    assertContainsEvent("missing value for mandatory attribute "
+        + "'build_setting_default' in 'build_setting_rule' rule");
+
+  }
+
+  @Test
+  public void testAnalysisTestCannotDependOnAnalysisTest() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "",
+        "def analysis_test_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def middle_rule_impl(ctx):",
+        "  return []",
+        "def inner_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "",
+        "my_transition = analysis_test_transition(",
+        "    settings = {",
+        "        '//command_line_option:test_arg' : ['yeehaw'] }",
+        ")",
+        "",
+        "inner_rule_test = rule(",
+        "  implementation = analysis_test_rule_impl,",
+        "  analysis_test = True,",
+        ")",
+        "middle_rule = rule(",
+        "  implementation = middle_rule_impl,",
+        "  attrs = {'dep':  attr.label()}",
+        ")",
+        "outer_rule_test = rule(",
+        "  implementation = analysis_test_rule_impl,",
+        "  analysis_test = True,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'outer_rule_test', 'middle_rule', 'inner_rule_test')",
+        "",
+        "outer_rule_test(name = 'outer', dep = ':middle')",
+        "middle_rule(name = 'middle', dep = ':inner')",
+        "inner_rule_test(name = 'inner')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:outer");
+    assertContainsEvent(
+        "analysis_test rule '//test:inner' cannot be transitively "
+            + "depended on by another analysis test rule");
+  }
+
+  @Test
+  public void testAnalysisTestOverDepsLimit() throws Exception {
+    setupAnalysisTestDepsLimitTest(10, 12, true);
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent(
+        "analysis test rule excedeed maximum dependency edge count. " + "Count: 14. Limit is 10.");
+  }
+
+  @Test
+  public void testAnalysisTestUnderDepsLimit() throws Exception {
+    setupAnalysisTestDepsLimitTest(10, 8, true);
+
+    assertThat(getConfiguredTarget("//test:r")).isNotNull();
+  }
+
+  @Test
+  public void testAnalysisDepsLimitOnlyForTransition() throws Exception {
+    setupAnalysisTestDepsLimitTest(3, 10, false);
+
+    assertThat(getConfiguredTarget("//test:r")).isNotNull();
+  }
+
+  private void setupAnalysisTestDepsLimitTest(
+      int limit, int dependencyChainSize, boolean useTransition) throws Exception {
+    Preconditions.checkArgument(dependencyChainSize > 2);
+    useConfiguration("--analysis_testing_deps_limit=" + limit);
+
+    String transitionDefinition;
+    if (useTransition) {
+      transitionDefinition =
+          "my_transition = analysis_test_transition("
+              + "settings = {'//command_line_option:test_arg' : ['yeehaw'] })";
+    } else {
+      transitionDefinition = "my_transition = None";
+    }
+
+    scratch.file(
+        "test/extension.bzl",
+        "",
+        "def outer_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def dep_rule_impl(ctx):",
+        "  return []",
+        "",
+        transitionDefinition,
+        "",
+        "dep_rule = rule(",
+        "  implementation = dep_rule_impl,",
+        "  attrs = {'deps': attr.label_list()}",
+        ")",
+        "outer_rule_test = rule(",
+        "  implementation = outer_rule_impl,",
+        "  fragments = ['java'],",
+        "  analysis_test = True,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    // Create a chain of targets where 'innerN' depends on 'inner{N+1}' until the max length.
+    StringBuilder dependingRulesChain = new StringBuilder();
+    for (int i = 0; i < dependencyChainSize - 1; i++) {
+      // Each dep_rule target also depends on the leaf.
+      // The leaf should not be counted multiple times.
+      dependingRulesChain.append(
+          String.format(
+              "dep_rule(name = 'inner%s', deps = [':inner%s', ':inner%s'])\n",
+              i, (i + 1), dependencyChainSize));
+    }
+    dependingRulesChain.append(
+        String.format(
+            "dep_rule(name = 'inner%s', deps = [':inner%s'])\n",
+            dependencyChainSize - 1, dependencyChainSize));
+    dependingRulesChain.append(String.format("dep_rule(name = 'inner%s')", dependencyChainSize));
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'dep_rule', 'outer_rule_test')",
+        "",
+        "outer_rule_test(name = 'r', dep = ':inner0')",
+        dependingRulesChain.toString());
+  }
+
+  @Test
+  public void testBadAllowlistTransition_onNonLabelAttr() throws Exception {
+    String allowlistAttributeName =
+        FunctionSplitTransitionAllowlist.ATTRIBUTE_NAME.replace("$", "_");
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "    return []",
+        "",
+        "my_rule = rule(_impl, attrs = {'"
+            + allowlistAttributeName
+            + "':attr.string(default = 'blah')})");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'my_rule')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_rule");
+    assertContainsEvent("_allowlist_function_transition attribute must be a label type");
+  }
+
+  @Test
+  public void testBadAllowlistTransition_noDefaultValue() throws Exception {
+    String allowlistAttributeName =
+        FunctionSplitTransitionAllowlist.ATTRIBUTE_NAME.replace("$", "_");
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "    return []",
+        "",
+        "my_rule = rule(_impl, attrs = {'" + allowlistAttributeName + "':attr.label()})");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'my_rule')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_rule");
+    assertContainsEvent("_allowlist_function_transition attribute must have a default value");
+  }
+
+  @Test
+  public void testBadAllowlistTransition_wrongDefaultValue() throws Exception {
+    String allowlistAttributeName =
+        FunctionSplitTransitionAllowlist.ATTRIBUTE_NAME.replace("$", "_");
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "    return []",
+        "",
+        "my_rule = rule(_impl, attrs = {'"
+            + allowlistAttributeName
+            + "':attr.label(default = Label('//test:my_other_rule'))})");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule')",
+        "my_rule(name = 'my_rule')",
+        "my_rule(name = 'my_other_rule')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_rule");
+    assertContainsEvent(
+        " _allowlist_function_transition attribute (//test:my_other_rule) does not have the"
+            + " expected value");
+  }
+
+  @Test
+  public void testBadAnalysisTestRule_notAnalysisTest() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "",
+        "def outer_rule_impl(ctx):",
+        "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
+        "def dep_rule_impl(ctx):",
+        "  return []",
+        "",
+        "my_transition = analysis_test_transition(",
+        "    settings = {",
+        "        '//command_line_option:test_arg' : ['yeehaw'] }",
+        ")",
+        "dep_rule = rule(",
+        "  implementation = dep_rule_impl,",
+        "  attrs = {'dep':  attr.label()}",
+        ")",
+        "outer_rule = rule(",
+        "  implementation = outer_rule_impl,",
+        "# analysis_test = True,",
+        "  fragments = ['java'],",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "  })");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'dep_rule', 'outer_rule_test')",
+        "",
+        "outer_rule(name = 'r', dep = ':inner')",
+        "dep_rule(name = 'inner')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:outer_rule");
+    assertContainsEvent(
+        "Only rule definitions with analysis_test=True may have attributes with "
+            + "analysis_test_transition transitions");
+  }
+
+  @Test
+  public void testBadAllowlistTransition_noAllowlist() throws Exception {
+    scratch.file(
+        "tools/allowlists/function_transition_allowlist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_allowlist',",
+        "    packages = [",
+        "        '//test/...',",
+        "    ],",
+        ")");
+    scratch.file(
+        "test/rules.bzl",
+        "def transition_func(settings):",
+        "  return {'t0': {'//command_line_option:cpu': 'k8'}}",
+        "my_transition = transition(implementation = transition_func, inputs = [],",
+        "  outputs = ['//command_line_option:cpu'])",
+        "def _my_rule_impl(ctx): ",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "#   '_allowlist_function_transition': attr.label(",
+        "#       default = '//tools/allowlists/function_transition_allowlist',",
+        "#   ),",
+        "  })",
+        "def _simple_rule_impl(ctx):",
+        "  return []",
+        "simple_rule = rule(_simple_rule_impl)");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule', 'simple_rule')",
+        "my_rule(name = 'my_rule', dep = ':dep')",
+        "simple_rule(name = 'dep')");
+    setStarlarkSemanticsOptions("--experimental_starlark_config_transitions");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_rule");
+    assertContainsEvent("Use of Starlark transition without allowlist");
+  }
+
+  @Test
+  public void testPrintFromTransitionImpl() throws Exception {
+    setStarlarkSemanticsOptions("--experimental_starlark_config_transitions");
+    scratch.file(
+        "tools/allowlists/function_transition_allowlist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_allowlist',",
+        "    packages = [",
+        "        '//test/...',",
+        "    ],",
+        ")");
+    scratch.file(
+        "test/rules.bzl",
+        "def _transition_impl(settings, attr):",
+        "  print('printing from transition impl', settings['//command_line_option:test_arg'])",
+        "  return {'//command_line_option:test_arg': "
+            + "settings['//command_line_option:test_arg']+['meow']}",
+        "my_transition = transition(",
+        "  implementation = _transition_impl,",
+        "  inputs = ['//command_line_option:test_arg'],",
+        "  outputs = ['//command_line_option:test_arg'],",
+        ")",
+        "def _rule_impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _rule_impl,",
+        "  cfg = my_transition,",
+        "  attrs = {",
+        "    'dep': attr.label(cfg = my_transition),",
+        "    '_allowlist_function_transition': attr.label(",
+        "        default = '//tools/allowlists/function_transition_allowlist',",
+        "    ),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule')",
+        "my_rule(name = 'test', dep = ':dep')",
+        "my_rule(name = 'dep')");
+
+    useConfiguration("--test_arg=meow");
+
+    getConfiguredTarget("//test");
+    // Test print from top level transition
+    assertContainsEvent("printing from transition impl [\"meow\"]");
+    // Test print from dep transition
+    assertContainsEvent("printing from transition impl [\"meow\", \"meow\"]");
+    // Test print from (non-top level) rule class transition
+    assertContainsEvent("printing from transition impl [\"meow\", \"meow\", \"meow\"]");
+  }
+
+  @Test
+  public void testTransitionEquality() throws Exception {
+    setStarlarkSemanticsOptions("--experimental_starlark_config_transitions");
+    scratch.file(
+        "tools/allowlists/function_transition_allowlist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_allowlist',",
+        "    packages = [",
+        "        '//test/...',",
+        "    ],",
+        ")");
+    scratch.file(
+        "test/rules.bzl",
+        "def _transition_impl(settings, attr):",
+        "  return {'//command_line_option:test_arg': ['meow']}",
+        "my_transition = transition(",
+        "  implementation = _transition_impl,",
+        "  inputs = [],",
+        "  outputs = ['//command_line_option:test_arg'],",
+        ")",
+        "def _rule_impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _rule_impl,",
+        "  cfg = my_transition,",
+        "  attrs = {",
+        "    'dep': attr.label(cfg = my_transition),",
+        "    '_allowlist_function_transition': attr.label(",
+        "        default = '//tools/allowlists/function_transition_allowlist',",
+        "    ),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule')",
+        "my_rule(name = 'test', dep = ':dep')",
+        "my_rule(name = 'dep')");
+
+    useConfiguration("--test_arg=meow");
+
+    StarlarkDefinedConfigTransition ruleTransition =
+        ((StarlarkAttributeTransitionProvider)
+                getTarget("//test")
+                    .getAssociatedRule()
+                    .getRuleClassObject()
+                    .getAttributeByName("dep")
+                    .getTransitionFactory())
+            .getStarlarkDefinedConfigTransitionForTesting();
+
+    StarlarkDefinedConfigTransition attrTransition =
+        ((StarlarkRuleTransitionProvider)
+                getTarget("//test").getAssociatedRule().getRuleClassObject().getTransitionFactory())
+            .getStarlarkDefinedConfigTransitionForTesting();
+
+    assertThat(ruleTransition).isEqualTo(attrTransition);
+    assertThat(attrTransition).isEqualTo(ruleTransition);
+    assertThat(ruleTransition.hashCode()).isEqualTo(attrTransition.hashCode());
+  }
+
+  @Test
+  public void testBadAllowlistTransition_allowlistNoCfg() throws Exception {
+    scratch.file(
+        "tools/allowlists/function_transition_allowlist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_allowlist',",
+        "    packages = [",
+        "        '//test/...',",
+        "    ],",
+        ")");
+    scratch.file(
+        "test/rules.bzl",
+        "def _my_rule_impl(ctx): ",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "#   'dep':  attr.label(cfg = my_transition),",
+        "    '_allowlist_function_transition': attr.label(",
+        "        default = '//tools/allowlists/function_transition_allowlist',",
+        "    ),",
+        "  })",
+        "def _simple_rule_impl(ctx):",
+        "  return []",
+        "simple_rule = rule(_simple_rule_impl)");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule', 'simple_rule')",
+        "my_rule(name = 'my_rule', dep = ':dep')",
+        "simple_rule(name = 'dep')");
+    setStarlarkSemanticsOptions("--experimental_starlark_config_transitions");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_rule");
+    assertContainsEvent("Unused function-based split transition allowlist");
+  }
+
+  @Test
+  public void testLicenseType() throws Exception {
+    // Note that attr.license is deprecated, and thus this test is subject to imminent removal.
+    // (See --incompatible_no_attr_license). However, this verifies that until the attribute
+    // is removed, values of the attribute are a valid Starlark type.
+    setStarlarkSemanticsOptions("--incompatible_no_attr_license=false");
+    scratch.file(
+        "test/rule.bzl",
+        "def _my_rule_impl(ctx): ",
+        "  print(ctx.attr.my_license)",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "    'my_license':  attr.license(),",
+        "  })");
+    scratch.file("test/BUILD", "load(':rule.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+    getConfiguredTarget("//test:test");
+
+    assertContainsEvent("[none]");
+  }
+
+  @Test
+  public void testNativeModuleFields() throws Exception {
+    // Check that
+    scratch.file(
+        "test/file.bzl",
+        "def valid(s):",
+        "    if not s[0].isalpha(): return False",
+        "    for c in s.elems():",
+        "        if not (c.isalpha() or c == '_' or c.isdigit()): return False",
+        "    return True",
+        "",
+        "bad_names = [name for name in dir(native) if not valid(name)]",
+        "print('bad_names =', bad_names)");
+    scratch.file("test/BUILD", "load('//test:file.bzl', 'bad_names')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:anything");
+    assertContainsEvent("bad_names = []");
+  }
+
+  @Test
+  public void testDisallowStructProviderSyntax() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=true");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct()",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'cr')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:cr");
+    assertContainsEvent(
+        "Returning a struct from a rule implementation function is deprecated and will be "
+            + "removed soon. It may be temporarily re-enabled by setting "
+            + "--incompatible_disallow_struct_provider_syntax=false");
+  }
+
+  @Test
+  public void testDisableTargetProviderFields() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disable_target_provider_fields=true");
+    scratch.file(
+        "test/starlark/rule.bzl",
+        "MyProvider = provider()",
+        "",
+        "def _my_rule_impl(ctx): ",
+        "  print(ctx.attr.dep.my_info)",
+        "def _dep_rule_impl(ctx): ",
+        "  my_info = MyProvider(foo = 'bar')",
+        "  return struct(my_info = my_info, providers = [my_info])",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(),",
+        "  })",
+        "dep_rule = rule(implementation = _dep_rule_impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load(':rule.bzl', 'my_rule', 'dep_rule')",
+        "",
+        "my_rule(name = 'r', dep = ':d')",
+        "dep_rule(name = 'd')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:r");
+    assertContainsEvent(
+        "Accessing providers via the field syntax on structs is deprecated and will be removed "
+            + "soon. It may be temporarily re-enabled by setting "
+            + "--incompatible_disable_target_provider_fields=false. "
+            + "See https://github.com/bazelbuild/bazel/issues/9014 for details.");
+  }
+
+  // Verifies that non-provider fields on the 'target' type are still available even with
+  // --incompatible_disable_target_provider_fields.
+  @Test
+  public void testDisableTargetProviderFields_actionsField() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disable_target_provider_fields=true");
+    scratch.file(
+        "test/starlark/rule.bzl",
+        "MyProvider = provider()",
+        "",
+        "def _my_rule_impl(ctx): ",
+        "  print(ctx.attr.dep.actions)",
+        "def _dep_rule_impl(ctx): ",
+        "  my_info = MyProvider(foo = 'bar')",
+        "  return struct(my_info = my_info, providers = [my_info])",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(),",
+        "  })",
+        "dep_rule = rule(implementation = _dep_rule_impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load(':rule.bzl', 'my_rule', 'dep_rule')",
+        "",
+        "my_rule(name = 'r', dep = ':d')",
+        "dep_rule(name = 'd')");
+
+    assertThat(getConfiguredTarget("//test/starlark:r")).isNotNull();
+  }
+
+  @Test
+  public void testDisableTargetProviderFields_disabled() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disable_target_provider_fields=false");
+    scratch.file(
+        "test/starlark/rule.bzl",
+        "MyProvider = provider()",
+        "",
+        "def _my_rule_impl(ctx): ",
+        "  print(ctx.attr.dep.my_info)",
+        "def _dep_rule_impl(ctx): ",
+        "  my_info = MyProvider(foo = 'bar')",
+        "  return struct(my_info = my_info, providers = [my_info])",
+        "my_rule = rule(",
+        "  implementation = _my_rule_impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(),",
+        "  })",
+        "dep_rule = rule(implementation = _dep_rule_impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load(':rule.bzl', 'my_rule', 'dep_rule')",
+        "",
+        "my_rule(name = 'r', dep = ':d')",
+        "dep_rule(name = 'd')");
+
+    assertThat(getConfiguredTarget("//test/starlark:r")).isNotNull();
+  }
+
+  @Test
+  public void testNoRuleOutputsParam() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_no_rule_outputs_param=true");
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _impl(ctx):",
+        "  output = ctx.outputs.out",
+        "  ctx.actions.write(output = output, content = 'hello')",
+        "",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  outputs = {\"out\": \"%{name}.txt\"})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:target");
+    assertContainsEvent(
+        "parameter 'outputs' is deprecated and will be removed soon. It may be temporarily "
+            + "re-enabled by setting --incompatible_no_rule_outputs_param=false");
+  }
+
+  @Test
+  public void testExecutableNotInRunfiles() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_file('exe')",
+        "  ctx.actions.run_shell(outputs=[exe], command='touch exe')",
+        "  runfile = ctx.actions.declare_file('rrr')",
+        "  ctx.actions.run_shell(outputs=[runfile], command='touch rrr')",
+        "  return struct(executable = exe, default_runfiles = ctx.runfiles(files = [runfile]))",
+        "my_rule = rule(implementation = _my_rule_impl, executable = True)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:target");
+    assertContainsEvent("exe not included in runfiles");
+  }
+
+  @Test
+  public void testCommandStringList() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_run_shell_command_string");
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_file('exe')",
+        "  ctx.actions.run_shell(outputs=[exe], command=['touch', 'exe'])",
+        "  return []",
+        "my_rule = rule(implementation = _my_rule_impl)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:target");
+    assertContainsEvent("'command' must be of type string");
+  }
+
+  /** Starlark integration test that forces inlining. */
+  @RunWith(JUnit4.class)
+  public static class StarlarkIntegrationTestsWithInlineCalls extends StarlarkIntegrationTest {
+
+    @Override
+    protected boolean usesInliningBzlLoadFunction() {
+      return true;
+    }
+
+    @Override
+    @Test
+    public void testRecursiveLoad() throws Exception {
+      scratch.file("test/starlark/ext2.bzl", "load('//test/starlark:ext1.bzl', 'symbol2')");
+
+      scratch.file("test/starlark/ext1.bzl", "load('//test/starlark:ext2.bzl', 'symbol1')");
+
+      scratch.file(
+          "test/starlark/BUILD",
+          "load('//test/starlark:ext1.bzl', 'custom_rule')",
+          "genrule(name = 'rule')");
+
+      reporter.removeHandler(failFastHandler);
+      BuildFileContainsErrorsException e =
+          assertThrows(
+              BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:rule"));
+      assertThat(e)
+          .hasMessageThat()
+          .contains("Starlark load cycle: [//test/starlark:ext1.bzl, //test/starlark:ext2.bzl]");
+    }
+
+    @Override
+    @Test
+    public void testRecursiveLoad2() throws Exception {
+      scratch.file("test/starlark/ext1.bzl", "load('//test/starlark:ext2.bzl', 'symbol2')");
+      scratch.file("test/starlark/ext2.bzl", "load('//test/starlark:ext3.bzl', 'symbol3')");
+      scratch.file("test/starlark/ext3.bzl", "load('//test/starlark:ext4.bzl', 'symbol4')");
+      scratch.file("test/starlark/ext4.bzl", "load('//test/starlark:ext2.bzl', 'symbol2')");
+
+      scratch.file(
+          "test/starlark/BUILD",
+          "load('//test/starlark:ext1.bzl', 'custom_rule')",
+          "genrule(name = 'rule')");
+
+      reporter.removeHandler(failFastHandler);
+      BuildFileContainsErrorsException e =
+          assertThrows(
+              BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:rule"));
+      assertThat(e)
+          .hasMessageThat()
+          .contains(
+              "Starlark load cycle: [//test/starlark:ext2.bzl, "
+                  + "//test/starlark:ext3.bzl, //test/starlark:ext4.bzl]");
+    }
+  }
+
+  @Test
+  public void testUnhashableInDictForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "y = [] in {}");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("unhashable type: 'list'");
+  }
+
+  @Test
+  public void testDictGetUnhashableForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "y = {}.get({})");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("unhashable type: 'dict'");
+  }
+
+  @Test
+  public void testUnknownStringEscapesForbidden() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_restrict_string_escapes=true");
+
+    scratch.file("test/extension.bzl", "y = \"\\z\"");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("invalid escape sequence: \\z");
+  }
+
+  @Test
+  public void testUnknownStringEscapes() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_restrict_string_escapes=false");
+
+    scratch.file("test/extension.bzl", "y = \"\\z\"");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    getConfiguredTarget("//test:r");
+  }
+
+  @Test
+  public void testSplitEmptySeparatorForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "y = 'abc'.split('')");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("Empty separator");
+  }
+
+  @Test
+  public void testIdentifierAssignmentFromOuterScope2() throws Exception {
+    scratch.file(
+        "test/extension.bzl",
+        "a = [1, 2, 3]",
+        "def f(): a[0] = 9",
+        "y = f()",
+        "fail() if a[0] != 9 else None");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    getConfiguredTarget("//test:r");
+  }
+
+  @Test
+  public void testIdentifierAssignmentFromOuterScopeForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "a = []", "def f(): a += [1]", "y = f()");
+
+    scratch.file("test/BUILD", "load('//test:extension.bzl', 'y')", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("local variable 'a' is referenced before assignment");
+  }
+
+  @Test
+  public void testHashFrozenListForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "y = []");
+
+    scratch.file(
+        "test/BUILD", "load('//test:extension.bzl', 'y')", "{y: 1}", "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("unhashable type: 'list'");
+  }
+
+  @Test
+  public void testHashFrozenDeepMutableForbidden() throws Exception {
+    scratch.file("test/extension.bzl", "y = {}");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:extension.bzl', 'y')",
+        "{('a', (y,), True): None}",
+        "cc_library(name = 'r')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:r");
+    assertContainsEvent("unhashable type: 'tuple'");
+  }
+
+  @Test
+  public void testNoOutputsError() throws Exception {
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  ctx.actions.run_shell(outputs=[], command='foo')",
+        "my_rule = rule(implementation = _my_rule_impl, executable = True)");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:target");
+    assertContainsEvent("param 'outputs' may not be empty");
+  }
+
+  @Test
+  public void testDeclareFileInvalidDirectory_withSibling() throws Exception {
+    scratch.file("test/dep/test_file.txt", "Test file");
+
+    scratch.file("test/dep/BUILD", "exports_files(['test_file.txt'])");
+
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_file('exe', sibling = ctx.file.dep)",
+        "  ctx.actions.run_shell(outputs=[exe], command=['touch', 'exe'])",
+        "  return []",
+        "my_rule = rule(implementation = _my_rule_impl,",
+        "    attrs = {'dep': attr.label(allow_single_file = True)})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target', dep = '//test/dep:test_file.txt')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test/starlark:target");
+    assertContainsEvent(
+        "the output artifact 'test/dep/exe' is not under package directory "
+            + "'test/starlark' for target '//test/starlark:target'");
+  }
+
+  @Test
+  public void testDeclareFileInvalidDirectory_noSibling() throws Exception {
+    scratch.file("test/dep/test_file.txt", "Test file");
+
+    scratch.file("test/dep/BUILD", "exports_files(['test_file.txt'])");
+
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_file('/foo/exe')",
+        "  ctx.actions.run_shell(outputs=[exe], command=['touch', 'exe'])",
+        "  return []",
+        "my_rule = rule(implementation = _my_rule_impl,",
+        "    attrs = {'dep': attr.label(allow_single_file = True)})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target', dep = '//test/dep:test_file.txt')");
+
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test/starlark:target")).isNull();
+    assertContainsEvent(
+        "the output artifact '/foo/exe' is not under package directory "
+            + "'test/starlark' for target '//test/starlark:target'");
+  }
+
+  @Test
+  public void testDeclareDirectoryInvalidParent_withSibling() throws Exception {
+    scratch.file("test/dep/test_file.txt", "Test file");
+
+    scratch.file("test/dep/BUILD", "exports_files(['test_file.txt'])");
+
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_directory('/foo/exe', sibling = ctx.file.dep)",
+        "  ctx.actions.run_shell(outputs=[exe], command=['touch', 'exe'])",
+        "  return []",
+        "my_rule = rule(implementation = _my_rule_impl,",
+        "    attrs = {'dep': attr.label(allow_single_file = True)})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target', dep = '//test/dep:test_file.txt')");
+
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test/starlark:target")).isNull();
+    assertContainsEvent(
+        "the output directory '/foo/exe' is not under package directory "
+            + "'test/starlark' for target '//test/starlark:target'");
+  }
+
+  @Test
+  public void testDeclareDirectoryInvalidParent_noSibling() throws Exception {
+    scratch.file("test/dep/test_file.txt", "Test file");
+
+    scratch.file("test/dep/BUILD", "exports_files(['test_file.txt'])");
+
+    scratch.file(
+        "test/starlark/test_rule.bzl",
+        "def _my_rule_impl(ctx):",
+        "  exe = ctx.actions.declare_directory('/foo/exe')",
+        "  ctx.actions.run_shell(outputs=[exe], command=['touch', 'exe'])",
+        "  return []",
+        "my_rule = rule(implementation = _my_rule_impl,",
+        "    attrs = {'dep': attr.label(allow_single_file = True)})");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:test_rule.bzl', 'my_rule')",
+        "my_rule(name = 'target', dep = '//test/dep:test_file.txt')");
+
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test/starlark:target")).isNull();
+    assertContainsEvent(
+        "the output directory '/foo/exe' is not under package directory "
+            + "'test/starlark' for target '//test/starlark:target'");
+  }
+
+  @Test
+  public void testLegacyProvider_AddCanonicalLegacyKeyAndModernKey() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(foo = apple_common.new_objc_provider(linkopt=depset(['foo'])))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'test')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:test");
+
+    ObjcProvider providerFromModernKey = target.get(ObjcProvider.STARLARK_CONSTRUCTOR);
+    ObjcProvider providerFromObjc = (ObjcProvider) target.get("objc");
+    ObjcProvider providerFromFoo = (ObjcProvider) target.get("foo");
+
+    // The modern key and the canonical legacy key "objc" are set to the one available ObjcProvider.
+    assertThat(providerFromModernKey.get(ObjcProvider.LINKOPT).toList()).containsExactly("foo");
+    assertThat(providerFromObjc.get(ObjcProvider.LINKOPT).toList()).containsExactly("foo");
+    assertThat(providerFromFoo.get(ObjcProvider.LINKOPT).toList()).containsExactly("foo");
+  }
+
+  @Test
+  public void testLegacyProvider_DontAutomaticallyAddKeysAlreadyPresent() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(providers = [apple_common.new_objc_provider(linkopt=depset(['prov']))],",
+        "       bah = apple_common.new_objc_provider(linkopt=depset(['bah'])),",
+        "       objc = apple_common.new_objc_provider(linkopt=depset(['objc'])))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'test')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:test");
+
+    ObjcProvider providerFromModernKey = target.get(ObjcProvider.STARLARK_CONSTRUCTOR);
+    ObjcProvider providerFromObjc = (ObjcProvider) target.get("objc");
+    ObjcProvider providerFromBah = (ObjcProvider) target.get("bah");
+
+    assertThat(providerFromModernKey.get(ObjcProvider.LINKOPT).toList()).containsExactly("prov");
+    assertThat(providerFromObjc.get(ObjcProvider.LINKOPT).toList()).containsExactly("objc");
+    assertThat(providerFromBah.get(ObjcProvider.LINKOPT).toList()).containsExactly("bah");
+  }
+
+  @Test
+  public void testLegacyProvider_FirstNoncanonicalKeyBecomesCanonical() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "def custom_rule_impl(ctx):",
+        "  return struct(providers = [apple_common.new_objc_provider(linkopt=depset(['prov']))],",
+        "       foo = apple_common.new_objc_provider(linkopt=depset(['foo'])),",
+        "       bar = apple_common.new_objc_provider(linkopt=depset(['bar'])))",
+        "",
+        "custom_rule = rule(implementation = custom_rule_impl)");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'custom_rule')",
+        "",
+        "custom_rule(name = 'test')");
+
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:test");
+
+    ObjcProvider providerFromModernKey = target.get(ObjcProvider.STARLARK_CONSTRUCTOR);
+    ObjcProvider providerFromObjc = (ObjcProvider) target.get("objc");
+    ObjcProvider providerFromFoo = (ObjcProvider) target.get("foo");
+    ObjcProvider providerFromBar = (ObjcProvider) target.get("bar");
+
+    assertThat(providerFromModernKey.get(ObjcProvider.LINKOPT).toList()).containsExactly("prov");
+    // The first defined provider is set to the legacy "objc" key.
+    assertThat(providerFromObjc.get(ObjcProvider.LINKOPT).toList()).containsExactly("foo");
+    assertThat(providerFromFoo.get(ObjcProvider.LINKOPT).toList()).containsExactly("foo");
+    assertThat(providerFromBar.get(ObjcProvider.LINKOPT).toList()).containsExactly("bar");
+  }
+
+  @Test
+  public void testCustomMallocUnset() throws Exception {
+    setUpCustomMallocRule();
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:malloc");
+    StructImpl provider = getMyInfoFromTarget(target);
+    Object customMalloc = provider.getValue("malloc");
+    assertThat(customMalloc).isInstanceOf(NoneType.class);
+  }
+
+  @Test
+  public void testCustomMallocSet() throws Exception {
+    setUpCustomMallocRule();
+    useConfiguration("--custom_malloc=//base:system_malloc");
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:malloc");
+    StructImpl provider = getMyInfoFromTarget(target);
+    RuleConfiguredTarget customMalloc = provider.getValue("malloc", RuleConfiguredTarget.class);
+    assertThat(customMalloc.getLabel().getCanonicalForm()).isEqualTo("//base:system_malloc");
+  }
+
+  private void setUpCustomMallocRule() throws IOException {
+    scratch.overwriteFile("base/BUILD", "cc_library(name = 'system_malloc')");
+    scratch.file(
+        "test/starlark/extension.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "",
+        "def _malloc_rule_impl(ctx):",
+        "  return [MyInfo(malloc = ctx.attr._custom_malloc)]",
+        "",
+        "malloc_rule = rule(",
+        "    implementation = _malloc_rule_impl,",
+        "    attrs = {",
+        "        '_custom_malloc': attr.label(",
+        "            default = configuration_field(",
+        "                fragment = 'cpp',",
+        "                name = 'custom_malloc',",
+        "            ),",
+        "            providers = [CcInfo],",
+        "        ),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:extension.bzl', 'malloc_rule')",
+        "",
+        "malloc_rule(name = 'malloc')");
+  }
+
+  // Test for an interesting situation for the inlining implementation's attempt to process
+  // subsequent load statements even when an earlier one has a missing Skyframe dep.
+  @Test
+  public void bzlFileWithErrorsLoadedThroughMultipleLoadPathsWithTheLatterOneHavingMissingDeps()
+      throws Exception {
+    scratch.file("test/starlark/error.bzl", "nope");
+    scratch.file("test/starlark/ok.bzl", "ok = 42");
+    scratch.file(
+        "test/starlark/loads-error-and-has-missing-deps.bzl",
+        "load('//test/starlark:error.bzl', 'doesntmatter')",
+        "load('//test/starlark:ok.bzl', 'ok')");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:error.bzl', 'doesntmatter')",
+        "load('//test/starlark:loads-error-and-has-missing-deps.bzl', 'doesntmatter')");
+
+    reporter.removeHandler(failFastHandler);
+    BuildFileContainsErrorsException e =
+        assertThrows(
+            BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:BUILD"));
+    assertThat(e).hasMessageThat().contains("Extension 'test/starlark/error.bzl' has errors");
+  }
+
+  // Test for an interesting situation for the inlining implementation's attempt to process
+  // subsequent load statements even when an earlier one has a missing Skyframe dep.
+  @Test
+  public void bzlFileWithErrorsLoadedThroughMultipleLoadPathsWithTheLatterOneNotHavingMissingDeps()
+      throws Exception {
+    scratch.file("test/starlark/error.bzl", "nope");
+    scratch.file("test/starlark/ok.bzl", "ok = 42");
+    scratch.file(
+        "test/starlark/loads-error-and-has-missing-deps.bzl",
+        "load('//test/starlark:error.bzl', 'doesntmatter')",
+        "load('//test/starlark:ok.bzl', 'ok')");
+    scratch.file(
+        "test/starlark/BUILD",
+        "load('//test/starlark:ok.bzl', 'ok')",
+        "load('//test/starlark:error.bzl', 'doesntmatter')",
+        "load('//test/starlark:loads-error-and-has-missing-deps.bzl', 'doesntmatter')");
+
+    reporter.removeHandler(failFastHandler);
+    BuildFileContainsErrorsException e =
+        assertThrows(
+            BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:BUILD"));
+    assertThat(e).hasMessageThat().contains("Extension 'test/starlark/error.bzl' has errors");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java
new file mode 100644
index 0000000..ea16805
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkOptionsParsingTest.java
@@ -0,0 +1,342 @@
+// Copyright 2018 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.runtime.StarlarkOptionsParser;
+import com.google.devtools.build.lib.starlark.util.StarlarkOptionsTestCase;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsParsingResult;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit test for the {@code StarlarkOptionsParser}. */
+@RunWith(JUnit4.class)
+public class StarlarkOptionsParsingTest extends StarlarkOptionsTestCase {
+
+  // test --flag=value
+  @Test
+  public void testFlagEqualsValueForm() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=666");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(666);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --flag value
+  @Test
+  public void testFlagSpaceValueForm() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting 666");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(666);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --@workspace//flag=value
+  @Test
+  public void testFlagNameWithWorkspace() throws Exception {
+    writeBasicIntFlag();
+    rewriteWorkspace("workspace(name = 'starlark_options_test')");
+
+    OptionsParsingResult result =
+        parseStarlarkOptions("--@starlark_options_test//test:my_int_setting=666");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("@starlark_options_test//test:my_int_setting"))
+        .isEqualTo(666);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --fake_flag=value
+  @Test
+  public void testBadFlag_equalsForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//fake_flag=blahblahblah"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+    assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
+  }
+
+  // test --fake_flag value
+  @Test
+  public void testBadFlag_spaceForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//fake_flag blahblahblah"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+    assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
+  }
+
+  // test --fake_flag
+  @Test
+  public void testBadFlag_boolForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//fake_flag"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+    assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
+  }
+
+  @Test
+  public void testSingleDash_notAllowed() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("-//test:my_int_setting=666");
+
+    assertThat(result.getStarlarkOptions()).isEmpty();
+    assertThat(result.getResidue()).containsExactly("-//test:my_int_setting=666");
+  }
+
+  // test --non_flag_setting=value
+  @Test
+  public void testNonFlagParsing() throws Exception {
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        "int_flag = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config.int(flag=False)",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', 'int_flag')",
+        "int_flag(name = 'my_int_setting', build_setting_default = 42)");
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_int_setting=666"));
+
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_int_setting=666");
+  }
+
+  // test --bool_flag
+  @Test
+  public void testBooleanFlag() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_bool_setting=false");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --nobool_flag
+  @Test
+  public void testNoPrefixedBooleanFlag() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--no//test:my_bool_setting");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --noint_flag
+  @Test
+  public void testNoPrefixedNonBooleanFlag() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class, () -> parseStarlarkOptions("--no//test:my_int_setting"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Illegal use of 'no' prefix on non-boolean option: //test:my_int_setting");
+  }
+
+  // test --int_flag
+  @Test
+  public void testFlagWithoutValue() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_int_setting"));
+
+    assertThat(e).hasMessageThat().isEqualTo("Expected value after --//test:my_int_setting");
+  }
+
+  // test --flag --flag
+  @Test
+  public void testRepeatFlagLastOneWins() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result =
+        parseStarlarkOptions("--//test:my_int_setting=4 --//test:my_int_setting=7");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(7);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --flagA=valueA --flagB=valueB
+  @Test
+  public void testMultipleFlags() throws Exception {
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        "int_flag = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config.int(flag=True)",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', 'int_flag')",
+        "int_flag(name = 'my_int_setting', build_setting_default = 42)",
+        "int_flag(name = 'my_other_int_setting', build_setting_default = 77)");
+
+    OptionsParsingResult result =
+        parseStarlarkOptions("--//test:my_int_setting=0 --//test:my_other_int_setting=0");
+
+    assertThat(result.getResidue()).isEmpty();
+    assertThat(result.getStarlarkOptions()).hasSize(2);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(0);
+    assertThat(result.getStarlarkOptions().get("//test:my_other_int_setting")).isEqualTo(0);
+  }
+
+  // test --non_build_setting
+  @Test
+  public void testNonBuildSetting() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        ")");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'my_rule')");
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_rule"));
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_rule");
+  }
+
+  // test --non_rule_configured_target
+  @Test
+  public void testNonRuleConfiguredTarget() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "genrule(",
+        "  name = 'my_gen',",
+        "  srcs = ['x.in'],",
+        "  outs = ['x.cc'],",
+        "  cmd = '$(locations :tool) $< >$@',",
+        "  tools = [':tool'],",
+        ")",
+        "cc_library(name = 'tool-dep')");
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:x.in"));
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:x.in");
+  }
+
+  // test --int_flag=non_int_value
+  @Test
+  public void testWrongValueType_int() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_int_setting=woohoo"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("While parsing option //test:my_int_setting=woohoo: 'woohoo' is not a int");
+  }
+
+  // test --bool_flag=non_bool_value
+  @Test
+  public void testWrongValueType_bool() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_bool_setting=woohoo"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("While parsing option //test:my_bool_setting=woohoo: 'woohoo' is not a boolean");
+  }
+
+  // test --int-flag=same value as default
+  @Test
+  public void testDontStoreDefaultValue() throws Exception {
+    // build_setting_default = 42
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=42");
+
+    assertThat(result.getStarlarkOptions()).isEmpty();
+  }
+
+  @Test
+  public void testOptionsAreParsedWithBuildTestsOnly() throws Exception {
+    writeBasicIntFlag();
+    optionsParser.parse("--build_tests_only");
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=15");
+
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(15);
+  }
+
+  @Test
+  public void testRemoveStarlarkOptionsWorks() throws Exception {
+    Pair<ImmutableList<String>, ImmutableList<String>> residueAndStarlarkOptions =
+        StarlarkOptionsParser.removeStarlarkOptions(
+            ImmutableList.of(
+                "--//local/starlark/option",
+                "--@some_repo//external/starlark/option",
+                "--@//main/repo/option",
+                "some-random-residue",
+                "--mangled//external/starlark/option"));
+    assertThat(residueAndStarlarkOptions.getFirst())
+        .containsExactly(
+            "--//local/starlark/option",
+            "--@some_repo//external/starlark/option",
+            "--@//main/repo/option");
+    assertThat(residueAndStarlarkOptions.getSecond())
+        .containsExactly("some-random-residue", "--mangled//external/starlark/option");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java
new file mode 100644
index 0000000..af4d729
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java
@@ -0,0 +1,1849 @@
+// Copyright 2014 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.config.transitions.NoTransition;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkAttrModule;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleClassFunctions.StarlarkRuleFunction;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.packages.AdvertisedProviderSet;
+import com.google.devtools.build.lib.packages.AspectParameters;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.BuildType;
+import com.google.devtools.build.lib.packages.ExecGroup;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.PredicateWithMessage;
+import com.google.devtools.build.lib.packages.RequiredProviders;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.StarlarkAspectClass;
+import com.google.devtools.build.lib.packages.StarlarkDefinedAspect;
+import com.google.devtools.build.lib.packages.StarlarkInfo;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StarlarkProviderIdentifier;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.packages.StructProvider;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.BzlLoadFunction;
+import com.google.devtools.build.lib.starlark.util.BazelEvaluationTestCase;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.Dict;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.FileOptions;
+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.StarlarkList;
+import com.google.devtools.build.lib.syntax.SyntaxError;
+import com.google.devtools.build.lib.syntax.Tuple;
+import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for StarlarkRuleClassFunctions. */
+@RunWith(JUnit4.class)
+public final class StarlarkRuleClassFunctionsTest extends BuildViewTestCase {
+
+  private final EvaluationTestCase ev = new BazelEvaluationTestCase();
+
+  private StarlarkRuleContext createRuleContext(String label) throws Exception {
+    return new StarlarkRuleContext(
+        getRuleContextForStarlark(getConfiguredTarget(label)), null, getStarlarkSemantics());
+  }
+
+  @Override
+  protected void setStarlarkSemanticsOptions(String... options) throws Exception {
+    super.setStarlarkSemanticsOptions(options); // for BuildViewTestCase
+    ev.setSemantics(options); // for StarlarkThread
+  }
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Before
+  public final void createBuildFile() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        "genrule(name = 'foo',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = ['a.txt', 'b.img'],",
+        "  tools = ['t.exe'],",
+        "  outs = ['c.txt'])",
+        "genrule(name = 'bar',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = [':jl', ':gl'],",
+        "  outs = ['d.txt'])",
+        "java_library(name = 'jl',",
+        "  srcs = ['a.java'])",
+        "genrule(name = 'gl',",
+        "  cmd = 'touch $(OUTS)',",
+        "  srcs = ['a.go'],",
+        "  outs = [ 'gl.a', 'gl.gcgox', ],",
+        "  output_to_bindir = 1,",
+        ")");
+  }
+
+  @Test
+  public void testCannotOverrideBuiltInAttribute() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "def impl(ctx):", //
+        "  return",
+        "r = rule(impl, attrs = {'tags': attr.string_list()})");
+    ev.assertContainsError(
+        "There is already a built-in attribute 'tags' which cannot be overridden");
+  }
+
+  @Test
+  public void testCannotOverrideBuiltInAttributeName() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "def impl(ctx):", //
+        "  return",
+        "r = rule(impl, attrs = {'name': attr.string()})");
+    ev.assertContainsError(
+        "There is already a built-in attribute 'name' which cannot be overridden");
+  }
+
+  @Test
+  public void testImplicitArgsAttribute() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "def _impl(ctx):",
+        "  pass",
+        "exec_rule = rule(implementation = _impl, executable = True)",
+        "non_exec_rule = rule(implementation = _impl)");
+    assertThat(getRuleClass("exec_rule").hasAttr("args", Type.STRING_LIST)).isTrue();
+    assertThat(getRuleClass("non_exec_rule").hasAttr("args", Type.STRING_LIST)).isFalse();
+  }
+
+  private RuleClass getRuleClass(String name) throws Exception {
+    return ((StarlarkRuleFunction) ev.lookup(name)).getRuleClass();
+  }
+
+  private void registerDummyStarlarkFunction() throws Exception {
+    ev.exec("def impl():", "  pass");
+  }
+
+  @Test
+  public void testAttrWithOnlyType() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string_list()");
+    assertThat(attr.getType()).isEqualTo(Type.STRING_LIST);
+  }
+
+  private Attribute buildAttribute(String name, String... lines) throws Exception {
+    String[] strings = lines.clone();
+    strings[strings.length - 1] = String.format("%s = %s", name, strings[strings.length - 1]);
+    evalAndExport(ev, strings);
+    StarlarkAttrModule.Descriptor lookup = (StarlarkAttrModule.Descriptor) ev.lookup(name);
+    return lookup != null ? lookup.build(name) : null;
+  }
+
+  @Test
+  public void testOutputListAttr() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.output_list()");
+    assertThat(attr.getType()).isEqualTo(BuildType.OUTPUT_LIST);
+  }
+
+  @Test
+  public void testIntListAttr() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.int_list()");
+    assertThat(attr.getType()).isEqualTo(Type.INTEGER_LIST);
+  }
+
+  @Test
+  public void testOutputAttr() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.output()");
+    assertThat(attr.getType()).isEqualTo(BuildType.OUTPUT);
+  }
+
+  @Test
+  public void testStringDictAttr() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string_dict(default = {'a': 'b'})");
+    assertThat(attr.getType()).isEqualTo(Type.STRING_DICT);
+  }
+
+  @Test
+  public void testStringListDictAttr() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string_list_dict(default = {'a': ['b', 'c']})");
+    assertThat(attr.getType()).isEqualTo(Type.STRING_LIST_DICT);
+  }
+
+  @Test
+  public void testAttrAllowedFileTypesAnyFile() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.label_list(allow_files = True)");
+    assertThat(attr.getAllowedFileTypesPredicate()).isEqualTo(FileTypeSet.ANY_FILE);
+  }
+
+  @Test
+  public void testAttrAllowedFileTypesWrongType() throws Exception {
+    ev.checkEvalErrorContains(
+        "allow_files should be a boolean or a string list", "attr.label_list(allow_files = 18)");
+  }
+
+  @Test
+  public void testAttrNameSpecialCharactersAreForbidden() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(ev, "def impl(ctx): return", "r = rule(impl, attrs = {'ab$c': attr.int()})");
+    ev.assertContainsError("attribute name `ab$c` is not a valid identifier");
+  }
+
+  @Test
+  public void testAttrNameCannotStartWithDigit() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(ev, "def impl(ctx): return", "r = rule(impl, attrs = {'2_foo': attr.int()})");
+    ev.assertContainsError("attribute name `2_foo` is not a valid identifier");
+  }
+
+  @Test
+  public void testRuleClassTooManyAttributes() throws Exception {
+    ev.setFailFast(false);
+
+    ImmutableList.Builder<String> linesBuilder =
+        ImmutableList.<String>builder()
+            .add("def impl(ctx): return")
+            .add("r = rule(impl, attrs = {");
+    for (int i = 0; i < 200; i++) {
+      linesBuilder.add("    'attr" + i + "': attr.int(),");
+    }
+    linesBuilder.add("})");
+
+    evalAndExport(ev, linesBuilder.build().toArray(new String[0]));
+
+    assertThat(ev.getEventCollector()).hasSize(1);
+    Event event = ev.getEventCollector().iterator().next();
+    assertThat(event.getKind()).isEqualTo(EventKind.ERROR);
+    assertThat(event.getMessage()).contains("Rule class r declared too many attributes");
+  }
+
+  @Test
+  public void testRuleClassTooLongAttributeName() throws Exception {
+    ev.setFailFast(false);
+
+    evalAndExport(
+        ev,
+        "def impl(ctx): return;",
+        "r = rule(impl, attrs = { '" + Strings.repeat("x", 150) + "': attr.int() })");
+
+    assertThat(ev.getEventCollector()).hasSize(1);
+    Event event = ev.getEventCollector().iterator().next();
+    assertThat(event.getKind()).isEqualTo(EventKind.ERROR);
+    assertThat(event.getMessage())
+        .matches("Attribute r\\.x{150}'s name is too long \\(150 > 128\\)");
+  }
+
+  @Test
+  public void testAttrAllowedSingleFileTypesWrongType() throws Exception {
+    ev.checkEvalErrorContains(
+        "allow_single_file should be a boolean or a string list",
+        "attr.label(allow_single_file = 18)");
+  }
+
+  @Test
+  public void testAttrWithList() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.label_list(allow_files = ['.xml'])");
+    assertThat(attr.getAllowedFileTypesPredicate().apply("a.xml")).isTrue();
+    assertThat(attr.getAllowedFileTypesPredicate().apply("a.txt")).isFalse();
+    assertThat(attr.isSingleArtifact()).isFalse();
+  }
+
+  @Test
+  public void testAttrSingleFileWithList() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.label(allow_single_file = ['.xml'])");
+    assertThat(attr.getAllowedFileTypesPredicate().apply("a.xml")).isTrue();
+    assertThat(attr.getAllowedFileTypesPredicate().apply("a.txt")).isFalse();
+    assertThat(attr.isSingleArtifact()).isTrue();
+  }
+
+  private static StarlarkProviderIdentifier legacy(String legacyId) {
+    return StarlarkProviderIdentifier.forLegacy(legacyId);
+  }
+
+  private static StarlarkProviderIdentifier declared(String exportedName) {
+    return StarlarkProviderIdentifier.forKey(new StarlarkProvider.Key(FAKE_LABEL, exportedName));
+  }
+
+  @Test
+  public void testAttrWithProviders() throws Exception {
+    Attribute attr =
+        buildAttribute("a1",
+            "b = provider()",
+            "attr.label_list(allow_files = True, providers = ['a', b])");
+    assertThat(attr.getRequiredProviders().isSatisfiedBy(set(legacy("a"), declared("b")))).isTrue();
+    assertThat(attr.getRequiredProviders().isSatisfiedBy(set(legacy("a")))).isFalse();
+  }
+
+  @Test
+  public void testAttrWithProvidersOneEmpty() throws Exception {
+    Attribute attr =
+        buildAttribute(
+            "a1",
+            "b = provider()",
+            "attr.label_list(allow_files = True, providers = [['a', b],[]])");
+    assertThat(attr.getRequiredProviders().acceptsAny()).isTrue();
+  }
+
+  @Test
+  public void testAttrWithProvidersList() throws Exception {
+    Attribute attr =
+        buildAttribute("a1",
+            "b = provider()",
+            "attr.label_list(allow_files = True, providers = [['a', b], ['c']])");
+    assertThat(attr.getRequiredProviders().isSatisfiedBy(set(legacy("a"), declared("b")))).isTrue();
+    assertThat(attr.getRequiredProviders().isSatisfiedBy(set(legacy("c")))).isTrue();
+    assertThat(attr.getRequiredProviders().isSatisfiedBy(set(legacy("a")))).isFalse();
+  }
+
+  private static AdvertisedProviderSet set(StarlarkProviderIdentifier... ids) {
+    AdvertisedProviderSet.Builder builder = AdvertisedProviderSet.builder();
+    for (StarlarkProviderIdentifier id : ids) {
+      builder.addStarlark(id);
+    }
+    return builder.build();
+  }
+
+  private void checkAttributeError(String expectedMessage, String... lines) throws Exception {
+    ev.setFailFast(false);
+    buildAttribute("fakeAttribute", lines);
+    MoreAsserts.assertContainsEvent(ev.getEventCollector(), expectedMessage);
+  }
+
+  @Test
+  public void testAttrWithWrongProvidersList() throws Exception {
+    checkAttributeError(
+        "element in 'providers' is of unexpected type. Either all elements should be providers,"
+            + " or all elements should be lists of providers,"
+            + " but got list with an element of type int.",
+        "attr.label_list(allow_files = True,  providers = [['a', 1], ['c']])");
+
+    checkAttributeError(
+        "element in 'providers' is of unexpected type. Either all elements should be providers,"
+            + " or all elements should be lists of providers,"
+            + " but got an element of type string.",
+        "b = provider()",
+        "attr.label_list(allow_files = True,  providers = [['a', b], 'c'])");
+
+    checkAttributeError(
+        "element in 'providers' is of unexpected type. Either all elements should be providers,"
+            + " or all elements should be lists of providers,"
+            + " but got an element of type string.",
+        "c = provider()",
+        "attr.label_list(allow_files = True,  providers = [['a', b], c])");
+  }
+
+  @Test
+  public void testLabelListWithAspects() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(implementation = _impl)",
+        "a = attr.label_list(aspects = [my_aspect])");
+    StarlarkAttrModule.Descriptor attr = (StarlarkAttrModule.Descriptor) ev.lookup("a");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    assertThat(aspect).isNotNull();
+    assertThat(attr.build("xxx").getAspectClasses()).containsExactly(aspect.getAspectClass());
+  }
+
+  @Test
+  public void testLabelWithAspects() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(implementation = _impl)",
+        "a = attr.label(aspects = [my_aspect])");
+    StarlarkAttrModule.Descriptor attr = (StarlarkAttrModule.Descriptor) ev.lookup("a");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    assertThat(aspect).isNotNull();
+    assertThat(attr.build("xxx").getAspectClasses()).containsExactly(aspect.getAspectClass());
+  }
+
+  @Test
+  public void testLabelListWithAspectsError() throws Exception {
+    ev.checkEvalErrorContains(
+        "at index 0 of aspects, got element of type int, want Aspect",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(implementation = _impl)",
+        "attr.label_list(aspects = [my_aspect, 123])");
+  }
+
+  @Test
+  public void testAspectExtraDeps() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { '_extra_deps' : attr.label(default = Label('//foo/bar:baz')) }",
+        ")");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    Attribute attribute = Iterables.getOnlyElement(aspect.getAttributes());
+    assertThat(attribute.getName()).isEqualTo("$extra_deps");
+    assertThat(attribute.getDefaultValue(null))
+        .isEqualTo(
+            Label.parseAbsolute(
+                "//foo/bar:baz",
+                /* defaultToMain= */ false,
+                /* repositoryMapping= */ ImmutableMap.of()));
+  }
+
+  @Test
+  public void testAspectParameter() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(values=['a', 'b']) }",
+        ")");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    Attribute attribute = Iterables.getOnlyElement(aspect.getAttributes());
+    assertThat(attribute.getName()).isEqualTo("param");
+  }
+
+  @Test
+  public void testAspectParameterRequiresValues() throws Exception {
+    ev.checkEvalErrorContains(
+        "Aspect parameter attribute 'param' must have type 'string' and use the 'values' "
+            + "restriction.",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(default = 'c') }",
+        ")");
+  }
+
+  @Test
+  public void testAspectParameterBadType() throws Exception {
+    ev.checkEvalErrorContains(
+        "Aspect parameter attribute 'param' must have type 'string' and use the 'values' "
+            + "restriction.",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.label(default = Label('//foo/bar:baz')) }",
+        ")");
+  }
+
+  @Test
+  public void testAspectParameterAndExtraDeps() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { 'param' : attr.string(values=['a', 'b']),",
+        "             '_extra' : attr.label(default = Label('//foo/bar:baz')) }",
+        ")");
+    StarlarkDefinedAspect aspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    assertThat(aspect.getAttributes()).hasSize(2);
+    assertThat(aspect.getParamAttributes()).containsExactly("param");
+  }
+
+  @Test
+  public void testAspectNoDefaultValueAttribute() throws Exception {
+    ev.checkEvalErrorContains(
+        "Aspect attribute '_extra_deps' has no default value",
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl,",
+        "   attrs = { '_extra_deps' : attr.label() }",
+        ")");
+  }
+
+  @Test
+  public void testAspectAddToolchain() throws Exception {
+    scratch.file("test/BUILD", "toolchain_type(name = 'my_toolchain_type')");
+    evalAndExport(
+        ev, "def _impl(ctx): pass", "a1 = aspect(_impl, toolchains=['//test:my_toolchain_type'])");
+    StarlarkDefinedAspect a = (StarlarkDefinedAspect) ev.lookup("a1");
+    assertThat(a.getRequiredToolchains()).containsExactly(makeLabel("//test:my_toolchain_type"));
+  }
+
+  @Test
+  public void testNonLabelAttrWithProviders() throws Exception {
+    ev.checkEvalErrorContains(
+        "unexpected keyword argument 'providers'", "attr.string(providers = ['a'])");
+  }
+
+  private static final RuleClass.ConfiguredTargetFactory<Object, Object, Exception>
+      DUMMY_CONFIGURED_TARGET_FACTORY =
+          ruleContext -> {
+            throw new IllegalStateException();
+          };
+
+  private RuleClass ruleClass(String name) {
+    return new RuleClass.Builder(name, RuleClassType.NORMAL, false)
+        .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+        .add(Attribute.attr("tags", Type.STRING_LIST))
+        .build();
+  }
+
+  @Test
+  public void testAttrAllowedRuleClassesSpecificRuleClasses() throws Exception {
+    Attribute attr = buildAttribute("a",
+        "attr.label_list(allow_rules = ['java_binary'], allow_files = True)");
+    assertThat(attr.getAllowedRuleClassesPredicate().apply(ruleClass("java_binary"))).isTrue();
+    assertThat(attr.getAllowedRuleClassesPredicate().apply(ruleClass("genrule"))).isFalse();
+  }
+
+  @Test
+  public void testAttrDefaultValue() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string(default = 'some value')");
+    assertThat(attr.getDefaultValueUnchecked()).isEqualTo("some value");
+  }
+
+  @Test
+  public void testLabelAttrDefaultValueAsString() throws Exception {
+    Attribute sligleAttr = buildAttribute("a1", "attr.label(default = '//foo:bar')");
+    assertThat(sligleAttr.getDefaultValueUnchecked())
+        .isEqualTo(
+            Label.parseAbsolute(
+                "//foo:bar",
+                /* defaultToMain= */ false,
+                /* repositoryMapping= */ ImmutableMap.of()));
+
+    Attribute listAttr =
+        buildAttribute("a2", "attr.label_list(default = ['//foo:bar', '//bar:foo'])");
+    assertThat(listAttr.getDefaultValueUnchecked())
+        .isEqualTo(
+            ImmutableList.of(
+                Label.parseAbsolute(
+                    "//foo:bar",
+                    /* defaultToMain= */ false,
+                    /* repositoryMapping= */ ImmutableMap.of()),
+                Label.parseAbsolute(
+                    "//bar:foo",
+                    /* defaultToMain= */ false,
+                    /*repositoryMapping= */ ImmutableMap.of())));
+
+    Attribute dictAttr =
+        buildAttribute("a3", "attr.label_keyed_string_dict(default = {'//foo:bar': 'my value'})");
+    assertThat(dictAttr.getDefaultValueUnchecked())
+        .isEqualTo(
+            ImmutableMap.of(
+                Label.parseAbsolute(
+                    "//foo:bar",
+                    /* defaultToMain= */ false,
+                    /* repositoryMapping= */ ImmutableMap.of()),
+                "my value"));
+  }
+
+  @Test
+  public void testLabelAttrDefaultValueAsStringBadValue() throws Exception {
+    ev.checkEvalErrorContains(
+        "invalid label '/foo:bar' in parameter 'default' of attribute 'label': "
+            + "invalid target name '/foo:bar'",
+        "attr.label(default = '/foo:bar')");
+
+    ev.checkEvalErrorContains(
+        "invalid label '/bar:foo' in element 1 of parameter 'default' of attribute "
+            + "'label_list': invalid target name '/bar:foo'",
+        "attr.label_list(default = ['//foo:bar', '/bar:foo'])");
+
+    ev.checkEvalErrorContains(
+        "invalid label '/bar:foo' in dict key element: invalid target name '/bar:foo'",
+        "attr.label_keyed_string_dict(default = {'//foo:bar': 'a', '/bar:foo': 'b'})");
+  }
+
+  @Test
+  public void testAttrDefaultValueBadType() throws Exception {
+    ev.checkEvalErrorContains("got value of type 'int', want 'string'", "attr.string(default = 1)");
+  }
+
+  @Test
+  public void testAttrMandatory() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string(mandatory=True)");
+    assertThat(attr.isMandatory()).isTrue();
+    assertThat(attr.isNonEmpty()).isFalse();
+  }
+
+  @Test
+  public void testAttrAllowEmpty() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string_list(allow_empty=False)");
+    assertThat(attr.isNonEmpty()).isTrue();
+    assertThat(attr.isMandatory()).isFalse();
+  }
+
+  @Test
+  public void testAttrBadKeywordArguments() throws Exception {
+    ev.checkEvalErrorContains(
+        "string() got unexpected keyword argument 'bad_keyword'", "attr.string(bad_keyword = '')");
+  }
+
+  @Test
+  public void testAttrCfg() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.label(cfg = 'host', allow_files = True)");
+    assertThat(attr.getTransitionFactory().isHost()).isTrue();
+  }
+
+  @Test
+  public void testAttrCfgTarget() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.label(cfg = 'target', allow_files = True)");
+    assertThat(NoTransition.isInstance(attr.getTransitionFactory())).isTrue();
+  }
+
+  @Test
+  public void incompatibleDataTransition() throws Exception {
+    EvalException expected =
+        assertThrows(EvalException.class, () -> ev.eval("attr.label(cfg = 'data')"));
+    assertThat(expected).hasMessageThat().contains("cfg must be either 'host' or 'target'");
+  }
+
+  @Test
+  public void testAttrValues() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.string(values = ['ab', 'cd'])");
+    PredicateWithMessage<Object> predicate = attr.getAllowedValues();
+    assertThat(predicate.apply("ab")).isTrue();
+    assertThat(predicate.apply("xy")).isFalse();
+  }
+
+  @Test
+  public void testAttrIntValues() throws Exception {
+    Attribute attr = buildAttribute("a1", "attr.int(values = [1, 2])");
+    PredicateWithMessage<Object> predicate = attr.getAllowedValues();
+    assertThat(predicate.apply(2)).isTrue();
+    assertThat(predicate.apply(3)).isFalse();
+  }
+
+  @Test
+  public void testAttrDoc() throws Exception {
+    // We don't actually store the doc in the attr definition; right now it's just meant to be
+    // extracted by documentation generating tools. So we don't have anything to assert and we just
+    // verify that no exceptions were thrown from building them.
+    buildAttribute("a1", "attr.bool(doc='foo')");
+    buildAttribute("a2", "attr.int(doc='foo')");
+    buildAttribute("a3", "attr.int_list(doc='foo')");
+    buildAttribute("a4", "attr.label(doc='foo')");
+    buildAttribute("a5", "attr.label_keyed_string_dict(doc='foo')");
+    buildAttribute("a6", "attr.label_list(doc='foo')");
+    buildAttribute("a8", "attr.output(doc='foo')");
+    buildAttribute("a9", "attr.output_list(doc='foo')");
+    buildAttribute("a10", "attr.string(doc='foo')");
+    buildAttribute("a11", "attr.string_dict(doc='foo')");
+    buildAttribute("a12", "attr.string_list(doc='foo')");
+    buildAttribute("a13", "attr.string_list_dict(doc='foo')");
+  }
+
+  @Test
+  public void testNoAttrLicense() throws Exception {
+    EvalException expected = assertThrows(EvalException.class, () -> ev.eval("attr.license()"));
+    assertThat(expected).hasMessageThat().contains("'attr' value has no field or method 'license'");
+  }
+
+  @Test
+  public void testAttrDocValueBadType() throws Exception {
+    ev.checkEvalErrorContains("got value of type 'int', want 'string'", "attr.string(doc = 1)");
+  }
+
+  @Test
+  public void testRuleImplementation() throws Exception {
+    evalAndExport(ev, "def impl(ctx): return None", "rule1 = rule(impl)");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("rule1")).getRuleClass();
+    assertThat(c.getConfiguredTargetFunction().getName()).isEqualTo("impl");
+  }
+
+  @Test
+  public void testRuleDoc() throws Exception {
+    evalAndExport(ev, "def impl(ctx): return None", "rule1 = rule(impl, doc='foo')");
+  }
+
+  @Test
+  public void testFunctionAsAttrDefault() throws Exception {
+    ev.exec("def f(): pass");
+
+    // Late-bound attributes, which are computed during analysis as a function
+    // of the configuration, are only available for attributes involving labels:
+    //   attr.label
+    //   attr.label_list
+    //   attr.label_keyed_string_dict
+    //   attr.output,
+    //   attr.output_list
+    // (See testRuleClassImplicitOutputFunctionDependingOnComputedAttribute
+    // for a more detailed positive test.)
+    evalAndExport(
+        ev,
+        "attr.label(default=f)",
+        "attr.label_list(default=f)",
+        "attr.label_keyed_string_dict(default=f)");
+    // For all other attribute types, the default value may not be a function.
+    //
+    // (This is a regression test for github.com/bazelbuild/bazel/issues/9463.
+    // The loading-phase feature of "computed attribute defaults" is not exposed
+    // to Starlark; the bug was that the @StarlarkMethod
+    // annotation was more permissive than the method declaration.)
+    ev.checkEvalErrorContains(
+        "got value of type 'function', want 'string'", "attr.string(default=f)");
+    ev.checkEvalErrorContains(
+        "got value of type 'function', want 'sequence'", "attr.string_list(default=f)");
+    ev.checkEvalErrorContains("got value of type 'function', want 'int'", "attr.int(default=f)");
+    ev.checkEvalErrorContains(
+        "got value of type 'function', want 'sequence'", "attr.int_list(default=f)");
+    ev.checkEvalErrorContains("got value of type 'function', want 'bool'", "attr.bool(default=f)");
+    ev.checkEvalErrorContains(
+        "got value of type 'function', want 'dict'", "attr.string_dict(default=f)");
+    ev.checkEvalErrorContains(
+        "got value of type 'function', want 'dict'", "attr.string_list_dict(default=f)");
+    // Note: attr.license appears to be disabled already.
+    // (see --incompatible_no_attr_license)
+  }
+
+  private static final Label FAKE_LABEL = Label.parseAbsoluteUnchecked("//fake/label.bzl");
+
+  @Test
+  public void testRuleAddAttribute() throws Exception {
+    evalAndExport(ev, "def impl(ctx): return None", "r1 = rule(impl, attrs={'a1': attr.string()})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.hasAttr("a1", Type.STRING)).isTrue();
+  }
+
+  private static void evalAndExport(EvaluationTestCase ev, String... lines) throws Exception {
+    ParserInput input = ParserInput.fromLines(lines);
+    Module module = ev.getModule();
+    StarlarkFile file = EvalUtils.parseAndValidate(input, FileOptions.DEFAULT, module);
+    if (!file.ok()) {
+      throw new SyntaxError.Exception(file.errors());
+    }
+    BzlLoadFunction.execAndExport(
+        file, FAKE_LABEL, ev.getEventHandler(), module, ev.getStarlarkThread());
+  }
+
+  @Test
+  public void testExportAliasedName() throws Exception {
+    // When there are multiple names aliasing the same StarlarkExportable, the first one to be
+    // declared should be used. Make sure we're not using lexicographical order, hash order,
+    // non-deterministic order, or anything else.
+    evalAndExport(
+        ev,
+        "def _impl(ctx): pass",
+        "d = rule(implementation = _impl)",
+        "a = d",
+        // Having more names improves the chance that non-determinism will be caught.
+        "b = d",
+        "c = d",
+        "e = d",
+        "f = d",
+        "foo = d",
+        "bar = d",
+        "baz = d",
+        "x = d",
+        "y = d",
+        "z = d");
+    String dName = ((StarlarkRuleFunction) ev.lookup("d")).getRuleClass().getName();
+    String fooName = ((StarlarkRuleFunction) ev.lookup("foo")).getRuleClass().getName();
+    assertThat(dName).isEqualTo("d");
+    assertThat(fooName).isEqualTo("d");
+  }
+
+  @Test
+  public void testOutputToGenfiles() throws Exception {
+    evalAndExport(ev, "def impl(ctx): pass", "r1 = rule(impl, output_to_genfiles=True)");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.hasBinaryOutput()).isFalse();
+  }
+
+  @Test
+  public void testRuleAddMultipleAttributes() throws Exception {
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None",
+        "r1 = rule(impl,",
+        "     attrs = {",
+        "            'a1': attr.label_list(allow_files=True),",
+        "            'a2': attr.int()",
+        "})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.hasAttr("a1", BuildType.LABEL_LIST)).isTrue();
+    assertThat(c.hasAttr("a2", Type.INTEGER)).isTrue();
+  }
+
+  @Test
+  public void testRuleAttributeFlag() throws Exception {
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None",
+        "r1 = rule(impl, attrs = {'a1': attr.string(mandatory=True)})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.getAttributeByName("a1").isMandatory()).isTrue();
+  }
+
+  @Test
+  public void testRuleOutputs() throws Exception {
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None", //
+        "r1 = rule(impl, outputs = {'a': 'a.txt'})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    ImplicitOutputsFunction function = c.getDefaultImplicitOutputsFunction();
+    assertThat(function.getImplicitOutputs(ev.getEventHandler(), null)).containsExactly("a.txt");
+  }
+
+  @Test
+  public void testRuleUnknownKeyword() throws Exception {
+    registerDummyStarlarkFunction();
+    ev.checkEvalErrorContains(
+        "unexpected keyword argument 'bad_keyword'", "rule(impl, bad_keyword = 'some text')");
+  }
+
+  @Test
+  public void testRuleImplementationMissing() throws Exception {
+    ev.checkEvalErrorContains(
+        "rule() missing 1 required positional argument: implementation", "rule(attrs = {})");
+  }
+
+  @Test
+  public void testRuleBadTypeForAdd() throws Exception {
+    registerDummyStarlarkFunction();
+    ev.checkEvalErrorContains(
+        "in call to rule(), parameter 'attrs' got value of type 'string', want 'dict or NoneType'",
+        "rule(impl, attrs = 'some text')");
+  }
+
+  @Test
+  public void testRuleBadTypeInAdd() throws Exception {
+    registerDummyStarlarkFunction();
+    ev.checkEvalErrorContains(
+        "got dict<string, string> for 'attrs', want dict<string, Attribute>",
+        "rule(impl, attrs = {'a1': 'some text'})");
+  }
+
+  @Test
+  public void testRuleBadTypeForDoc() throws Exception {
+    registerDummyStarlarkFunction();
+    ev.checkEvalErrorContains("got value of type 'int', want 'string'", "rule(impl, doc = 1)");
+  }
+
+  @Test
+  public void testLabel() throws Exception {
+    Object result = ev.eval("Label('//foo/foo:foo')");
+    assertThat(result).isInstanceOf(Label.class);
+    assertThat(result.toString()).isEqualTo("//foo/foo:foo");
+  }
+
+  @Test
+  public void testLabelSameInstance() throws Exception {
+    Object l1 = ev.eval("Label('//foo/foo:foo')");
+    // Implicitly creates a new pkgContext and environment, yet labels should be the same.
+    Object l2 = ev.eval("Label('//foo/foo:foo')");
+    assertThat(l1).isSameInstanceAs(l2);
+  }
+
+  @Test
+  public void testLabelNameAndPackage() throws Exception {
+    Object result = ev.eval("Label('//foo/bar:baz').name");
+    assertThat(result).isEqualTo("baz");
+    // NB: implicitly creates a new pkgContext and environments, yet labels should be the same.
+    result = ev.eval("Label('//foo/bar:baz').package");
+    assertThat(result).isEqualTo("foo/bar");
+  }
+
+  @Test
+  public void testRuleLabelDefaultValue() throws Exception {
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None\n"
+            + "r1 = rule(impl, attrs = {'a1': "
+            + "attr.label(default = Label('//foo:foo'), allow_files=True)})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    Attribute a = c.getAttributeByName("a1");
+    assertThat(a.getDefaultValueUnchecked()).isInstanceOf(Label.class);
+    assertThat(a.getDefaultValueUnchecked().toString()).isEqualTo("//foo:foo");
+  }
+
+  @Test
+  public void testIntDefaultValue() throws Exception {
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None",
+        "r1 = rule(impl, attrs = {'a1': attr.int(default = 40+2)})");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    Attribute a = c.getAttributeByName("a1");
+    assertThat(a.getDefaultValueUnchecked()).isEqualTo(42);
+  }
+
+  @Test
+  public void testRuleInheritsBaseRuleAttributes() throws Exception {
+    evalAndExport(ev, "def impl(ctx): return None", "r1 = rule(impl)");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.hasAttr("tags", Type.STRING_LIST)).isTrue();
+    assertThat(c.hasAttr("visibility", BuildType.NODEP_LABEL_LIST)).isTrue();
+    assertThat(c.hasAttr("deprecation", Type.STRING)).isTrue();
+    assertThat(c.hasAttr(":action_listener", BuildType.LABEL_LIST))
+        .isTrue(); // required for extra actions
+  }
+
+  private void checkTextMessage(String from, String... lines) throws Exception {
+    String[] strings = lines.clone();
+    Object result = ev.eval(from);
+    String expect = "";
+    if (strings.length > 0) {
+      expect = Joiner.on("\n").join(lines) + "\n";
+    }
+    assertThat(result).isEqualTo(expect);
+  }
+
+  @Test
+  public void testSimpleTextMessagesBooleanFields() throws Exception {
+    checkTextMessage("struct(name=True).to_proto()", "name: true");
+    checkTextMessage("struct(name=False).to_proto()", "name: false");
+  }
+
+  @Test
+  public void testStructRestrictedOverrides() throws Exception {
+    ev.checkEvalErrorContains(
+        "cannot override built-in struct function 'to_json'", "struct(to_json='foo')");
+
+    ev.checkEvalErrorContains(
+        "cannot override built-in struct function 'to_proto'", "struct(to_proto='foo')");
+  }
+
+  @Test
+  public void testSimpleTextMessages() throws Exception {
+    checkTextMessage("struct(name='value').to_proto()", "name: \"value\"");
+    checkTextMessage("struct(name=[]).to_proto()"); // empty lines
+    checkTextMessage("struct(name=['a', 'b']).to_proto()", "name: \"a\"", "name: \"b\"");
+    checkTextMessage("struct(name=123).to_proto()", "name: 123");
+    checkTextMessage("struct(name=[1, 2, 3]).to_proto()", "name: 1", "name: 2", "name: 3");
+    checkTextMessage("struct(a=struct(b='b')).to_proto()", "a {", "  b: \"b\"", "}");
+    checkTextMessage(
+        "struct(a=[struct(b='x'), struct(b='y')]).to_proto()",
+        "a {",
+        "  b: \"x\"",
+        "}",
+        "a {",
+        "  b: \"y\"",
+        "}");
+    checkTextMessage(
+        "struct(a=struct(b=struct(c='c'))).to_proto()", "a {", "  b {", "    c: \"c\"", "  }", "}");
+    // dict to_proto tests
+    checkTextMessage("struct(name={}).to_proto()"); // empty lines
+    checkTextMessage(
+        "struct(name={'a': 'b'}).to_proto()", "name {", "  key: \"a\"", "  value: \"b\"", "}");
+    checkTextMessage(
+        "struct(name={'c': 'd', 'a': 'b'}).to_proto()",
+        "name {",
+        "  key: \"c\"",
+        "  value: \"d\"",
+        "}",
+        "name {",
+        "  key: \"a\"",
+        "  value: \"b\"",
+        "}");
+    checkTextMessage(
+        "struct(x=struct(y={'a': 1})).to_proto()",
+        "x {",
+        "  y {",
+        "    key: \"a\"",
+        "    value: 1",
+        "  }",
+        "}");
+    checkTextMessage(
+        "struct(name={'a': struct(b=1, c=2)}).to_proto()",
+        "name {",
+        "  key: \"a\"",
+        "  value {",
+        "    b: 1",
+        "    c: 2",
+        "  }",
+        "}");
+    checkTextMessage(
+        "struct(name={'a': struct(b={4: 'z', 3: 'y'}, c=2)}).to_proto()",
+        "name {",
+        "  key: \"a\"",
+        "  value {",
+        "    b {",
+        "      key: 4",
+        "      value: \"z\"",
+        "    }",
+        "    b {",
+        "      key: 3",
+        "      value: \"y\"",
+        "    }",
+        "    c: 2",
+        "  }",
+        "}");
+  }
+
+  @Test
+  public void testProtoFieldsOrder() throws Exception {
+    checkTextMessage("struct(d=4, b=2, c=3, a=1).to_proto()", "a: 1", "b: 2", "c: 3", "d: 4");
+  }
+
+  @Test
+  public void testTextMessageEscapes() throws Exception {
+    checkTextMessage("struct(name='a\"b').to_proto()", "name: \"a\\\"b\"");
+    checkTextMessage("struct(name='a\\'b').to_proto()", "name: \"a'b\"");
+    checkTextMessage("struct(name='a\\nb').to_proto()", "name: \"a\\nb\"");
+
+    // struct(name="a\\\"b") -> name: "a\\\"b"
+    checkTextMessage("struct(name='a\\\\\\\"b').to_proto()", "name: \"a\\\\\\\"b\"");
+  }
+
+  @Test
+  public void testTextMessageInvalidElementInListStructure() throws Exception {
+    ev.checkEvalErrorContains(
+        "Invalid text format, expected a struct, a dict, a string, a bool, or "
+            + "an int but got a list for list element in struct field 'a'",
+        "struct(a=[['b']]).to_proto()");
+  }
+
+  @Test
+  public void testTextMessageInvalidStructure() throws Exception {
+    ev.checkEvalErrorContains(
+        "Invalid text format, expected a struct, a dict, a string, a bool, or an int "
+            + "but got a function for struct field 'a'",
+        "struct(a=rule).to_proto()");
+  }
+
+  private void checkJson(String from, String expected) throws Exception {
+    Object result = ev.eval(from);
+    assertThat(result).isEqualTo(expected);
+  }
+
+  @Test
+  public void testJsonBooleanFields() throws Exception {
+    checkJson("struct(name=True).to_json()", "{\"name\":true}");
+    checkJson("struct(name=False).to_json()", "{\"name\":false}");
+  }
+
+  @Test
+  public void testJsonDictFields() throws Exception {
+    checkJson("struct(config={}).to_json()", "{\"config\":{}}");
+    checkJson("struct(config={'key': 'value'}).to_json()", "{\"config\":{\"key\":\"value\"}}");
+    ev.checkEvalErrorContains(
+        "Keys must be a string but got a int for struct field 'config'",
+        "struct(config={1:2}).to_json()");
+    ev.checkEvalErrorContains(
+        "Keys must be a string but got a int for dict value 'foo'",
+        "struct(config={'foo':{1:2}}).to_json()");
+    ev.checkEvalErrorContains(
+        "Keys must be a string but got a bool for struct field 'config'",
+        "struct(config={True: False}).to_json()");
+  }
+
+  @Test
+  public void testJsonEncoding() throws Exception {
+    checkJson("struct(name='value').to_json()", "{\"name\":\"value\"}");
+    checkJson("struct(name=['a', 'b']).to_json()", "{\"name\":[\"a\",\"b\"]}");
+    checkJson("struct(name=123).to_json()", "{\"name\":123}");
+    checkJson("struct(name=[1, 2, 3]).to_json()", "{\"name\":[1,2,3]}");
+    checkJson("struct(a=struct(b='b')).to_json()", "{\"a\":{\"b\":\"b\"}}");
+    checkJson("struct(a=[struct(b='x'), struct(b='y')]).to_json()",
+        "{\"a\":[{\"b\":\"x\"},{\"b\":\"y\"}]}");
+    checkJson("struct(a=struct(b=struct(c='c'))).to_json()",
+        "{\"a\":{\"b\":{\"c\":\"c\"}}}");
+  }
+
+  @Test
+  public void testJsonEscapes() throws Exception {
+    checkJson("struct(name='a\"b').to_json()", "{\"name\":\"a\\\"b\"}");
+    checkJson("struct(name='a\\'b').to_json()", "{\"name\":\"a'b\"}");
+    checkJson("struct(name='a\\\\b').to_json()", "{\"name\":\"a\\\\b\"}");
+    checkJson("struct(name='a\\nb').to_json()", "{\"name\":\"a\\nb\"}");
+    checkJson("struct(name='a\\rb').to_json()", "{\"name\":\"a\\rb\"}");
+    checkJson("struct(name='a\\tb').to_json()", "{\"name\":\"a\\tb\"}");
+  }
+
+  @Test
+  public void testJsonNestedListStructure() throws Exception {
+    checkJson("struct(a=[['b']]).to_json()", "{\"a\":[[\"b\"]]}");
+  }
+
+  @Test
+  public void testJsonInvalidStructure() throws Exception {
+    ev.checkEvalErrorContains(
+        "Invalid text format, expected a struct, a string, a bool, or an int but got a "
+            + "function for struct field 'a'",
+        "struct(a=rule).to_json()");
+  }
+
+  @Test
+  public void testLabelAttrWrongDefault() throws Exception {
+    ev.checkEvalErrorContains(
+        "got value of type 'int', want 'Label or string or LateBoundDefault or function or"
+            + " NoneType'",
+        "attr.label(default = 123)");
+  }
+
+  @Test
+  public void testLabelGetRelative() throws Exception {
+    assertThat(ev.eval("Label('//foo:bar').relative('baz')").toString()).isEqualTo("//foo:baz");
+    assertThat(ev.eval("Label('//foo:bar').relative('//baz:qux')").toString())
+        .isEqualTo("//baz:qux");
+  }
+
+  @Test
+  public void testLabelGetRelativeSyntaxError() throws Exception {
+    ev.checkEvalErrorContains(
+        "invalid target name 'bad//syntax': target names may not contain '//' path separators",
+        "Label('//foo:bar').relative('bad//syntax')");
+  }
+
+  @Test
+  public void testStructCreation() throws Exception {
+    // TODO(fwe): cannot be handled by current testing suite
+    ev.exec("x = struct(a = 1, b = 2)");
+    assertThat(ev.lookup("x")).isInstanceOf(ClassObject.class);
+  }
+
+  @Test
+  public void testStructFields() throws Exception {
+    // TODO(fwe): cannot be handled by current testing suite
+    ev.exec("x = struct(a = 1, b = 2)");
+    ClassObject x = (ClassObject) ev.lookup("x");
+    assertThat(x.getValue("a")).isEqualTo(1);
+    assertThat(x.getValue("b")).isEqualTo(2);
+  }
+
+  @Test
+  public void testStructEquality() throws Exception {
+    assertThat((Boolean) ev.eval("struct(a = 1, b = 2) == struct(b = 2, a = 1)")).isTrue();
+    assertThat((Boolean) ev.eval("struct(a = 1) == struct(a = 1, b = 2)")).isFalse();
+    assertThat((Boolean) ev.eval("struct(a = 1, b = 2) == struct(a = 1)")).isFalse();
+    // Compare a recursive object to itself to make sure reference equality is checked
+    ev.exec("s = struct(a = 1, b = []); s.b.append(s)");
+    assertThat((Boolean) ev.eval("s == s")).isTrue();
+    assertThat((Boolean) ev.eval("struct(a = 1, b = 2) == struct(a = 1, b = 3)")).isFalse();
+    assertThat((Boolean) ev.eval("struct(a = 1) == [1]")).isFalse();
+    assertThat((Boolean) ev.eval("[1] == struct(a = 1)")).isFalse();
+    assertThat((Boolean) ev.eval("struct() == struct()")).isTrue();
+    assertThat((Boolean) ev.eval("struct() == struct(a = 1)")).isFalse();
+
+    ev.exec("foo = provider(); bar = provider()");
+    assertThat((Boolean) ev.eval("struct(a = 1) == foo(a = 1)")).isFalse();
+    assertThat((Boolean) ev.eval("foo(a = 1) == struct(a = 1)")).isFalse();
+    assertThat((Boolean) ev.eval("foo(a = 1) == bar(a = 1)")).isFalse();
+    assertThat((Boolean) ev.eval("foo(a = 1) == foo(a = 1)")).isTrue();
+  }
+
+  @Test
+  public void testStructIncomparability() throws Exception {
+    ev.checkEvalErrorContains("Cannot compare structs", "struct(a = 1) < struct(a = 2)");
+    ev.checkEvalErrorContains("Cannot compare structs", "struct(a = 1) > struct(a = 2)");
+    ev.checkEvalErrorContains("Cannot compare structs", "struct(a = 1) <= struct(a = 2)");
+    ev.checkEvalErrorContains("Cannot compare structs", "struct(a = 1) >= struct(a = 2)");
+  }
+
+  @Test
+  public void testStructAccessingFieldsFromStarlark() throws Exception {
+    ev.exec("x = struct(a = 1, b = 2)", "x1 = x.a", "x2 = x.b");
+    assertThat(ev.lookup("x1")).isEqualTo(1);
+    assertThat(ev.lookup("x2")).isEqualTo(2);
+  }
+
+  @Test
+  public void testStructAccessingUnknownField() throws Exception {
+    ev.checkEvalErrorContains(
+        "'struct' value has no field or method 'c'\n" + "Available attributes: a, b",
+        "x = struct(a = 1, b = 2)",
+        "y = x.c");
+  }
+
+  @Test
+  public void testStructAccessingUnknownFieldWithArgs() throws Exception {
+    ev.checkEvalErrorContains(
+        "'struct' value has no field or method 'c'", "x = struct(a = 1, b = 2)", "y = x.c()");
+  }
+
+  @Test
+  public void testStructAccessingNonFunctionFieldWithArgs() throws Exception {
+    ev.checkEvalErrorContains(
+        "'int' object is not callable", "x = struct(a = 1, b = 2)", "x1 = x.a(1)");
+  }
+
+  @Test
+  public void testStructAccessingFunctionFieldWithArgs() throws Exception {
+    ev.exec("def f(x): return x+5", "x = struct(a = f, b = 2)", "x1 = x.a(1)");
+    assertThat(ev.lookup("x1")).isEqualTo(6);
+  }
+
+  @Test
+  public void testStructPosArgs() throws Exception {
+    ev.checkEvalErrorContains(
+        "struct() got unexpected positional argument", "x = struct(1, b = 2)");
+  }
+
+  @Test
+  public void testStructConcatenationFieldNames() throws Exception {
+    // TODO(fwe): cannot be handled by current testing suite
+    ev.exec(
+        "x = struct(a = 1, b = 2)", //
+        "y = struct(c = 1, d = 2)",
+        "z = x + y\n");
+    StructImpl z = (StructImpl) ev.lookup("z");
+    assertThat(z.getFieldNames()).containsExactly("a", "b", "c", "d");
+  }
+
+  @Test
+  public void testStructConcatenationFieldValues() throws Exception {
+    // TODO(fwe): cannot be handled by current testing suite
+    ev.exec(
+        "x = struct(a = 1, b = 2)", //
+        "y = struct(c = 1, d = 2)",
+        "z = x + y\n");
+    StructImpl z = (StructImpl) ev.lookup("z");
+    assertThat(z.getValue("a")).isEqualTo(1);
+    assertThat(z.getValue("b")).isEqualTo(2);
+    assertThat(z.getValue("c")).isEqualTo(1);
+    assertThat(z.getValue("d")).isEqualTo(2);
+  }
+
+  @Test
+  public void testStructConcatenationCommonFields() throws Exception {
+    ev.checkEvalErrorContains(
+        "cannot add struct instances with common field 'a'",
+        "x = struct(a = 1, b = 2)",
+        "y = struct(c = 1, a = 2)",
+        "z = x + y\n");
+  }
+
+  @Test
+  public void testConditionalStructConcatenation() throws Exception {
+    // TODO(fwe): cannot be handled by current testing suite
+    ev.exec(
+        "def func():",
+        "  x = struct(a = 1, b = 2)",
+        "  if True:",
+        "    x += struct(c = 1, d = 2)",
+        "  return x",
+        "x = func()");
+    StructImpl x = (StructImpl) ev.lookup("x");
+    assertThat(x.getValue("a")).isEqualTo(1);
+    assertThat(x.getValue("b")).isEqualTo(2);
+    assertThat(x.getValue("c")).isEqualTo(1);
+    assertThat(x.getValue("d")).isEqualTo(2);
+  }
+
+  @Test
+  public void testGetattrNoAttr() throws Exception {
+    ev.checkEvalErrorContains(
+        "'struct' value has no field or method 'b'\nAvailable attributes: a",
+        "s = struct(a='val')",
+        "getattr(s, 'b')");
+  }
+
+  @Test
+  public void testGetattr() throws Exception {
+    ev.exec("s = struct(a='val')", "x = getattr(s, 'a')", "y = getattr(s, 'b', 'def')");
+    assertThat(ev.lookup("x")).isEqualTo("val");
+    assertThat(ev.lookup("y")).isEqualTo("def");
+  }
+
+  @Test
+  public void testHasattr() throws Exception {
+    ev.exec(
+        "s = struct(a=1)", //
+        "x = hasattr(s, 'a')",
+        "y = hasattr(s, 'b')\n");
+    assertThat(ev.lookup("x")).isEqualTo(true);
+    assertThat(ev.lookup("y")).isEqualTo(false);
+  }
+
+  @Test
+  public void testStructStr() throws Exception {
+    assertThat(ev.eval("str(struct(x = 2, y = 3, z = 4))"))
+        .isEqualTo("struct(x = 2, y = 3, z = 4)");
+  }
+
+  @Test
+  public void testStructsInSets() throws Exception {
+    ev.exec("depset([struct(a='a')])");
+  }
+
+  @Test
+  public void testStructsInDicts() throws Exception {
+    ev.exec("d = {struct(a = 1): 'aa', struct(b = 2): 'bb'}");
+    assertThat(ev.eval("d[struct(a = 1)]")).isEqualTo("aa");
+    assertThat(ev.eval("d[struct(b = 2)]")).isEqualTo("bb");
+    assertThat(ev.eval("str([d[k] for k in d])")).isEqualTo("[\"aa\", \"bb\"]");
+
+    ev.checkEvalErrorContains("unhashable type: 'struct'", "{struct(a = []): 'foo'}");
+  }
+
+  @Test
+  public void testStructDictMembersAreMutable() throws Exception {
+    ev.exec(
+        "s = struct(x = {'a' : 1})", //
+        "s.x['b'] = 2\n");
+    assertThat(((StructImpl) ev.lookup("s")).getValue("x"))
+        .isEqualTo(ImmutableMap.of("a", 1, "b", 2));
+  }
+
+  @Test
+  public void testDepsetGoodCompositeItem() throws Exception {
+    ev.exec("def func():", "  return depset([struct(a='a')])", "s = func()");
+    ImmutableList<?> result = ((Depset) ev.lookup("s")).toList();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0)).isInstanceOf(StructImpl.class);
+  }
+
+  private static StructImpl makeStruct(String field, Object value) {
+    return StructProvider.STRUCT.create(ImmutableMap.of(field, value), "no field '%'");
+  }
+
+  private static StructImpl makeBigStruct(@Nullable Mutability mu) {
+    // struct(a=[struct(x={1:1}), ()], b=(), c={2:2})
+    return StructProvider.STRUCT.create(
+        ImmutableMap.<String, Object>of(
+            "a",
+                StarlarkList.<Object>of(
+                    mu,
+                    StructProvider.STRUCT.create(
+                        ImmutableMap.<String, Object>of("x", Dict.<Object, Object>of(mu, 1, 1)),
+                        "no field '%s'"),
+                    Tuple.of()),
+            "b", Tuple.of(),
+            "c", Dict.<Object, Object>of(mu, 2, 2)),
+        "no field '%s'");
+  }
+
+  @Test
+  public void testStructMutabilityShallow() throws Exception {
+    assertThat(EvalUtils.isImmutable(makeStruct("a", 1))).isTrue();
+  }
+
+  private static StarlarkList<Object> makeList(@Nullable Mutability mu) {
+    return StarlarkList.<Object>of(mu, 1, 2, 3);
+  }
+
+  @Test
+  public void testStructMutabilityDeep() throws Exception {
+    assertThat(EvalUtils.isImmutable(Tuple.<Object>of(makeList(null)))).isTrue();
+    assertThat(EvalUtils.isImmutable(makeStruct("a", makeList(null)))).isTrue();
+    assertThat(EvalUtils.isImmutable(makeBigStruct(null))).isTrue();
+
+    Mutability mu = Mutability.create("test");
+    assertThat(EvalUtils.isImmutable(Tuple.<Object>of(makeList(mu)))).isFalse();
+    assertThat(EvalUtils.isImmutable(makeStruct("a", makeList(mu)))).isFalse();
+    assertThat(EvalUtils.isImmutable(makeBigStruct(mu))).isFalse();
+  }
+
+  @Test
+  public void declaredProviders() throws Exception {
+    evalAndExport(ev, "data = provider()", "d = data(x = 1, y ='abc')", "d_x = d.x", "d_y = d.y");
+    assertThat(ev.lookup("d_x")).isEqualTo(1);
+    assertThat(ev.lookup("d_y")).isEqualTo("abc");
+    StarlarkProvider dataConstructor = (StarlarkProvider) ev.lookup("data");
+    StructImpl data = (StructImpl) ev.lookup("d");
+    assertThat(data.getProvider()).isEqualTo(dataConstructor);
+    assertThat(dataConstructor.isExported()).isTrue();
+    assertThat(dataConstructor.getPrintableName()).isEqualTo("data");
+    assertThat(dataConstructor.getKey()).isEqualTo(new StarlarkProvider.Key(FAKE_LABEL, "data"));
+  }
+
+  @Test
+  public void declaredProvidersConcatSuccess() throws Exception {
+    evalAndExport(
+        ev,
+        "data = provider()",
+        "dx = data(x = 1)",
+        "dy = data(y = 'abc')",
+        "dxy = dx + dy",
+        "x = dxy.x",
+        "y = dxy.y");
+    assertThat(ev.lookup("x")).isEqualTo(1);
+    assertThat(ev.lookup("y")).isEqualTo("abc");
+    StarlarkProvider dataConstructor = (StarlarkProvider) ev.lookup("data");
+    StructImpl dx = (StructImpl) ev.lookup("dx");
+    assertThat(dx.getProvider()).isEqualTo(dataConstructor);
+    StructImpl dy = (StructImpl) ev.lookup("dy");
+    assertThat(dy.getProvider()).isEqualTo(dataConstructor);
+  }
+
+  @Test
+  public void declaredProvidersConcatError() throws Exception {
+    evalAndExport(ev, "data1 = provider()", "data2 = provider()");
+
+    ev.checkEvalErrorContains(
+        "Cannot use '+' operator on instances of different providers (data1 and data2)",
+        "d1 = data1(x = 1)",
+        "d2 = data2(y = 2)",
+        "d = d1 + d2");
+  }
+
+  @Test
+  public void declaredProvidersWithFieldsConcatSuccess() throws Exception {
+    evalAndExport(
+        ev,
+        "data = provider(fields=['f1', 'f2'])",
+        "d1 = data(f1 = 4)",
+        "d2 = data(f2 = 5)",
+        "d3 = d1 + d2",
+        "f1 = d3.f1",
+        "f2 = d3.f2");
+    assertThat(ev.lookup("f1")).isEqualTo(4);
+    assertThat(ev.lookup("f2")).isEqualTo(5);
+  }
+
+  @Test
+  public void declaredProvidersWithFieldsConcatError() throws Exception {
+    evalAndExport(ev, "data1 = provider(fields=['f1', 'f2'])", "data2 = provider(fields=['f3'])");
+    ev.checkEvalErrorContains(
+        "Cannot use '+' operator on instances of different providers (data1 and data2)",
+        "d1 = data1(f1=1, f2=2)",
+        "d2 = data2(f3=3)",
+        "d = d1 + d2");
+  }
+
+  @Test
+  public void declaredProvidersWithOverlappingFieldsConcatError() throws Exception {
+    evalAndExport(ev, "data = provider(fields=['f1', 'f2'])");
+    ev.checkEvalErrorContains(
+        "cannot add struct instances with common field 'f1'",
+        "d1 = data(f1 = 4)",
+        "d2 = data(f1 = 5)",
+        "d1 + d2");
+  }
+
+  @Test
+  public void structsAsDeclaredProvidersTest() throws Exception {
+    evalAndExport(ev, "data = struct(x = 1)");
+    StructImpl data = (StructImpl) ev.lookup("data");
+    assertThat(StructProvider.STRUCT.isExported()).isTrue();
+    assertThat(data.getProvider()).isEqualTo(StructProvider.STRUCT);
+    assertThat(data.getProvider().getKey()).isEqualTo(StructProvider.STRUCT.getKey());
+  }
+
+  @Test
+  public void declaredProvidersDoc() throws Exception {
+    evalAndExport(ev, "data1 = provider(doc='foo')");
+  }
+
+  @Test
+  public void declaredProvidersBadTypeForDoc() throws Exception {
+    ev.checkEvalErrorContains("got value of type 'int', want 'string'", "provider(doc = 1)");
+  }
+
+  @Test
+  public void aspectAllAttrs() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):", //
+        "   pass",
+        "my_aspect = aspect(_impl, attr_aspects=['*'])");
+
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    assertThat(myAspect.getDefinition(AspectParameters.EMPTY).propagateAlong("foo")).isTrue();
+  }
+
+  @Test
+  public void aspectRequiredAspectProvidersSingle() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "cc = provider()",
+        "my_aspect = aspect(_impl, required_aspect_providers=['java', cc])");
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    RequiredProviders requiredProviders = myAspect.getDefinition(AspectParameters.EMPTY)
+        .getRequiredProvidersForAspects();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.ANY)).isTrue();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.EMPTY)).isFalse();
+    assertThat(
+            requiredProviders.isSatisfiedBy(
+                AdvertisedProviderSet.builder()
+                    .addStarlark(declared("cc"))
+                    .addStarlark("java")
+                    .build()))
+        .isTrue();
+    assertThat(
+            requiredProviders.isSatisfiedBy(
+                AdvertisedProviderSet.builder().addStarlark("cc").build()))
+        .isFalse();
+  }
+
+  @Test
+  public void aspectRequiredAspectProvidersAlternatives() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "cc = provider()",
+        "my_aspect = aspect(_impl, required_aspect_providers=[['java'], [cc]])");
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    RequiredProviders requiredProviders = myAspect.getDefinition(AspectParameters.EMPTY)
+        .getRequiredProvidersForAspects();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.ANY)).isTrue();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.EMPTY)).isFalse();
+    assertThat(
+            requiredProviders.isSatisfiedBy(
+                AdvertisedProviderSet.builder().addStarlark("java").build()))
+        .isTrue();
+    assertThat(
+            requiredProviders.isSatisfiedBy(
+                AdvertisedProviderSet.builder().addStarlark(declared("cc")).build()))
+        .isTrue();
+    assertThat(
+            requiredProviders.isSatisfiedBy(
+                AdvertisedProviderSet.builder().addStarlark("prolog").build()))
+        .isFalse();
+  }
+
+  @Test
+  public void aspectRequiredAspectProvidersEmpty() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "my_aspect = aspect(_impl, required_aspect_providers=[])");
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    RequiredProviders requiredProviders = myAspect.getDefinition(AspectParameters.EMPTY)
+        .getRequiredProvidersForAspects();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.ANY)).isFalse();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.EMPTY)).isFalse();
+  }
+
+  @Test
+  public void aspectRequiredAspectProvidersDefault() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):", //
+        "   pass",
+        "my_aspect = aspect(_impl)");
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    RequiredProviders requiredProviders = myAspect.getDefinition(AspectParameters.EMPTY)
+        .getRequiredProvidersForAspects();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.ANY)).isFalse();
+    assertThat(requiredProviders.isSatisfiedBy(AdvertisedProviderSet.EMPTY)).isFalse();
+  }
+
+  @Test
+  public void aspectProvides() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "y = provider()",
+        "my_aspect = aspect(_impl, provides = ['x', y])");
+    StarlarkDefinedAspect myAspect = (StarlarkDefinedAspect) ev.lookup("my_aspect");
+    AdvertisedProviderSet advertisedProviders = myAspect.getDefinition(AspectParameters.EMPTY)
+        .getAdvertisedProviders();
+    assertThat(advertisedProviders.canHaveAnyProvider()).isFalse();
+    assertThat(advertisedProviders.getStarlarkProviders())
+        .containsExactly(legacy("x"), declared("y"));
+  }
+
+  @Test
+  public void aspectProvidesError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):",
+        "   pass",
+        "y = provider()",
+        "my_aspect = aspect(_impl, provides = ['x', 1])");
+    MoreAsserts.assertContainsEvent(ev.getEventCollector(),
+        " Illegal argument: element in 'provides' is of unexpected type."
+            + " Should be list of providers, but got item of type int. ");
+  }
+
+  @Test
+  public void aspectDoc() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impl(target, ctx):", //
+        "   pass",
+        "my_aspect = aspect(_impl, doc='foo')");
+  }
+
+  @Test
+  public void aspectBadTypeForDoc() throws Exception {
+    registerDummyStarlarkFunction();
+    ev.checkEvalErrorContains("got value of type 'int', want 'string'", "aspect(impl, doc = 1)");
+  }
+
+  @Test
+  public void fancyExports() throws Exception {
+    evalAndExport(
+        ev,
+        "def _impla(target, ctx): pass",
+        "p, (a, p1) = [",
+        "   provider(),",
+        "   [ aspect(_impla),",
+        "     provider() ]",
+        "]");
+    StarlarkProvider p = (StarlarkProvider) ev.lookup("p");
+    StarlarkDefinedAspect a = (StarlarkDefinedAspect) ev.lookup("a");
+    StarlarkProvider p1 = (StarlarkProvider) ev.lookup("p1");
+    assertThat(p.getPrintableName()).isEqualTo("p");
+    assertThat(p.getKey()).isEqualTo(new StarlarkProvider.Key(FAKE_LABEL, "p"));
+    assertThat(p1.getPrintableName()).isEqualTo("p1");
+    assertThat(p1.getKey()).isEqualTo(new StarlarkProvider.Key(FAKE_LABEL, "p1"));
+    assertThat(a.getAspectClass()).isEqualTo(new StarlarkAspectClass(FAKE_LABEL, "a"));
+  }
+
+  @Test
+  public void multipleTopLevels() throws Exception {
+    evalAndExport(
+        ev,
+        "p = provider()", //
+        "p1 = p");
+    StarlarkProvider p = (StarlarkProvider) ev.lookup("p");
+    StarlarkProvider p1 = (StarlarkProvider) ev.lookup("p1");
+    assertThat(p).isEqualTo(p1);
+    assertThat(p.getKey()).isEqualTo(new StarlarkProvider.Key(FAKE_LABEL, "p"));
+    assertThat(p1.getKey()).isEqualTo(new StarlarkProvider.Key(FAKE_LABEL, "p"));
+  }
+
+  @Test
+  public void providerWithFields() throws Exception {
+    evalAndExport(
+        ev,
+        "p = provider(fields = ['x', 'y'])", //
+        "p1 = p(x = 1, y = 2)",
+        "x = p1.x",
+        "y = p1.y");
+    StarlarkProvider p = (StarlarkProvider) ev.lookup("p");
+    StarlarkInfo p1 = (StarlarkInfo) ev.lookup("p1");
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(ev.lookup("x")).isEqualTo(1);
+    assertThat(ev.lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsDict() throws Exception {
+    evalAndExport(
+        ev,
+        "p = provider(fields = { 'x' : 'I am x', 'y' : 'I am y'})",
+        "p1 = p(x = 1, y = 2)",
+        "x = p1.x",
+        "y = p1.y");
+    StarlarkProvider p = (StarlarkProvider) ev.lookup("p");
+    StarlarkInfo p1 = (StarlarkInfo) ev.lookup("p1");
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(ev.lookup("x")).isEqualTo(1);
+    assertThat(ev.lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsOptional() throws Exception {
+    evalAndExport(
+        ev,
+        "p = provider(fields = ['x', 'y'])", //
+        "p1 = p(y = 2)",
+        "y = p1.y");
+    StarlarkProvider p = (StarlarkProvider) ev.lookup("p");
+    StarlarkInfo p1 = (StarlarkInfo) ev.lookup("p1");
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(ev.lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsOptionalError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "p = provider(fields = ['x', 'y'])", //
+        "p1 = p(y = 2)",
+        "x = p1.x");
+    MoreAsserts.assertContainsEvent(
+        ev.getEventCollector(), " 'p' value has no field or method 'x'");
+  }
+
+  @Test
+  public void providerWithExtraFieldsError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(ev, "p = provider(fields = ['x', 'y'])", "p1 = p(x = 1, y = 2, z = 3)");
+    MoreAsserts.assertContainsEvent(
+        ev.getEventCollector(), "unexpected keyword z in call to instantiate provider p");
+  }
+
+  @Test
+  public void providerWithEmptyFieldsError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "p = provider(fields = [])", //
+        "p1 = p(x = 1, y = 2, z = 3)");
+    MoreAsserts.assertContainsEvent(
+        ev.getEventCollector(), "unexpected keywords x, y, z in call to instantiate provider p");
+  }
+
+  @Test
+  public void providerWithDuplicateFieldsError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        ev,
+        "p = provider(fields = ['a', 'b'])", //
+        "p(a = 1, b = 2, **dict(b = 3))");
+    MoreAsserts.assertContainsEvent(
+        ev.getEventCollector(),
+        "got multiple values for parameter b in call to instantiate provider p");
+  }
+
+  @Test
+  public void starTheOnlyAspectArg() throws Exception {
+    ev.checkEvalErrorContains(
+        "'*' must be the only string in 'attr_aspects' list",
+        "def _impl(target, ctx):",
+        "   pass",
+        "aspect(_impl, attr_aspects=['*', 'foo'])");
+  }
+
+  @Test
+  public void testMandatoryConfigParameterForExecutableLabels() throws Exception {
+    scratch.file("third_party/foo/extension.bzl",
+      "def _main_rule_impl(ctx):",
+      "    pass",
+      "my_rule = rule(_main_rule_impl,",
+      "    attrs = { ",
+      "        'exe' : attr.label(executable = True, allow_files = True),",
+      "    },",
+      ")"
+    );
+    scratch.file("third_party/foo/BUILD",
+      "load(':extension.bzl', 'my_rule')",
+      "my_rule(name = 'main', exe = ':tool.sh')"
+    );
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> createRuleContext("//third_party/foo:main"));
+    assertThat(expected).hasMessageThat()
+        .contains("cfg parameter is mandatory when executable=True is provided.");
+  }
+
+  @Test
+  public void testRuleAddToolchain() throws Exception {
+    scratch.file("test/BUILD", "toolchain_type(name = 'my_toolchain_type')");
+    evalAndExport(
+        ev,
+        "def impl(ctx): return None",
+        "r1 = rule(impl, toolchains=['//test:my_toolchain_type'])");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.getRequiredToolchains()).containsExactly(makeLabel("//test:my_toolchain_type"));
+  }
+
+  @Test
+  public void testRuleAddExecutionConstraints() throws Exception {
+    registerDummyStarlarkFunction();
+    scratch.file("test/BUILD", "toolchain_type(name = 'my_toolchain_type')");
+    evalAndExport(
+        ev,
+        "r1 = rule(",
+        "  implementation = impl,",
+        "  toolchains=['//test:my_toolchain_type'],",
+        "  exec_compatible_with=['//constraint:cv1', '//constraint:cv2'],",
+        ")");
+    RuleClass c = ((StarlarkRuleFunction) ev.lookup("r1")).getRuleClass();
+    assertThat(c.getExecutionPlatformConstraints())
+        .containsExactly(makeLabel("//constraint:cv1"), makeLabel("//constraint:cv2"));
+  }
+
+  @Test
+  public void testRuleAddExecGroup() throws Exception {
+    setStarlarkSemanticsOptions("--experimental_exec_groups=true");
+
+    registerDummyStarlarkFunction();
+    scratch.file("test/BUILD", "toolchain_type(name = 'my_toolchain_type')");
+    evalAndExport(
+        ev,
+        "plum = rule(",
+        "  implementation = impl,",
+        "  exec_groups = {",
+        "    'group': exec_group(",
+        "      toolchains=['//test:my_toolchain_type'],",
+        "      exec_compatible_with=['//constraint:cv1', '//constraint:cv2'],",
+        "    ),",
+        "  },",
+        ")");
+    RuleClass plum = ((StarlarkRuleFunction) ev.lookup("plum")).getRuleClass();
+    assertThat(plum.getRequiredToolchains()).isEmpty();
+    assertThat(plum.getExecGroups().get("group").requiredToolchains())
+        .containsExactly(makeLabel("//test:my_toolchain_type"));
+    assertThat(plum.getExecutionPlatformConstraints()).isEmpty();
+    assertThat(plum.getExecGroups().get("group").execCompatibleWith())
+        .containsExactly(makeLabel("//constraint:cv1"), makeLabel("//constraint:cv2"));
+  }
+
+  @Test
+  public void testRuleFunctionReturnsNone() throws Exception {
+    scratch.file("test/rule.bzl",
+        "def _impl(ctx):",
+        "  pass",
+        "foo_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {'params': attr.string_list()},",
+        ")");
+    scratch.file("test/BUILD",
+        "load(':rule.bzl', 'foo_rule')",
+        "r = foo_rule(name='foo')",  // Custom rule should return None
+        "c = cc_library(name='cc')", // Native rule should return None
+        "",
+        "foo_rule(",
+        "    name='check',",
+        "    params = [type(r), type(c)]",
+        ")");
+    invalidatePackages();
+    StarlarkRuleContext context = createRuleContext("//test:check");
+    @SuppressWarnings("unchecked")
+    StarlarkList<Object> params = (StarlarkList<Object>) context.getAttr().getValue("params");
+    assertThat(params.get(0)).isEqualTo("NoneType");
+    assertThat(params.get(1)).isEqualTo("NoneType");
+  }
+
+  @Test
+  public void testTypeOfStruct() throws Exception {
+    ev.exec("p = type(struct)", "s = type(struct())");
+
+    assertThat(ev.lookup("p")).isEqualTo("Provider");
+    assertThat(ev.lookup("s")).isEqualTo("struct");
+  }
+
+  @Test
+  public void testCreateExecGroup() throws Exception {
+    setStarlarkSemanticsOptions("--experimental_exec_groups=true");
+
+    scratch.file("test/BUILD", "toolchain_type(name = 'my_toolchain_type')");
+    evalAndExport(
+        ev,
+        "group = exec_group(",
+        "  toolchains=['//test:my_toolchain_type'],",
+        "  exec_compatible_with=['//constraint:cv1', '//constraint:cv2'],",
+        ")");
+    ExecGroup group = ((ExecGroup) ev.lookup("group"));
+    assertThat(group.requiredToolchains()).containsExactly(makeLabel("//test:my_toolchain_type"));
+    assertThat(group.execCompatibleWith())
+        .containsExactly(makeLabel("//constraint:cv1"), makeLabel("//constraint:cv2"));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java
new file mode 100644
index 0000000..2567bac
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java
@@ -0,0 +1,2974 @@
+// 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.devtools.build.lib.analysis.ToolchainCollection.DEFAULT_EXEC_GROUP_NAME;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ActionsProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ExecGroupCollection;
+import com.google.devtools.build.lib.analysis.ResolvedToolchainContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.StarlarkAction;
+import com.google.devtools.build.lib.analysis.configuredtargets.FileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.analysis.util.MockRule;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.packages.Provider;
+import com.google.devtools.build.lib.packages.StarlarkInfo;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StarlarkProviderIdentifier;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.rules.java.JavaInfo;
+import com.google.devtools.build.lib.rules.java.JavaSourceJarsProvider;
+import com.google.devtools.build.lib.rules.python.PyProviderUtils;
+import com.google.devtools.build.lib.starlark.util.BazelEvaluationTestCase;
+import com.google.devtools.build.lib.syntax.Dict;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.Sequence;
+import com.google.devtools.build.lib.syntax.Starlark;
+import com.google.devtools.build.lib.syntax.StarlarkList;
+import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.ArrayList;
+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 for {@link StarlarkRuleContext}. */
+@RunWith(JUnit4.class)
+public final class StarlarkRuleContextTest extends BuildViewTestCase {
+
+  private StarlarkRuleContext createRuleContext(String label) throws Exception {
+    return new StarlarkRuleContext(
+        getRuleContextForStarlark(getConfiguredTarget(label)), null, getStarlarkSemantics());
+  }
+
+  private final EvaluationTestCase ev = new BazelEvaluationTestCase();
+
+  /** A test rule that exercises the semantics of mandatory providers. */
+  private static final MockRule TESTING_RULE_FOR_MANDATORY_PROVIDERS =
+      () ->
+          MockRule.define(
+              "testing_rule_for_mandatory_providers",
+              (builder, env) ->
+                  builder
+                      .setUndocumented()
+                      .add(attr("srcs", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))
+                      .add(
+                          attr("deps", LABEL_LIST)
+                              .legacyAllowAnyFileType()
+                              .mandatoryProvidersList(
+                                  ImmutableList.of(
+                                      ImmutableList.of(StarlarkProviderIdentifier.forLegacy("a")),
+                                      ImmutableList.of(
+                                          StarlarkProviderIdentifier.forLegacy("b"),
+                                          StarlarkProviderIdentifier.forLegacy("c"))))));
+
+  @Override
+  protected ConfiguredRuleClassProvider createRuleClassProvider() {
+    ConfiguredRuleClassProvider.Builder builder =
+        new ConfiguredRuleClassProvider.Builder()
+            .addRuleDefinition(TESTING_RULE_FOR_MANDATORY_PROVIDERS);
+    TestRuleClassProvider.addStandardRules(builder);
+    return builder.build();
+  }
+
+  @Before
+  public final void setupMyInfoAndGenerateBuildFile() throws Exception {
+    scratch.file("myinfo/myinfo.bzl", "MyInfo = provider()");
+    scratch.file("myinfo/BUILD");
+    scratch.file(
+        "foo/BUILD",
+        "package(features = ['-f1', 'f2', 'f3'])",
+        "genrule(name = 'foo',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = ['a.txt', 'b.img'],",
+        "  tools = ['t.exe'],",
+        "  outs = ['c.txt'])",
+        "genrule(name = 'foo2',",
+        "  cmd = 'dummy_cmd',",
+        "  outs = ['e.txt'])",
+        "genrule(name = 'bar',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = [':jl', ':gl'],",
+        "  outs = ['d.txt'])",
+        "java_library(name = 'jl',",
+        "  srcs = ['a.java'])",
+        "android_library(name = 'androidlib',",
+        "  srcs = ['a.java'])",
+        "java_import(name = 'asr',",
+        "  jars = [ 'asr.jar' ],",
+        "  srcjar = 'asr-src.jar',",
+        ")",
+        "genrule(name = 'gl',",
+        "  cmd = 'touch $(OUTS)',",
+        "  srcs = ['a.go'],",
+        "  outs = [ 'gl.a', 'gl.gcgox', ],",
+        "  output_to_bindir = 1,",
+        ")",
+        "cc_library(name = 'cc_with_features',",
+        "           srcs = ['dummy.cc'],",
+        "           features = ['f1', '-f3'],",
+        ")");
+  }
+
+  private void setRuleContext(StarlarkRuleContext ctx) throws Exception {
+    ev.update("ruleContext", ctx);
+  }
+
+  private void setUpAttributeErrorTest() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:macros.bzl', 'macro_native_rule', 'macro_starlark_rule', 'starlark_rule')",
+        "macro_native_rule(name = 'm_native',",
+        "  deps = [':jlib'])",
+        "macro_starlark_rule(name = 'm_starlark',",
+        "  deps = [':jlib'])",
+        "java_library(name = 'jlib',",
+        "  srcs = ['bla.java'])",
+        "cc_library(name = 'cclib',",
+        "  deps = [':jlib'])",
+        "starlark_rule(name = 'skyrule',",
+        "  deps = [':jlib'])");
+    scratch.file(
+        "test/macros.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(providers = ['some_provider'], allow_files=True)",
+        "  }",
+        ")",
+        "def macro_native_rule(name, deps): ",
+        "  native.cc_library(name = name, deps = deps)",
+        "def macro_starlark_rule(name, deps):",
+        "  starlark_rule(name = name, deps = deps)");
+    reporter.removeHandler(failFastHandler);
+  }
+
+  @Test
+  public void hasCorrectLocationForRuleAttributeError_NativeRuleWithMacro() throws Exception {
+    setUpAttributeErrorTest();
+    assertThrows(Exception.class, () -> createRuleContext("//test:m_native"));
+    assertContainsEvent("misplaced here");
+    // Skip the part of the error message that has details about the allowed deps since the mocks
+    // for the mac tests might have different values for them.
+    assertContainsEvent(
+        ". Since this "
+            + "rule was created by the macro 'macro_native_rule', the error might have been caused "
+            + "by the macro implementation");
+  }
+
+  @Test
+  public void hasCorrectLocationForRuleAttributeError_StarlarkRuleWithMacro() throws Exception {
+    setUpAttributeErrorTest();
+    assertThrows(Exception.class, () -> createRuleContext("//test:m_starlark"));
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:4:20: in deps attribute of starlark_rule rule "
+            + "//test:m_starlark: '//test:jlib' does not have mandatory providers:"
+            + " 'some_provider'. "
+            + "Since this rule was created by the macro 'macro_starlark_rule', the error might "
+            + "have been caused by the macro implementation");
+  }
+
+  @Test
+  public void hasCorrectLocationForRuleAttributeError_NativeRule() throws Exception {
+    setUpAttributeErrorTest();
+    assertThrows(Exception.class, () -> createRuleContext("//test:cclib"));
+    assertContainsEvent("misplaced here");
+    // Skip the part of the error message that has details about the allowed deps since the mocks
+    // for the mac tests might have different values for them.
+    assertDoesNotContainEvent("Since this rule was created by the macro");
+  }
+
+  @Test
+  public void hasCorrectLocationForRuleAttributeError_StarlarkRule() throws Exception {
+    setUpAttributeErrorTest();
+    assertThrows(Exception.class, () -> createRuleContext("//test:skyrule"));
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:10:14: in deps attribute of "
+            + "starlark_rule rule //test:skyrule: '//test:jlib' does not have mandatory providers: "
+            + "'some_provider'");
+  }
+
+  @Test
+  public void testMandatoryProvidersListWithStarlark() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'starlark_rule', 'my_rule', 'my_other_rule')",
+        "my_rule(name = 'mylib',",
+        "  srcs = ['a.py'])",
+        "starlark_rule(name = 'skyrule1',",
+        "  deps = [':mylib'])",
+        "my_other_rule(name = 'my_other_lib',",
+        "  srcs = ['a.py'])",
+        "starlark_rule(name = 'skyrule2',",
+        "  deps = [':my_other_lib'])");
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(providers = [['a'], ['b', 'c']],",
+        "    allow_files=True)",
+        "  }",
+        ")",
+        "def my_rule_impl(ctx):",
+        "  return struct(a = [])",
+        "my_rule = rule(implementation = my_rule_impl, ",
+        "  attrs = { 'srcs' : attr.label_list(allow_files=True)})",
+        "def my_other_rule_impl(ctx):",
+        "  return struct(b = [])",
+        "my_other_rule = rule(implementation = my_other_rule_impl, ",
+        "  attrs = { 'srcs' : attr.label_list(allow_files=True)})");
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test:skyrule1")).isNotNull();
+
+    assertThrows(Exception.class, () -> createRuleContext("//test:skyrule2"));
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:8:14: in deps attribute of "
+            + "starlark_rule rule //test:skyrule2: '//test:my_other_lib' does not have "
+            + "mandatory providers: 'a' or 'c'");
+  }
+
+  @Test
+  public void testMandatoryProvidersListWithNative() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule', 'my_other_rule')",
+        "my_rule(name = 'mylib',",
+        "  srcs = ['a.py'])",
+        "testing_rule_for_mandatory_providers(name = 'skyrule1',",
+        "  deps = [':mylib'])",
+        "my_other_rule(name = 'my_other_lib',",
+        "  srcs = ['a.py'])",
+        "testing_rule_for_mandatory_providers(name = 'skyrule2',",
+        "  deps = [':my_other_lib'])");
+    scratch.file(
+        "test/rules.bzl",
+        "def my_rule_impl(ctx):",
+        "  return struct(a = [])",
+        "my_rule = rule(implementation = my_rule_impl, ",
+        "  attrs = { 'srcs' : attr.label_list(allow_files=True)})",
+        "def my_other_rule_impl(ctx):",
+        "  return struct(b = [])",
+        "my_other_rule = rule(implementation = my_other_rule_impl, ",
+        "  attrs = { 'srcs' : attr.label_list(allow_files=True)})");
+    reporter.removeHandler(failFastHandler);
+    assertThat(getConfiguredTarget("//test:skyrule1")).isNotNull();
+
+    assertThrows(Exception.class, () -> createRuleContext("//test:skyrule2"));
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:8:37: in deps attribute of "
+            + "testing_rule_for_mandatory_providers rule //test:skyrule2: '//test:my_other_lib' "
+            + "does not have mandatory providers: 'a' or 'c'");
+  }
+
+  /* Sharing setup code between the testPackageBoundaryError*() methods is not possible since the
+   * errors already happen when loading the file. Consequently, all tests would fail at the same
+   * statement. */
+  @Test
+  public void testPackageBoundaryError_NativeRule() throws Exception {
+    scratch.file("test/BUILD", "cc_library(name = 'cclib',", "  srcs = ['sub/my_sub_lib.h'])");
+    scratch.file("test/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:cclib");
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:1:11: Label '//test:sub/my_sub_lib.h' is invalid because "
+            + "'test/sub' is a subpackage; perhaps you meant to put the colon here: "
+            + "'//test/sub:my_sub_lib.h'?");
+  }
+
+  @Test
+  public void testPackageBoundaryError_StarlarkRule() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:macros.bzl', 'starlark_rule')",
+        "starlark_rule(name = 'skyrule',",
+        "  srcs = ['sub/my_sub_lib.h'])");
+    scratch.file("test/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    scratch.file(
+        "test/macros.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'srcs': attr.label_list(allow_files=True)",
+        "  }",
+        ")");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:skyrule");
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:2:14: Label '//test:sub/my_sub_lib.h' is invalid because "
+            + "'test/sub' is a subpackage; perhaps you meant to put the colon here: "
+            + "'//test/sub:my_sub_lib.h'?");
+  }
+
+  @Test
+  public void testPackageBoundaryError_StarlarkMacro() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:macros.bzl', 'macro_starlark_rule')",
+        "macro_starlark_rule(name = 'm_starlark',",
+        "  srcs = ['sub/my_sub_lib.h'])");
+    scratch.file("test/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    scratch.file(
+        "test/macros.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'srcs': attr.label_list(allow_files=True)",
+        "  }",
+        ")",
+        "def macro_starlark_rule(name, srcs=[]):",
+        "  starlark_rule(name = name, srcs = srcs)");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:m_starlark");
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:2:20: Label '//test:sub/my_sub_lib.h' is invalid because"
+            + " 'test/sub' is a subpackage; perhaps you meant to put the colon here: "
+            + "'//test/sub:my_sub_lib.h'?");
+  }
+
+  /* The error message for this case used to be wrong. */
+  @Test
+  public void testPackageBoundaryError_ExternalRepository_Boundary() throws Exception {
+    scratch.file("r/WORKSPACE");
+    scratch.file("r/BUILD");
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='r')")
+            .build());
+    scratch.file("BUILD", "cc_library(name = 'cclib',", "  srcs = ['r/my_sub_lib.h'])");
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//:cclib");
+    assertContainsEvent(
+        "/workspace/BUILD:1:11: Label '//:r/my_sub_lib.h' is invalid because "
+            + "'@r//' is a subpackage");
+  }
+
+  /* The error message for this case used to be wrong. */
+  @Test
+  public void testPackageBoundaryError_ExternalRepository_EntirelyInside() throws Exception {
+    scratch.file("/r/WORKSPACE");
+    scratch.file("/r/BUILD", "cc_library(name = 'cclib',", "  srcs = ['sub/my_sub_lib.h'])");
+    scratch.file("/r/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='/r')")
+            .build());
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("@r//:cclib");
+    assertContainsEvent(
+        "/external/r/BUILD:1:11: Label '@r//:sub/my_sub_lib.h' is invalid because "
+            + "'@r//sub' is a subpackage; perhaps you meant to put the colon here: "
+            + "'@r//sub:my_sub_lib.h'?");
+  }
+
+  /*
+   * Making the location in BUILD file the default for "crosses boundary of subpackage" errors does
+   * not work in this case since the error actually happens in the bzl file. However, because of
+   * the current design, we can neither show the location in the bzl file nor display both
+   * locations (BUILD + bzl).
+   *
+   * Since this case is less common than having such an error in a BUILD file, we can live
+   * with it.
+   */
+  @Test
+  public void testPackageBoundaryError_StarlarkMacroWithErrorInBzlFile() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:macros.bzl', 'macro_starlark_rule')",
+        "macro_starlark_rule(name = 'm_starlark')");
+    scratch.file("test/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    scratch.file(
+        "test/macros.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'srcs': attr.label_list(allow_files=True)",
+        "  }",
+        ")",
+        "def macro_starlark_rule(name, srcs=[]):",
+        "  starlark_rule(name = name, srcs = srcs + ['sub/my_sub_lib.h'])");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:m_starlark");
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:2:20: Label '//test:sub/my_sub_lib.h' "
+            + "is invalid because 'test/sub' is a subpackage");
+  }
+
+  @Test
+  public void testPackageBoundaryError_NativeMacro() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:macros.bzl', 'macro_native_rule')",
+        "macro_native_rule(name = 'm_native',",
+        "  srcs = ['sub/my_sub_lib.h'])");
+    scratch.file("test/sub/BUILD", "cc_library(name = 'my_sub_lib', srcs = ['my_sub_lib.h'])");
+    scratch.file(
+        "test/macros.bzl",
+        "def macro_native_rule(name, deps=[], srcs=[]): ",
+        "  native.cc_library(name = name, deps = deps, srcs = srcs)");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:m_native");
+    assertContainsEvent(
+        "ERROR /workspace/test/BUILD:2:18: Label '//test:sub/my_sub_lib.h' "
+            + "is invalid because 'test/sub' is a subpackage");
+  }
+
+  @Test
+  public void shouldGetPrerequisiteArtifacts() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.files.srcs");
+    assertArtifactList(result, ImmutableList.of("a.txt", "b.img"));
+  }
+
+  private static void assertArtifactList(Object result, List<String> artifacts) {
+    assertThat(result).isInstanceOf(Sequence.class);
+    Sequence<?> resultList = (Sequence) result;
+    assertThat(resultList).hasSize(artifacts.size());
+    int i = 0;
+    for (String artifact : artifacts) {
+      assertThat(((Artifact) resultList.get(i++)).getFilename()).isEqualTo(artifact);
+    }
+  }
+
+  @Test
+  public void shouldGetPrerequisites() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:bar");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.srcs");
+    // Check for a known provider
+    TransitiveInfoCollection tic1 = (TransitiveInfoCollection) ((Sequence) result).get(0);
+    assertThat(JavaInfo.getProvider(JavaSourceJarsProvider.class, tic1)).isNotNull();
+    // Check an unimplemented provider too
+    assertThat(PyProviderUtils.hasLegacyProvider(tic1)).isFalse();
+  }
+
+  @Test
+  public void shouldGetPrerequisite() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:asr");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.srcjar");
+    TransitiveInfoCollection tic = (TransitiveInfoCollection) result;
+    assertThat(tic).isInstanceOf(FileConfiguredTarget.class);
+    assertThat(tic.getLabel().getName()).isEqualTo("asr-src.jar");
+  }
+
+  @Test
+  public void testGetRuleAttributeListType() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.outs");
+    assertThat(result).isInstanceOf(Sequence.class);
+  }
+
+  @Test
+  public void testGetRuleSelect() throws Exception {
+    scratch.file("test/starlark/BUILD");
+    scratch.file(
+        "test/starlark/rulestr.bzl", "def rule_dict(name):", "  return native.existing_rule(name)");
+
+    scratch.file(
+        "test/getrule/BUILD",
+        "load('//test/starlark:rulestr.bzl', 'rule_dict')",
+        "cc_library(name ='x', ",
+        "  srcs = select({'//conditions:default': []})",
+        ")",
+        "rule_dict('x')");
+
+    // Parse the BUILD file, to make sure select() makes it out of native.rule().
+    createRuleContext("//test/getrule:x");
+  }
+
+  @Test
+  public void testExistingRuleReturnNone() throws Exception {
+    scratch.file(
+        "test/rulestr.bzl",
+        "def test_rule(name, x):",
+        "  print(native.existing_rule(x))",
+        "  if native.existing_rule(x) == None:",
+        "    native.cc_library(name = name)");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rulestr.bzl', 'test_rule')",
+        "test_rule('a', 'does not exist')",
+        "test_rule('b', 'BUILD')");
+
+    assertThat(getConfiguredTarget("//test:a")).isNotNull();
+    assertThat(getConfiguredTarget("//test:b")).isNotNull();
+  }
+
+  @Test
+  public void existingRuleWithSelect() throws Exception {
+    scratch.file(
+        "test/existing_rule.bzl",
+        "def macro():",
+        "  s = select({'//foo': ['//bar']})",
+        "  native.cc_library(name = 'x', srcs = s)",
+        "  print(native.existing_rule('x')['srcs'])");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:existing_rule.bzl', 'macro')",
+        "macro()",
+        "cc_library(name = 'a', srcs = [])");
+    getConfiguredTarget("//test:a");
+    assertContainsEvent("select({Label(\"//foo:foo\"): [Label(\"//bar:bar\")]})");
+  }
+
+  @Test
+  public void testGetRule() throws Exception {
+    scratch.file("test/starlark/BUILD");
+    scratch.file(
+        "test/starlark/rulestr.bzl",
+        "def rule_dict(name):",
+        "  return native.existing_rule(name)",
+        "def rules_dict():",
+        "  return native.existing_rules()",
+        "def nop(ctx):",
+        "  pass",
+        "nop_rule = rule(attrs = {'x': attr.label()}, implementation = nop)",
+        "consume_rule = rule(attrs = {'s': attr.string_list()}, implementation = nop)");
+
+    scratch.file(
+        "test/getrule/BUILD",
+        "load('//test/starlark:rulestr.bzl', 'rules_dict', 'rule_dict', 'nop_rule',"
+            + "'consume_rule')",
+        "genrule(name = 'a', outs = ['a.txt'], ",
+        "        licenses = ['notice'],",
+        "        output_to_bindir = False,",
+        "        tools = [ '//test:bla' ], cmd = 'touch $@')",
+        "nop_rule(name = 'c', x = ':a')",
+        "rlist= rules_dict()",
+        "consume_rule(name = 'all_str', s = [rlist['a']['kind'], rlist['a']['name'], ",
+        "                                    rlist['c']['kind'], rlist['c']['name']])",
+        "adict = rule_dict('a')",
+        "cdict = rule_dict('c')",
+        "consume_rule(name = 'a_str', ",
+        "             s = [adict['kind'], adict['name'], adict['outs'][0], adict['tools'][0]])",
+        "consume_rule(name = 'genrule_attr', ",
+        "             s = adict.keys())",
+        "consume_rule(name = 'c_str', s = [cdict['kind'], cdict['name'], cdict['x']])");
+
+    StarlarkRuleContext allContext = createRuleContext("//test/getrule:all_str");
+    setRuleContext(allContext);
+    List<?> result = (List) ev.eval("ruleContext.attr.s");
+    assertThat(result).containsExactly("genrule", "a", "nop_rule", "c");
+
+    setRuleContext(createRuleContext("//test/getrule:a_str"));
+    result = (List) ev.eval("ruleContext.attr.s");
+    assertThat(result).containsExactly("genrule", "a", ":a.txt", "//test:bla");
+
+    setRuleContext(createRuleContext("//test/getrule:c_str"));
+    result = (List) ev.eval("ruleContext.attr.s");
+    assertThat(result).containsExactly("nop_rule", "c", ":a");
+
+    setRuleContext(createRuleContext("//test/getrule:genrule_attr"));
+    result = (List) ev.eval("ruleContext.attr.s");
+    assertThat(result)
+        .containsAtLeast(
+            "name",
+            "visibility",
+            "transitive_configs",
+            "tags",
+            "generator_name",
+            "generator_function",
+            "generator_location",
+            "features",
+            "compatible_with",
+            "restricted_to",
+            "srcs",
+            "tools",
+            "toolchains",
+            "outs",
+            "cmd",
+            "output_to_bindir",
+            "local",
+            "message",
+            "executable",
+            "stamp",
+            "heuristic_label_expansion",
+            "kind");
+  }
+
+  @Test
+  public void testExistingRuleDictIsMutable() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('inc.bzl', 'f')", //
+        "f()");
+    scratch.file(
+        "test/inc.bzl", //
+        "def f():",
+        "  native.config_setting(name='x', define_values={'key': 'value'})",
+        "  r = native.existing_rule('x')",
+        "  r['define_values']['key'] = 123"); // mutate the dict
+
+    // Logically this belongs among the loading-phase tests of existing_rules. Where are they?
+    assertThat(getConfiguredTarget("//test:BUILD")).isNotNull(); // no error
+  }
+
+  @Test
+  public void testGetRuleAttributeListValue() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.outs");
+    assertThat(((Sequence) result)).hasSize(1);
+  }
+
+  @Test
+  public void testGetRuleAttributeListValueNoGet() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.outs");
+    assertThat(((Sequence) result)).hasSize(1);
+  }
+
+  @Test
+  public void testGetRuleAttributeStringTypeValue() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.cmd");
+    assertThat((String) result).isEqualTo("dummy_cmd");
+  }
+
+  @Test
+  public void testGetRuleAttributeStringTypeValueNoGet() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.attr.cmd");
+    assertThat((String) result).isEqualTo("dummy_cmd");
+  }
+
+  @Test
+  public void testGetRuleAttributeBadAttributeName() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains("No attribute 'bad'", "ruleContext.attr.bad");
+  }
+
+  @Test
+  public void testGetLabel() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.label");
+    assertThat(((Label) result).toString()).isEqualTo("//foo:foo");
+  }
+
+  @Test
+  public void testRuleError() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains("message", "fail('message')");
+  }
+
+  @Test
+  public void testAttributeError() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains("attribute srcs: message", "fail(attr='srcs', msg='message')");
+  }
+
+  @Test
+  public void testGetExecutablePrerequisite() throws Exception {
+    setRuleContext(createRuleContext("//foo:androidlib"));
+    Object result = ev.eval("ruleContext.executable._idlclass");
+    assertThat(((Artifact) result).getFilename()).matches("^IdlClass(\\.exe){0,1}$");
+  }
+
+  @Test
+  public void testCreateSpawnActionArgumentsWithExecutableFilesToRunProvider() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:androidlib");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['--a','--b'],",
+        "  executable = ruleContext.executable._idlclass)");
+    StarlarkAction action =
+        (StarlarkAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getCommandFilename()).matches("^.*/IdlClass(\\.exe){0,1}$");
+  }
+
+  @Test
+  public void testCreateStarlarkActionArgumentsWithUnusedInputsList() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  executable = 'executable',",
+        "  unused_inputs_list = ruleContext.files.srcs[0])");
+    StarlarkAction action =
+        (StarlarkAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getUnusedInputsList()).isPresent();
+    assertThat(action.getUnusedInputsList().get().getFilename()).isEqualTo("a.txt");
+    assertThat(action.discoversInputs()).isTrue();
+    assertThat(action.isShareable()).isFalse();
+  }
+
+  @Test
+  public void testCreateStarlarkActionArgumentsWithoutUnusedInputsList() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  executable = 'executable',",
+        "  unused_inputs_list = None)");
+    StarlarkAction action =
+        (StarlarkAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getUnusedInputsList()).isEmpty();
+    assertThat(action.discoversInputs()).isFalse();
+  }
+
+  @Test
+  public void testOutputs() throws Exception {
+    setRuleContext(createRuleContext("//foo:bar"));
+    Iterable<?> result = (Iterable) ev.eval("ruleContext.outputs.outs");
+    assertThat(((Artifact) Iterables.getOnlyElement(result)).getFilename()).isEqualTo("d.txt");
+  }
+
+  @Test
+  public void testStarlarkRuleContextGetDefaultShellEnv() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.configuration.default_shell_env");
+    assertThat(result).isInstanceOf(Dict.class);
+  }
+
+  @Test
+  public void testCheckPlaceholders() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.check_placeholders('%{name}', ['name'])");
+    assertThat(result).isEqualTo(true);
+  }
+
+  @Test
+  public void testCheckPlaceholdersBadPlaceholder() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.check_placeholders('%{name}', ['abc'])");
+    assertThat(result).isEqualTo(false);
+  }
+
+  @Test
+  public void testExpandMakeVariables() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.expand_make_variables('cmd', '$(ABC)', {'ABC': 'DEF'})");
+    assertThat(result).isEqualTo("DEF");
+  }
+
+  @Test
+  public void testExpandMakeVariablesShell() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.expand_make_variables('cmd', '$$ABC', {})");
+    assertThat(result).isEqualTo("$ABC");
+  }
+
+  private void setUpMakeVarToolchain() throws Exception {
+    scratch.file(
+        "vars/vars.bzl",
+        "def _make_var_supplier_impl(ctx):",
+        "  val = ctx.attr.value",
+        "  return [platform_common.TemplateVariableInfo({'MAKE_VAR_VALUE': val})]",
+        "make_var_supplier = rule(",
+        "    implementation = _make_var_supplier_impl,",
+        "    attrs = {",
+        "        'value': attr.string(mandatory = True),",
+        "    })",
+        "def _make_var_user_impl(ctx):",
+        "  return []",
+        "make_var_user = rule(",
+        "    implementation = _make_var_user_impl,",
+        ")");
+    scratch.file(
+        "vars/BUILD",
+        "load(':vars.bzl', 'make_var_supplier', 'make_var_user')",
+        "make_var_supplier(name = 'supplier', value = 'foo')",
+        "cc_toolchain_alias(name = 'current_cc_toolchain')",
+        "make_var_user(",
+        "    name = 'vars',",
+        "    toolchains = [':supplier', ':current_cc_toolchain'],",
+        ")");
+  }
+
+  @Test
+  public void testExpandMakeVariables_cc() throws Exception {
+    setUpMakeVarToolchain();
+    setRuleContext(createRuleContext("//vars:vars"));
+    String result = (String) ev.eval("ruleContext.expand_make_variables('cmd', '$(CC)', {})");
+    assertThat(result).isNotEmpty();
+  }
+
+  @Test
+  public void testExpandMakeVariables_toolchain() throws Exception {
+    setUpMakeVarToolchain();
+    setRuleContext(createRuleContext("//vars:vars"));
+    Object result = ev.eval("ruleContext.expand_make_variables('cmd', '$(MAKE_VAR_VALUE)', {})");
+    assertThat(result).isEqualTo("foo");
+  }
+
+  @Test
+  public void testVar_toolchain() throws Exception {
+    setUpMakeVarToolchain();
+    setRuleContext(createRuleContext("//vars:vars"));
+    Object result = ev.eval("ruleContext.var['MAKE_VAR_VALUE']");
+    assertThat(result).isEqualTo("foo");
+  }
+
+  @Test
+  public void testConfiguration() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.configuration");
+    assertThat(ruleContext.getRuleContext().getConfiguration()).isSameInstanceAs(result);
+  }
+
+  @Test
+  public void testFeatures() throws Exception {
+    setRuleContext(createRuleContext("//foo:cc_with_features"));
+    Object result = ev.eval("ruleContext.features");
+    assertThat((Sequence) result).containsExactly("f1", "f2");
+  }
+
+  @Test
+  public void testDisabledFeatures() throws Exception {
+    setRuleContext(createRuleContext("//foo:cc_with_features"));
+    Object result = ev.eval("ruleContext.disabled_features");
+    assertThat((Sequence) result).containsExactly("f3");
+  }
+
+  @Test
+  public void testHostConfiguration() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.host_configuration");
+    assertThat(ruleContext.getRuleContext().getHostConfiguration()).isSameInstanceAs(result);
+  }
+
+  @Test
+  public void testWorkspaceName() throws Exception {
+    assertThat(ruleClassProvider.getRunfilesPrefix()).isNotNull();
+    assertThat(ruleClassProvider.getRunfilesPrefix()).isNotEmpty();
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.workspace_name");
+    assertThat(ruleClassProvider.getRunfilesPrefix()).isEqualTo(result);
+  }
+
+  @Test
+  public void testDeriveArtifactLegacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_new_actions_api=false");
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.new_file(ruleContext.genfiles_dir," + "  'a/b.txt')");
+    PathFragment fragment = ((Artifact) result).getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/a/b.txt");
+  }
+
+  @Test
+  public void testDeriveArtifact() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.actions.declare_file('a/b.txt')");
+    PathFragment fragment = ((Artifact) result).getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/a/b.txt");
+  }
+
+  @Test
+  public void testDeriveTreeArtifact() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.actions.declare_directory('a/b')");
+    Artifact artifact = (Artifact) result;
+    PathFragment fragment = artifact.getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/a/b");
+    assertThat(artifact.isTreeArtifact()).isTrue();
+  }
+
+  @Test
+  public void testDeriveTreeArtifactType() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    String result = (String) ev.eval("type(ruleContext.actions.declare_directory('a/b'))");
+    assertThat(result).isEqualTo("File");
+  }
+
+  @Test
+  public void testDeriveTreeArtifactNextToSibling() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Artifact artifact =
+        (Artifact)
+            ev.eval(
+                "ruleContext.actions.declare_directory('c',"
+                    + " sibling=ruleContext.actions.declare_directory('a/b'))");
+    PathFragment fragment = artifact.getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/a/c");
+    assertThat(artifact.isTreeArtifact()).isTrue();
+  }
+
+  @Test
+  public void testParamFileLegacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_new_actions_api=false");
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result =
+        ev.eval(
+            "ruleContext.new_file(ruleContext.bin_dir," + "ruleContext.files.tools[0], '.params')");
+    PathFragment fragment = ((Artifact) result).getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/t.exe.params");
+  }
+
+  @Test
+  public void testParamFileSuffixLegacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_new_actions_api=false");
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result =
+        ev.eval(
+            "ruleContext.new_file(ruleContext.files.tools[0], "
+                + "ruleContext.files.tools[0].basename + '.params')");
+    PathFragment fragment = ((Artifact) result).getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/t.exe.params");
+  }
+
+  @Test
+  public void testParamFileSuffix() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result =
+        ev.eval(
+            "ruleContext.actions.declare_file(ruleContext.files.tools[0].basename + '.params', "
+                + "sibling = ruleContext.files.tools[0])");
+    PathFragment fragment = ((Artifact) result).getRootRelativePath();
+    assertThat(fragment.getPathString()).isEqualTo("foo/t.exe.params");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictConvertsToTargetToStringMap() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "filegroup(name='dep')",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={':dep': 'value'})");
+
+    invalidatePackages();
+    setRuleContext(createRuleContext("//:r"));
+    Label keyLabel = (Label) ev.eval("ruleContext.attr.label_dict.keys()[0].label");
+    assertThat(keyLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+    String valueString = (String) ev.eval("ruleContext.attr.label_dict.values()[0]");
+    assertThat(valueString).isEqualTo("value");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictTranslatesAliases() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "filegroup(name='dep')",
+        "alias(name='alias', actual='dep')",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={':alias': 'value'})");
+
+    invalidatePackages();
+    setRuleContext(createRuleContext("//:r"));
+    Label keyLabel = (Label) ev.eval("ruleContext.attr.label_dict.keys()[0].label");
+    assertThat(keyLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+    String valueString = (String) ev.eval("ruleContext.attr.label_dict.values()[0]");
+    assertThat(valueString).isEqualTo("value");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAcceptsDefaultValues() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(default={Label('//:default'): 'defs'}),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "filegroup(name='default')",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r')");
+
+    invalidatePackages();
+    setRuleContext(createRuleContext("//:r"));
+    Label keyLabel = (Label) ev.eval("ruleContext.attr.label_dict.keys()[0].label");
+    assertThat(keyLabel).isEqualTo(Label.parseAbsolute("//:default", ImmutableMap.of()));
+    String valueString = (String) ev.eval("ruleContext.attr.label_dict.values()[0]");
+    assertThat(valueString).isEqualTo("defs");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsFilesWhenAllowFilesIsTrue() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_files=True),",
+        "  }",
+        ")");
+
+    scratch.file("myfile.cc");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={'myfile.cc': 'value'})");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsFilesOfAppropriateTypes() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_files=['.cc']),",
+        "  }",
+        ")");
+
+    scratch.file("myfile.cc");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={'myfile.cc': 'value'})");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelKeyedStringDictForbidsFilesOfIncorrectTypes() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_files=['.cc']),",
+        "  }",
+        ")");
+
+    scratch.file("myfile.cpp");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={'myfile.cpp': 'value'})");
+
+    invalidatePackages();
+    getConfiguredTarget("//:r");
+    assertContainsEvent("file '//:myfile.cpp' is misplaced here (expected .cc)");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictForbidsFilesWhenAllowFilesIsFalse() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_files=False),",
+        "  }",
+        ")");
+
+    scratch.file("myfile.cpp");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={'myfile.cpp': 'value'})");
+
+    invalidatePackages();
+    getConfiguredTarget("//:r");
+    assertContainsEvent(
+        "in label_dict attribute of my_rule rule //:r: "
+            + "source file '//:myfile.cpp' is misplaced here (expected no files)");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsRulesWithRequiredProviders_legacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(providers=[['my_provider']]),",
+        "  }",
+        ")",
+        "def _dep_impl(ctx):",
+        "  return struct(my_provider=5)",
+        "my_dep_rule = rule(",
+        "  implementation = _dep_impl,",
+        "  attrs = {}",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule', 'my_dep_rule')",
+        "my_dep_rule(name='dep')",
+        "my_rule(name='r',",
+        "        label_dict={':dep': 'value'})");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsRulesWithRequiredProviders() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(providers=[MyInfo]),",
+        "  }",
+        ")",
+        "def _dep_impl(ctx):",
+        "  return MyInfo(my_provider=5)",
+        "my_dep_rule = rule(",
+        "  implementation = _dep_impl,",
+        "  attrs = {}",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule', 'my_dep_rule')",
+        "my_dep_rule(name='dep')",
+        "my_rule(name='r',",
+        "        label_dict={':dep': 'value'})");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelKeyedStringDictForbidsRulesMissingRequiredProviders() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(providers=[['my_provider']]),",
+        "  }",
+        ")",
+        "def _dep_impl(ctx):",
+        "  return",
+        "my_dep_rule = rule(",
+        "  implementation = _dep_impl,",
+        "  attrs = {}",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule', 'my_dep_rule')",
+        "my_dep_rule(name='dep')",
+        "my_rule(name='r',",
+        "        label_dict={':dep': 'value'})");
+
+    invalidatePackages();
+    getConfiguredTarget("//:r");
+    assertContainsEvent(
+        "in label_dict attribute of my_rule rule //:r: "
+            + "'//:dep' does not have mandatory providers: 'my_provider'");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictForbidsEmptyDictWhenAllowEmptyIsFalse() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_empty=False),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={})");
+
+    invalidatePackages();
+    getConfiguredTarget("//:r");
+    assertContainsEvent(
+        "in label_dict attribute of my_rule rule //:r: " + "attribute must be non empty");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsEmptyDictWhenAllowEmptyIsTrue() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(allow_empty=True),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD",
+        "load('//:my_rule.bzl', 'my_rule')",
+        "my_rule(name='r',",
+        "        label_dict={})");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelKeyedStringDictForbidsMissingAttributeWhenMandatoryIsTrue()
+      throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(mandatory=True),",
+        "  }",
+        ")");
+
+    scratch.file("BUILD", "load('//:my_rule.bzl', 'my_rule')", "my_rule(name='r')");
+
+    invalidatePackages();
+    getConfiguredTarget("//:r");
+    assertContainsEvent("missing value for mandatory attribute 'label_dict' in 'my_rule' rule");
+  }
+
+  @Test
+  public void testLabelKeyedStringDictAllowsMissingAttributeWhenMandatoryIsFalse()
+      throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'label_dict': attr.label_keyed_string_dict(mandatory=False),",
+        "  }",
+        ")");
+
+    scratch.file("BUILD", "load('//:my_rule.bzl', 'my_rule')", "my_rule(name='r')");
+
+    invalidatePackages();
+    createRuleContext("//:r");
+    assertNoEvents();
+  }
+
+  @Test
+  public void testLabelAttributeDefault() throws Exception {
+    scratch.file(
+        "my_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'explicit_dep': attr.label(default = Label('//:dep')),",
+        "    '_implicit_dep': attr.label(default = Label('//:dep')),",
+        "    'explicit_dep_list': attr.label_list(default = [Label('//:dep')]),",
+        "    '_implicit_dep_list': attr.label_list(default = [Label('//:dep')]),",
+        "  }",
+        ")");
+
+    scratch.file(
+        "BUILD", "filegroup(name='dep')", "load('//:my_rule.bzl', 'my_rule')", "my_rule(name='r')");
+
+    invalidatePackages();
+    setRuleContext(createRuleContext("//:r"));
+    Label explicitDepLabel = (Label) ev.eval("ruleContext.attr.explicit_dep.label");
+    assertThat(explicitDepLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+    Label implicitDepLabel = (Label) ev.eval("ruleContext.attr._implicit_dep.label");
+    assertThat(implicitDepLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+    Label explicitDepListLabel = (Label) ev.eval("ruleContext.attr.explicit_dep_list[0].label");
+    assertThat(explicitDepListLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+    Label implicitDepListLabel = (Label) ev.eval("ruleContext.attr._implicit_dep_list[0].label");
+    assertThat(implicitDepListLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testRelativeLabelInExternalRepository() throws Exception {
+    scratch.file(
+        "external_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "external_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'internal_dep': attr.label(default = Label('//:dep'))",
+        "  }",
+        ")");
+
+    scratch.file("BUILD", "filegroup(name='dep')");
+
+    scratch.file("/r/WORKSPACE");
+    scratch.file(
+        "/r/a/BUILD", "load('@//:external_rule.bzl', 'external_rule')", "external_rule(name='r')");
+
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='/r')")
+            .build());
+
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    setRuleContext(createRuleContext("@r//a:r"));
+    Label depLabel = (Label) ev.eval("ruleContext.attr.internal_dep.label");
+    assertThat(depLabel).isEqualTo(Label.parseAbsolute("//:dep", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testCallerRelativeLabelInExternalRepository() throws Exception {
+    scratch.file("BUILD");
+    scratch.file(
+        "external_rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "external_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'internal_dep': attr.label(",
+        "        default = Label('//:dep', relative_to_caller_repository = True)",
+        "    )",
+        "  }",
+        ")");
+
+    scratch.file("/r/WORKSPACE");
+    scratch.file("/r/BUILD", "filegroup(name='dep')");
+
+    scratch.file(
+        "/r/a/BUILD", "load('@//:external_rule.bzl', 'external_rule')", "external_rule(name='r')");
+
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r', path='/r')")
+            .build());
+
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    setRuleContext(createRuleContext("@r//a:r"));
+    Label depLabel = (Label) ev.eval("ruleContext.attr.internal_dep.label");
+    assertThat(depLabel).isEqualTo(Label.parseAbsolute("@r//:dep", ImmutableMap.of()));
+  }
+
+  @Test
+  public void testExternalWorkspaceLoad() throws Exception {
+    scratch.file(
+        "/r1/BUILD",
+        "filegroup(name = 'test',",
+        " srcs = ['test.txt'],",
+        " visibility = ['//visibility:public'],",
+        ")");
+    scratch.file("/r1/WORKSPACE");
+    scratch.file("/r2/BUILD", "exports_files(['test.bzl'])");
+    scratch.file(
+        "/r2/test.bzl",
+        "def macro(name, path):",
+        "  native.local_repository(name = name, path = path)");
+    scratch.file("/r2/WORKSPACE");
+    scratch.file(
+        "/r2/other_test.bzl", "def other_macro(name, path):", "  print(name + ': ' + path)");
+    scratch.file("BUILD");
+
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name='r2', path='/r2')")
+            .add("load('@r2//:test.bzl', 'macro')")
+            .add("macro('r1', '/r1')")
+            .add("NEXT_NAME = 'r3'")
+            // We can still refer to r2 in other chunks:
+            .add("load('@r2//:other_test.bzl', 'other_macro')")
+            .add("macro(NEXT_NAME, '/r2')") // and we can still use macro outside of its chunk.
+            .build());
+
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    assertThat(getConfiguredTarget("@r1//:test")).isNotNull();
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testLoadBlockRepositoryRedefinition() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("/bar/WORKSPACE");
+    scratch.file("/bar/bar.txt");
+    scratch.file("/bar/BUILD", "filegroup(name = 'baz', srcs = ['bar.txt'])");
+    scratch.file("/baz/WORKSPACE");
+    scratch.file("/baz/baz.txt");
+    scratch.file("/baz/BUILD", "filegroup(name = 'baz', srcs = ['baz.txt'])");
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name = 'foo', path = '/bar')")
+            .add("local_repository(name = 'foo', path = '/baz')")
+            .build());
+
+    invalidatePackages(
+        /*alsoConfigs=*/ false); // Repository shuffling messes with toolchain labels.
+    assertThat(
+            (List)
+                getConfiguredTargetAndData("@foo//:baz")
+                    .getTarget()
+                    .getAssociatedRule()
+                    .getAttributeContainer()
+                    .getAttr("srcs"))
+        .contains(Label.parseAbsolute("@foo//:baz.txt", ImmutableMap.of()));
+
+    scratch.overwriteFile("BUILD");
+    scratch.overwriteFile("bar.bzl", "dummy = 1");
+
+    scratch.overwriteFile(
+        "WORKSPACE",
+        new ImmutableList.Builder<String>()
+            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
+            .add("local_repository(name = 'foo', path = '/bar')")
+            .add("load('//:bar.bzl', 'dummy')")
+            .add("local_repository(name = 'foo', path = '/baz')")
+            .build());
+
+    invalidatePackages(/*alsoConfigs=*/ false); // Repository shuffling messes with toolchains.
+    assertThrows(Exception.class, () -> createRuleContext("@foo//:baz"));
+    assertContainsEvent(
+        "Cannot redefine repository after any load statement in the WORKSPACE file "
+            + "(for repository 'foo')");
+  }
+
+  @Test
+  public void testAccessingRunfiles() throws Exception {
+    scratch.file("test/a.py");
+    scratch.file("test/b.py");
+    scratch.file("test/__init__.py");
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "starlark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'dep': attr.label(),",
+        "  },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'starlark_rule')",
+        "py_binary(name = 'lib', srcs = ['lib.py', 'lib2.py'])",
+        "starlark_rule(name = 'foo', dep = ':lib')",
+        "py_binary(name = 'lib_with_init', srcs = ['lib_with_init.py', 'lib2.py', '__init__.py'])",
+        "starlark_rule(name = 'foo_with_init', dep = ':lib_with_init')");
+
+    setRuleContext(createRuleContext("//test:foo"));
+    Object filenames =
+        ev.eval("[f.short_path for f in ruleContext.attr.dep.default_runfiles.files.to_list()]");
+    assertThat(filenames).isInstanceOf(Sequence.class);
+    Sequence<?> filenamesList = (Sequence) filenames;
+    assertThat(filenamesList).containsAtLeast("test/lib.py", "test/lib2.py");
+    Object emptyFilenames =
+        ev.eval("ruleContext.attr.dep.default_runfiles.empty_filenames.to_list()");
+    assertThat(emptyFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> emptyFilenamesList = (Sequence) emptyFilenames;
+    assertThat(emptyFilenamesList).containsExactly("test/__init__.py");
+
+    setRuleContext(createRuleContext("//test:foo_with_init"));
+    Object noEmptyFilenames =
+        ev.eval("ruleContext.attr.dep.default_runfiles.empty_filenames.to_list()");
+    assertThat(noEmptyFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> noEmptyFilenamesList = (Sequence) noEmptyFilenames;
+    assertThat(noEmptyFilenamesList).isEmpty();
+  }
+
+  @Test
+  public void testAccessingRunfilesSymlinks_legacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file("test/a.py");
+    scratch.file("test/b.py");
+    scratch.file(
+        "test/rule.bzl",
+        "def symlink_impl(ctx):",
+        "  symlinks = {",
+        "    'symlink_' + f.short_path: f",
+        "    for f in ctx.files.symlink",
+        "  }",
+        "  return struct(",
+        "    runfiles = ctx.runfiles(",
+        "      symlinks=symlinks,",
+        "    )",
+        "  )",
+        "symlink_rule = rule(",
+        "  implementation = symlink_impl,",
+        "  attrs = {",
+        "    'symlink': attr.label(allow_files=True),",
+        "  },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'symlink_rule')",
+        "symlink_rule(name = 'lib_with_symlink', symlink = ':a.py')",
+        "sh_binary(",
+        "  name = 'test_with_symlink',",
+        "  srcs = ['test/b.py'],",
+        "  data = [':lib_with_symlink'],",
+        ")");
+    setRuleContext(createRuleContext("//test:test_with_symlink"));
+    Object symlinkPaths =
+        ev.eval("[s.path for s in ruleContext.attr.data[0].data_runfiles.symlinks.to_list()]");
+    assertThat(symlinkPaths).isInstanceOf(Sequence.class);
+    Sequence<?> symlinkPathsList = (Sequence) symlinkPaths;
+    assertThat(symlinkPathsList).containsExactly("symlink_test/a.py").inOrder();
+    Object symlinkFilenames =
+        ev.eval(
+            "[s.target_file.short_path for s in"
+                + " ruleContext.attr.data[0].data_runfiles.symlinks.to_list()]");
+    assertThat(symlinkFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> symlinkFilenamesList = (Sequence) symlinkFilenames;
+    assertThat(symlinkFilenamesList).containsExactly("test/a.py").inOrder();
+  }
+
+  @Test
+  public void testAccessingRunfilesSymlinks() throws Exception {
+    scratch.file("test/a.py");
+    scratch.file("test/b.py");
+    scratch.file(
+        "test/rule.bzl",
+        "def symlink_impl(ctx):",
+        "  symlinks = {",
+        "    'symlink_' + f.short_path: f",
+        "    for f in ctx.files.symlink",
+        "  }",
+        "  return DefaultInfo(",
+        "    runfiles = ctx.runfiles(",
+        "      symlinks=symlinks,",
+        "    )",
+        "  )",
+        "symlink_rule = rule(",
+        "  implementation = symlink_impl,",
+        "  attrs = {",
+        "    'symlink': attr.label(allow_files=True),",
+        "  },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'symlink_rule')",
+        "symlink_rule(name = 'lib_with_symlink', symlink = ':a.py')",
+        "sh_binary(",
+        "  name = 'test_with_symlink',",
+        "  srcs = ['test/b.py'],",
+        "  data = [':lib_with_symlink'],",
+        ")");
+    setRuleContext(createRuleContext("//test:test_with_symlink"));
+    Object symlinkPaths =
+        ev.eval("[s.path for s in ruleContext.attr.data[0].data_runfiles.symlinks.to_list()]");
+    assertThat(symlinkPaths).isInstanceOf(Sequence.class);
+    Sequence<?> symlinkPathsList = (Sequence) symlinkPaths;
+    assertThat(symlinkPathsList).containsExactly("symlink_test/a.py").inOrder();
+    Object symlinkFilenames =
+        ev.eval(
+            "[s.target_file.short_path for s in"
+                + " ruleContext.attr.data[0].data_runfiles.symlinks.to_list()]");
+    assertThat(symlinkFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> symlinkFilenamesList = (Sequence) symlinkFilenames;
+    assertThat(symlinkFilenamesList).containsExactly("test/a.py").inOrder();
+  }
+
+  @Test
+  public void testAccessingRunfilesRootSymlinks_legacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file("test/a.py");
+    scratch.file("test/b.py");
+    scratch.file(
+        "test/rule.bzl",
+        "def root_symlink_impl(ctx):",
+        "  root_symlinks = {",
+        "    'root_symlink_' + f.short_path: f",
+        "    for f in ctx.files.root_symlink",
+        "  }",
+        "  return struct(",
+        "    runfiles = ctx.runfiles(",
+        "      root_symlinks=root_symlinks,",
+        "    )",
+        "  )",
+        "root_symlink_rule = rule(",
+        "  implementation = root_symlink_impl,",
+        "  attrs = {",
+        "    'root_symlink': attr.label(allow_files=True)",
+        "  },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'root_symlink_rule')",
+        "root_symlink_rule(name = 'lib_with_root_symlink', root_symlink = ':a.py')",
+        "sh_binary(",
+        "  name = 'test_with_root_symlink',",
+        "  srcs = ['test/b.py'],",
+        "  data = [':lib_with_root_symlink'],",
+        ")");
+    setRuleContext(createRuleContext("//test:test_with_root_symlink"));
+    Object rootSymlinkPaths =
+        ev.eval("[s.path for s in ruleContext.attr.data[0].data_runfiles.root_symlinks.to_list()]");
+    assertThat(rootSymlinkPaths).isInstanceOf(Sequence.class);
+    Sequence<?> rootSymlinkPathsList = (Sequence) rootSymlinkPaths;
+    assertThat(rootSymlinkPathsList).containsExactly("root_symlink_test/a.py").inOrder();
+    Object rootSymlinkFilenames =
+        ev.eval(
+            "[s.target_file.short_path for s in"
+                + " ruleContext.attr.data[0].data_runfiles.root_symlinks.to_list()]");
+    assertThat(rootSymlinkFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> rootSymlinkFilenamesList = (Sequence) rootSymlinkFilenames;
+    assertThat(rootSymlinkFilenamesList).containsExactly("test/a.py").inOrder();
+  }
+
+  @Test
+  public void testAccessingRunfilesRootSymlinks() throws Exception {
+    scratch.file("test/a.py");
+    scratch.file("test/b.py");
+    scratch.file(
+        "test/rule.bzl",
+        "def root_symlink_impl(ctx):",
+        "  root_symlinks = {",
+        "    'root_symlink_' + f.short_path: f",
+        "    for f in ctx.files.root_symlink",
+        "  }",
+        "  return DefaultInfo(",
+        "    runfiles = ctx.runfiles(",
+        "      root_symlinks=root_symlinks,",
+        "    )",
+        "  )",
+        "root_symlink_rule = rule(",
+        "  implementation = root_symlink_impl,",
+        "  attrs = {",
+        "    'root_symlink': attr.label(allow_files=True)",
+        "  },",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'root_symlink_rule')",
+        "root_symlink_rule(name = 'lib_with_root_symlink', root_symlink = ':a.py')",
+        "sh_binary(",
+        "  name = 'test_with_root_symlink',",
+        "  srcs = ['test/b.py'],",
+        "  data = [':lib_with_root_symlink'],",
+        ")");
+    setRuleContext(createRuleContext("//test:test_with_root_symlink"));
+    Object rootSymlinkPaths =
+        ev.eval("[s.path for s in ruleContext.attr.data[0].data_runfiles.root_symlinks.to_list()]");
+    assertThat(rootSymlinkPaths).isInstanceOf(Sequence.class);
+    Sequence<?> rootSymlinkPathsList = (Sequence) rootSymlinkPaths;
+    assertThat(rootSymlinkPathsList).containsExactly("root_symlink_test/a.py").inOrder();
+    Object rootSymlinkFilenames =
+        ev.eval(
+            "[s.target_file.short_path for s in"
+                + " ruleContext.attr.data[0].data_runfiles.root_symlinks.to_list()]");
+    assertThat(rootSymlinkFilenames).isInstanceOf(Sequence.class);
+    Sequence<?> rootSymlinkFilenamesList = (Sequence) rootSymlinkFilenames;
+    assertThat(rootSymlinkFilenamesList).containsExactly("test/a.py").inOrder();
+  }
+
+  @Test
+  public void testExternalShortPath() throws Exception {
+    scratch.file("/bar/WORKSPACE");
+    scratch.file("/bar/bar.txt");
+    scratch.file("/bar/BUILD", "exports_files(['bar.txt'])");
+    FileSystemUtils.appendIsoLatin1(
+        scratch.resolve("WORKSPACE"), "local_repository(name = 'foo', path = '/bar')");
+    scratch.file(
+        "test/BUILD",
+        "genrule(",
+        "    name = 'lib',",
+        "    srcs = ['@foo//:bar.txt'],",
+        "    cmd = 'echo $(SRCS) $@',",
+        "    outs = ['lib.out'],",
+        "    executable = 1,",
+        ")");
+    invalidatePackages();
+    StarlarkRuleContext ruleContext = createRuleContext("//test:lib");
+    setRuleContext(ruleContext);
+    String filename = ev.eval("ruleContext.files.srcs[0].short_path").toString();
+    assertThat(filename).isEqualTo("../foo/bar.txt");
+  }
+
+  // Borrowed from Scratch.java.
+  private static String linesAsString(String... lines) {
+    StringBuilder builder = new StringBuilder();
+    for (String line : lines) {
+      builder.append(line);
+      builder.append('\n');
+    }
+    return builder.toString();
+  }
+
+  // The common structure of the following actions tests is a rule under test depended upon by
+  // a testing rule, where the rule under test has one output and one caller-supplied action.
+
+  private static String getSimpleUnderTestDefinition(
+      boolean withStarlarkTestable, String[] actionLines) {
+    return linesAsString(
+        // TODO(b/153667498): Just passing fail to map_each parameter of Args.add_all does not work.
+        "def fail_with_message(s):",
+        "    fail(s)",
+        "",
+        "def _undertest_impl(ctx):",
+        "  out = ctx.outputs.out",
+        "  " + Joiner.on("\n  ").join(actionLines),
+        "undertest_rule = rule(",
+        "  implementation = _undertest_impl,",
+        "  outputs = {'out': '%{name}.txt'},",
+        withStarlarkTestable ? "  _skylark_testable = True," : "",
+        ")");
+  }
+
+  private static String getSimpleUnderTestDefinition(String... actionLines) {
+    return getSimpleUnderTestDefinition(true, actionLines);
+  }
+
+  private static String getSimpleNontestableUnderTestDefinition(String... actionLines) {
+    return getSimpleUnderTestDefinition(false, actionLines);
+  }
+
+  private final String testingRuleDefinition =
+      linesAsString(
+          "def _testing_impl(ctx):",
+          "  pass",
+          "testing_rule = rule(",
+          "  implementation = _testing_impl,",
+          "  attrs = {'dep': attr.label()},",
+          ")");
+
+  private final String simpleBuildDefinition =
+      linesAsString(
+          "load(':rules.bzl', 'undertest_rule', 'testing_rule')",
+          "undertest_rule(",
+          "    name = 'undertest',",
+          ")",
+          "testing_rule(",
+          "    name = 'testing',",
+          "    dep = ':undertest',",
+          ")");
+
+  @Test
+  public void testDependencyActionsProvider() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition(
+            "ctx.actions.run_shell(outputs=[out], command='echo foo123 > ' + out.path)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+
+    Object provider = ev.eval("ruleContext.attr.dep[Actions]");
+    assertThat(provider).isInstanceOf(StructImpl.class);
+    assertThat(((StructImpl) provider).getProvider()).isEqualTo(ActionsProvider.INSTANCE);
+    ev.update("actions", provider);
+
+    Map<?, ?> mapping = (Dict<?, ?>) ev.eval("actions.by_file");
+    assertThat(mapping).hasSize(1);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    Object actionUnchecked = ev.eval("actions.by_file[file]");
+    assertThat(actionUnchecked).isInstanceOf(ActionAnalysisMetadata.class);
+  }
+
+  @Test
+  public void testNoAccessToDependencyActionsWithoutStarlarkTest() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleNontestableUnderTestDefinition(
+            "ctx.actions.run_shell(outputs=[out], command='echo foo123 > ' + out.path)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+
+    Exception e = assertThrows(Exception.class, () -> ev.eval("ruleContext.attr.dep[Actions]"));
+    assertThat(e)
+        .hasMessageThat()
+        .contains(
+            "<target //test:undertest> (rule 'undertest_rule') doesn't contain "
+                + "declared provider 'Actions'");
+  }
+
+  @Test
+  public void testAbstractActionInterface() throws Exception {
+    setStarlarkSemanticsOptions(
+        "--incompatible_disallow_struct_provider_syntax=false",
+        "--incompatible_no_rule_outputs_param=false");
+    scratch.file(
+        "test/rules.bzl",
+        "def _undertest_impl(ctx):",
+        "  out1 = ctx.outputs.out1",
+        "  out2 = ctx.outputs.out2",
+        "  ctx.actions.write(output=out1, content='foo123')",
+        "  ctx.actions.run_shell(outputs=[out2], inputs=[out1],",
+        "                        command='cp ' + out1.path + ' ' + out2.path)",
+        "  return struct(out1=out1, out2=out2)",
+        "undertest_rule = rule(",
+        "  implementation = _undertest_impl,",
+        "  outputs = {'out1': '%{name}1.txt',",
+        "             'out2': '%{name}2.txt'},",
+        "  _skylark_testable = True,",
+        ")",
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file1", ev.eval("ruleContext.attr.dep.out1"));
+    ev.update("file2", ev.eval("ruleContext.attr.dep.out2"));
+    ev.update("action1", ev.eval("ruleContext.attr.dep[Actions].by_file[file1]"));
+    ev.update("action2", ev.eval("ruleContext.attr.dep[Actions].by_file[file2]"));
+
+    assertThat(ev.eval("action1.inputs")).isInstanceOf(Depset.class);
+    assertThat(ev.eval("action1.outputs")).isInstanceOf(Depset.class);
+
+    assertThat(ev.eval("action1.argv")).isEqualTo(Starlark.NONE);
+    assertThat(ev.eval("action2.content")).isEqualTo(Starlark.NONE);
+    assertThat(ev.eval("action1.substitutions")).isEqualTo(Starlark.NONE);
+
+    assertThat(ev.eval("action1.inputs.to_list()")).isEqualTo(ev.eval("[]"));
+    assertThat(ev.eval("action1.outputs.to_list()")).isEqualTo(ev.eval("[file1]"));
+    assertThat(ev.eval("action2.inputs.to_list()")).isEqualTo(ev.eval("[file1]"));
+    assertThat(ev.eval("action2.outputs.to_list()")).isEqualTo(ev.eval("[file2]"));
+  }
+
+  // For created_actions() tests, the "undertest" rule represents both the code under test and the
+  // Starlark user test code itself.
+
+  @Test
+  public void testCreatedActions() throws Exception {
+    setStarlarkSemanticsOptions(
+        "--incompatible_disallow_struct_provider_syntax=false",
+        "--incompatible_no_rule_outputs_param=false");
+    // createRuleContext() gives us the context for a rule upon entry into its analysis function.
+    // But we need to inspect the result of calling created_actions() after the rule context has
+    // been modified by creating actions. So we'll call created_actions() from within the analysis
+    // function and pass it along as a provider.
+    scratch.file(
+        "test/rules.bzl",
+        "def _undertest_impl(ctx):",
+        "  out1 = ctx.outputs.out1",
+        "  out2 = ctx.outputs.out2",
+        "  ctx.actions.run_shell(outputs=[out1], command='echo foo123 > ' + out1.path,",
+        "                        mnemonic='foo')",
+        "  v = ctx.created_actions().by_file",
+        "  ctx.actions.run_shell(outputs=[out2], command='echo bar123 > ' + out2.path)",
+        "  return struct(v=v, out1=out1, out2=out2)",
+        "undertest_rule = rule(",
+        "  implementation = _undertest_impl,",
+        "  outputs = {'out1': '%{name}1.txt',",
+        "             'out2': '%{name}2.txt'},",
+        "  _skylark_testable = True,",
+        ")",
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+
+    Object mapUnchecked = ev.eval("ruleContext.attr.dep.v");
+    assertThat(mapUnchecked).isInstanceOf(Dict.class);
+    Map<?, ?> map = (Dict) mapUnchecked;
+    // Should only have the first action because created_actions() was called
+    // before the second action was created.
+    Object file = ev.eval("ruleContext.attr.dep.out1");
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(file);
+    Object actionUnchecked = map.get(file);
+    assertThat(actionUnchecked).isInstanceOf(ActionAnalysisMetadata.class);
+    assertThat(((ActionAnalysisMetadata) actionUnchecked).getMnemonic()).isEqualTo("foo");
+  }
+
+  @Test
+  public void testNoAccessToCreatedActionsWithoutStarlarkTest() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleNontestableUnderTestDefinition(
+            "ctx.actions.run_shell(outputs=[out], command='echo foo123 > ' + out.path)"));
+    scratch.file(
+        "test/BUILD",
+        "load(':rules.bzl', 'undertest_rule')",
+        "undertest_rule(",
+        "    name = 'undertest',",
+        ")");
+    StarlarkRuleContext ruleContext = createRuleContext("//test:undertest");
+    setRuleContext(ruleContext);
+
+    Object result = ev.eval("ruleContext.created_actions()");
+    assertThat(result).isEqualTo(Starlark.NONE);
+  }
+
+  @Test
+  public void testSpawnActionInterface() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition(
+            "ctx.actions.run_shell(outputs=[out], command='echo foo123 > ' + out.path)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    Object argvUnchecked = ev.eval("action.argv");
+    assertThat(argvUnchecked).isInstanceOf(StarlarkList.class);
+    StarlarkList<?> argv = (StarlarkList) argvUnchecked;
+    assertThat(argv).hasSize(3);
+    assertThat(argv.isImmutable()).isTrue();
+    Object result = ev.eval("action.argv[2].startswith('echo foo123')");
+    assertThat((Boolean) result).isTrue();
+  }
+
+  @Test
+  public void testRunShellUsesHelperScriptForLongCommand() throws Exception {
+    setStarlarkSemanticsOptions(
+        "--incompatible_disallow_struct_provider_syntax=false",
+        "--incompatible_no_rule_outputs_param=false");
+    // createRuleContext() gives us the context for a rule upon entry into its analysis function.
+    // But we need to inspect the result of calling created_actions() after the rule context has
+    // been modified by creating actions. So we'll call created_actions() from within the analysis
+    // function and pass it along as a provider.
+    scratch.file(
+        "test/rules.bzl",
+        "def _undertest_impl(ctx):",
+        "  out1 = ctx.outputs.out1",
+        "  out2 = ctx.outputs.out2",
+        "  out3 = ctx.outputs.out3",
+        "  ctx.actions.run_shell(outputs=[out1],",
+        "                        command='( %s ; ) > $1' % (",
+        "                            ' ; '.join(['echo xxx%d' % i for i in range(0, 7000)])),",
+        "                        mnemonic='mnemonic1',",
+        "                        arguments=[out1.path])",
+        "  ctx.actions.run_shell(outputs=[out2],",
+        "                        command='echo foo > ' + out2.path,",
+        "                        mnemonic='mnemonic2')",
+        "  ctx.actions.run_shell(outputs=[out3],",
+        "                        command='( %s ; ) > $1' % (",
+        "                            ' ; '.join(['echo yyy%d' % i for i in range(0, 7000)])),",
+        "                        mnemonic='mnemonic3',",
+        "                        arguments=[out3.path])",
+        "  v = ctx.created_actions().by_file",
+        "  return struct(v=v, out1=out1, out2=out2, out3=out3)",
+        "",
+        "undertest_rule = rule(",
+        "    implementation=_undertest_impl,",
+        "    outputs={'out1': '%{name}1.txt',",
+        "             'out2': '%{name}2.txt',",
+        "             'out3': '%{name}3.txt'},",
+        "    _skylark_testable = True,",
+        ")",
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+
+    Object mapUnchecked = ev.eval("ruleContext.attr.dep.v");
+    assertThat(mapUnchecked).isInstanceOf(Dict.class);
+    Map<?, ?> map = (Dict) mapUnchecked;
+    Object out1 = ev.eval("ruleContext.attr.dep.out1");
+    Object out2 = ev.eval("ruleContext.attr.dep.out2");
+    Object out3 = ev.eval("ruleContext.attr.dep.out3");
+    // 5 actions in total: 3 SpawnActions and 2 FileWriteActions for the two long commands.
+    assertThat(map).hasSize(5);
+    assertThat(map).containsKey(out1);
+    assertThat(map).containsKey(out2);
+    assertThat(map).containsKey(out3);
+    Object action1Unchecked = map.get(out1);
+    Object action2Unchecked = map.get(out2);
+    Object action3Unchecked = map.get(out3);
+    assertThat(action1Unchecked).isInstanceOf(ActionAnalysisMetadata.class);
+    assertThat(action2Unchecked).isInstanceOf(ActionAnalysisMetadata.class);
+    assertThat(action3Unchecked).isInstanceOf(ActionAnalysisMetadata.class);
+    ActionAnalysisMetadata spawnAction1 = (ActionAnalysisMetadata) action1Unchecked;
+    ActionAnalysisMetadata spawnAction2 = (ActionAnalysisMetadata) action2Unchecked;
+    ActionAnalysisMetadata spawnAction3 = (ActionAnalysisMetadata) action3Unchecked;
+    assertThat(spawnAction1.getMnemonic()).isEqualTo("mnemonic1");
+    assertThat(spawnAction2.getMnemonic()).isEqualTo("mnemonic2");
+    assertThat(spawnAction3.getMnemonic()).isEqualTo("mnemonic3");
+    Artifact helper1 =
+        Iterables.getOnlyElement(
+            Iterables.filter(
+                spawnAction1.getInputs().toList(),
+                a -> a.getFilename().equals("undertest.run_shell_0.sh")));
+    assertThat(
+            Iterables.filter(
+                spawnAction2.getInputs().toList(), a -> a.getFilename().contains("run_shell_")))
+        .isEmpty();
+    Artifact helper3 =
+        Iterables.getOnlyElement(
+            Iterables.filter(
+                spawnAction3.getInputs().toList(),
+                a -> a.getFilename().equals("undertest.run_shell_2.sh")));
+    assertThat(map).containsKey(helper1);
+    assertThat(map).containsKey(helper3);
+    Object action4Unchecked = map.get(helper1);
+    Object action5Unchecked = map.get(helper3);
+    assertThat(action4Unchecked).isInstanceOf(FileWriteAction.class);
+    assertThat(action5Unchecked).isInstanceOf(FileWriteAction.class);
+    FileWriteAction fileWriteAction1 = (FileWriteAction) action4Unchecked;
+    FileWriteAction fileWriteAction2 = (FileWriteAction) action5Unchecked;
+    assertThat(fileWriteAction1.getFileContents()).contains("echo xxx6999 ;");
+    assertThat(fileWriteAction2.getFileContents()).contains("echo yyy6999 ;");
+  }
+
+  @Test
+  public void testInvalidMnemonic() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "  out = ctx.actions.declare_file('f')",
+        "  ctx.actions.run_shell(",
+        "      outputs=[out], command='false', mnemonic='@@@')",
+        "r = rule(implementation = _impl)");
+    scratch.file("test/BUILD", "load('//test:rule.bzl', 'r')", "r(name = 'target')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:target");
+    assertContainsEvent(
+        "mnemonic must only contain letters and/or digits, and have non-zero length, was: \"@@@\"");
+  }
+
+  @Test
+  public void testFileWriteActionInterface() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition("ctx.actions.write(output=out, content='foo123')"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    Object contentUnchecked = ev.eval("action.content");
+    assertThat(contentUnchecked).isInstanceOf(String.class);
+    assertThat(contentUnchecked).isEqualTo("foo123");
+  }
+
+  @Test
+  public void testFileWriteActionInterfaceWithArgs() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition(
+            "args = ctx.actions.args()",
+            "args.add('foo123')",
+            "ctx.actions.write(output=out, content=args)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    Object contentUnchecked = ev.eval("action.content");
+    assertThat(contentUnchecked).isInstanceOf(String.class);
+    // Args content ends the file with a newline
+    assertThat(contentUnchecked).isEqualTo("foo123\n");
+  }
+
+  @Test
+  public void testFileWriteActionInterfaceWithArgsContainingTreeArtifact() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition(
+            "directory = ctx.actions.declare_directory('dir')",
+            "ctx.actions.run_shell(",
+            "    outputs = [directory],",
+            "    command = 'mkdir {out}'",
+            ")",
+            "args = ctx.actions.args()",
+            "args.add_all([directory])",
+            "ctx.actions.write(output=out, content=args)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    // If the Args contain a directory File that needs to be expanded, the contents are not known
+    // at analysis time.
+    Object contentUnchecked = ev.eval("action.content");
+    assertThat(contentUnchecked).isEqualTo(Starlark.NONE);
+  }
+
+  @Test
+  public void testFileWriteActionInterfaceWithArgsExpansionError() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        getSimpleUnderTestDefinition(
+            "args = ctx.actions.args()",
+            "args.add_all(['args expansion error message'], map_each = fail_with_message)",
+            "ctx.actions.write(output=out, content=args)"),
+        testingRuleDefinition);
+    scratch.file("test/BUILD", simpleBuildDefinition);
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    // If there's a failure when expanding Args, that error message is propagated.
+    EvalException e =
+        assertThrows(
+            "Should be an error expanding action.content",
+            EvalException.class,
+            () -> ev.eval("action.content"));
+    assertThat(e)
+        .hasMessageThat()
+        .matches(
+            "Error expanding command line: \n\n/workspace/test/rules\\.bzl:\\d+:\\d+: args "
+                + "expansion error message");
+  }
+
+  @Test
+  public void testTemplateExpansionActionInterface() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        "def _undertest_impl(ctx):",
+        "  out = ctx.outputs.out",
+        "  ctx.actions.expand_template(output=out,",
+        "                              template=ctx.file.template, substitutions={'a': 'b'})",
+        "undertest_rule = rule(",
+        "  implementation = _undertest_impl,",
+        "  outputs = {'out': '%{name}.txt'},",
+        "  attrs = {'template': attr.label(allow_single_file=True)},",
+        "  _skylark_testable = True,",
+        ")",
+        testingRuleDefinition);
+    scratch.file("test/template.txt", "aaaaa", "bcdef");
+    scratch.file(
+        "test/BUILD",
+        "load(':rules.bzl', 'undertest_rule', 'testing_rule')",
+        "undertest_rule(",
+        "    name = 'undertest',",
+        "    template = ':template.txt',",
+        ")",
+        "testing_rule(",
+        "    name = 'testing',",
+        "    dep = ':undertest',",
+        ")");
+    StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+    setRuleContext(ruleContext);
+    ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+    ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+    assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+    Object contentUnchecked = ev.eval("action.content");
+    assertThat(contentUnchecked).isInstanceOf(String.class);
+    assertThat(contentUnchecked).isEqualTo("bbbbb\nbcdef\n");
+
+    Object substitutionsUnchecked = ev.eval("action.substitutions");
+    assertThat(substitutionsUnchecked).isInstanceOf(Dict.class);
+    assertThat(substitutionsUnchecked).isEqualTo(Dict.of((Mutability) null, "a", "b"));
+  }
+
+  private void setUpCoverageInstrumentedTest() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "cc_library(",
+        "  name = 'foo',",
+        "  srcs = ['foo.cc'],",
+        "  deps = [':bar'],",
+        ")",
+        "cc_library(",
+        "  name = 'bar',",
+        "  srcs = ['bar.cc'],",
+        ")");
+  }
+
+  @Test
+  public void testCoverageInstrumentedCoverageDisabled() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--nocollect_code_coverage", "--instrumentation_filter=.");
+    StarlarkRuleContext ruleContext = createRuleContext("//test:foo");
+    setRuleContext(ruleContext);
+    Object result = ev.eval("ruleContext.coverage_instrumented()");
+    assertThat((Boolean) result).isFalse();
+  }
+
+  @Test
+  public void testCoverageInstrumentedFalseForSourceFileLabel() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--collect_code_coverage", "--instrumentation_filter=.");
+    setRuleContext(createRuleContext("//test:foo"));
+    Object result = ev.eval("ruleContext.coverage_instrumented(ruleContext.attr.srcs[0])");
+    assertThat((Boolean) result).isFalse();
+  }
+
+  @Test
+  public void testCoverageInstrumentedDoesNotMatchFilter() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--collect_code_coverage", "--instrumentation_filter=:foo");
+    setRuleContext(createRuleContext("//test:bar"));
+    Object result = ev.eval("ruleContext.coverage_instrumented()");
+    assertThat((Boolean) result).isFalse();
+  }
+
+  @Test
+  public void testCoverageInstrumentedMatchesFilter() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--collect_code_coverage", "--instrumentation_filter=:foo");
+    setRuleContext(createRuleContext("//test:foo"));
+    Object result = ev.eval("ruleContext.coverage_instrumented()");
+    assertThat((Boolean) result).isTrue();
+  }
+
+  @Test
+  public void testCoverageInstrumentedDoesNotMatchFilterNonDefaultLabel() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--collect_code_coverage", "--instrumentation_filter=:foo");
+    setRuleContext(createRuleContext("//test:foo"));
+    // //test:bar does not match :foo, though //test:foo would.
+    Object result = ev.eval("ruleContext.coverage_instrumented(ruleContext.attr.deps[0])");
+    assertThat((Boolean) result).isFalse();
+  }
+
+  @Test
+  public void testCoverageInstrumentedMatchesFilterNonDefaultLabel() throws Exception {
+    setUpCoverageInstrumentedTest();
+    useConfiguration("--collect_code_coverage", "--instrumentation_filter=:bar");
+    setRuleContext(createRuleContext("//test:foo"));
+    // //test:bar does match :bar, though //test:foo would not.
+    Object result = ev.eval("ruleContext.coverage_instrumented(ruleContext.attr.deps[0])");
+    assertThat((Boolean) result).isTrue();
+  }
+
+  // A list of attributes and methods ctx objects have
+  private final List<String> ctxAttributes =
+      ImmutableList.of(
+          "attr",
+          "split_attr",
+          "executable",
+          "file",
+          "files",
+          "workspace_name",
+          "label",
+          "fragments",
+          "host_fragments",
+          "configuration",
+          "host_configuration",
+          "coverage_instrumented(dep)",
+          "features",
+          "bin_dir",
+          "genfiles_dir",
+          "outputs",
+          "rule",
+          "aspect_ids",
+          "var",
+          "tokenize('foo')",
+          "expand('foo', [], Label('//test:main'))",
+          "new_file('foo.txt')",
+          "new_file(file, 'foo.txt')",
+          "actions.declare_file('foo.txt')",
+          "actions.declare_file('foo.txt', sibling = file)",
+          "actions.declare_directory('foo.txt')",
+          "actions.declare_directory('foo.txt', sibling = file)",
+          "actions.do_nothing(mnemonic = 'foo', inputs = [file])",
+          "actions.expand_template(template = file, output = file, substitutions = {})",
+          "actions.run(executable = file, outputs = [file])",
+          "actions.run_shell(command = 'foo', outputs = [file])",
+          "actions.write(file, 'foo')",
+          "check_placeholders('foo', [])",
+          "runfiles()",
+          "resolve_command(command = 'foo')",
+          "resolve_tools()");
+
+  @Test
+  public void testFrozenRuleContextHasInaccessibleAttributes() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_new_actions_api=false");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'main_rule', 'dep_rule')",
+        "dep_rule(name = 'dep')",
+        "main_rule(name = 'main', deps = [':dep'])");
+    scratch.file("test/rules.bzl");
+
+    for (String attribute : ctxAttributes) {
+      scratch.overwriteFile(
+          "test/rules.bzl",
+          "load('//myinfo:myinfo.bzl', 'MyInfo')",
+          "def _main_impl(ctx):",
+          "  dep = ctx.attr.deps[0]",
+          "  file = ctx.outputs.file",
+          "  foo = dep[MyInfo].dep_ctx." + attribute,
+          "main_rule = rule(",
+          "  implementation = _main_impl,",
+          "  attrs = {",
+          "    'deps': attr.label_list()",
+          "  },",
+          "  outputs = {'file': 'output.txt'},",
+          ")",
+          "def _dep_impl(ctx):",
+          "  return MyInfo(dep_ctx = ctx)",
+          "dep_rule = rule(implementation = _dep_impl)");
+      invalidatePackages();
+      AssertionError e =
+          assertThrows(
+              "Should have been unable to access dep_ctx." + attribute,
+              AssertionError.class,
+              () -> getConfiguredTarget("//test:main"));
+      assertThat(e)
+          .hasMessageThat()
+          .contains(
+              "cannot access field or method '"
+                  + Iterables.get(Splitter.on('(').split(attribute), 0)
+                  + "' of rule context for '//test:dep' outside of its own rule implementation "
+                  + "function");
+    }
+  }
+
+  @Test
+  public void testFrozenRuleContextForAspectsHasInaccessibleAttributes() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_new_actions_api=false");
+    List<String> attributes = new ArrayList<>();
+    attributes.addAll(ctxAttributes);
+    attributes.addAll(
+        ImmutableList.of("rule.attr", "rule.executable", "rule.file", "rule.files", "rule.kind"));
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'my_rule')",
+        "my_rule(name = 'dep')",
+        "my_rule(name = 'mid', deps = [':dep'])",
+        "my_rule(name = 'main', deps = [':mid'])");
+    scratch.file("test/rules.bzl");
+    for (String attribute : attributes) {
+      scratch.overwriteFile(
+          "test/rules.bzl",
+          "def _rule_impl(ctx):",
+          "  pass",
+          "def _aspect_impl(target, ctx):",
+          "  if ctx.rule.attr.deps:",
+          "    dep = ctx.rule.attr.deps[0]",
+          "    file = ctx.actions.declare_file('file.txt')",
+          "    foo = dep." + (attribute.startsWith("rule.") ? "" : "ctx.") + attribute,
+          "  return struct(ctx = ctx, rule=ctx.rule)",
+          "MyAspect = aspect(implementation=_aspect_impl)",
+          "my_rule = rule(",
+          "  implementation = _rule_impl,",
+          "  attrs = {",
+          "    'deps': attr.label_list(aspects = [MyAspect])",
+          "  },",
+          ")");
+      invalidatePackages();
+      AssertionError e =
+          assertThrows(
+              "Should have been unable to access dep." + attribute,
+              AssertionError.class,
+              () -> getConfiguredTarget("//test:main"));
+      assertThat(e)
+          .hasMessageThat()
+          .contains(
+              "cannot access field or method '"
+                  + Iterables.get(Splitter.on('(').split(attribute), 0)
+                  + "' of rule context for '//test:dep' outside of its own rule implementation "
+                  + "function");
+    }
+  }
+
+  private static final List<String> deprecatedActionsApi =
+      ImmutableList.of("new_file('foo.txt')", "new_file(file, 'foo.txt')");
+
+  @Test
+  public void testIncompatibleNewActionsApi() throws Exception {
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'main_rule')", "main_rule(name = 'main')");
+    scratch.file("test/rules.bzl");
+
+    for (String actionApi : deprecatedActionsApi) {
+      scratch.overwriteFile(
+          "test/rules.bzl",
+          "def _main_impl(ctx):",
+          "  file = ctx.outputs.file",
+          "  foo = ctx." + actionApi,
+          "main_rule = rule(",
+          "  implementation = _main_impl,",
+          "  attrs = {",
+          "    'deps': attr.label_list()",
+          "  },",
+          "  outputs = {'file': 'output.txt'},",
+          ")");
+      setStarlarkSemanticsOptions("--incompatible_new_actions_api=true");
+      invalidatePackages();
+      AssertionError e =
+          assertThrows(
+              "Should have reported deprecation error for: " + actionApi,
+              AssertionError.class,
+              () -> getConfiguredTarget("//test:main"));
+      assertWithMessage(actionApi + " reported wrong error")
+          .that(e)
+          .hasMessageThat()
+          .contains("Use --incompatible_new_actions_api=false");
+    }
+  }
+
+  @Test
+  public void testMapAttributeOrdering() throws Exception {
+    scratch.file(
+        "a/a.bzl",
+        "key_provider = provider(fields=['keys'])",
+        "def _impl(ctx):",
+        "  return [key_provider(keys=ctx.attr.value.keys())]",
+        "a = rule(implementation=_impl, attrs={'value': attr.string_dict()})");
+    scratch.file(
+        "a/BUILD",
+        "load(':a.bzl', 'a')",
+        "a(name='a', value={'c': 'c', 'b': 'b', 'a': 'a', 'f': 'f', 'e': 'e', 'd': 'd'})");
+
+    ConfiguredTarget a = getConfiguredTarget("//a");
+    StarlarkProvider.Key key =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//a:a.bzl", ImmutableMap.of()), "key_provider");
+
+    StarlarkInfo keyInfo = (StarlarkInfo) a.get(key);
+    Sequence<?> keys = (Sequence) keyInfo.getValue("keys");
+    assertThat(keys).containsExactly("c", "b", "a", "f", "e", "d").inOrder();
+  }
+
+  private void writeIntFlagBuildSettingFiles() throws Exception {
+    scratch.file(
+        "test/build_setting.bzl",
+        "BuildSettingInfo = provider(fields = ['name', 'value'])",
+        "def _impl(ctx):",
+        "  return [BuildSettingInfo(name = ctx.attr.name, value = ctx.build_setting_value)]",
+        "",
+        "int_flag = rule(",
+        "  implementation = _impl,",
+        "  build_setting = config.int(flag = True),",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', 'int_flag')",
+        "int_flag(name = 'int_flag', build_setting_default = 42)");
+  }
+
+  @Test
+  public void testBuildSettingValue_explicitlySet() throws Exception {
+    writeIntFlagBuildSettingFiles();
+    useConfiguration(ImmutableMap.of("//test:int_flag", 24));
+
+    ConfiguredTarget buildSetting = getConfiguredTarget("//test:int_flag");
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.create(buildSetting.getLabel().getPackageIdentifier(), "build_setting.bzl"),
+            "BuildSettingInfo");
+    StructImpl buildSettingInfo = (StructImpl) buildSetting.get(key);
+
+    assertThat(buildSettingInfo.getValue("value")).isEqualTo(24);
+  }
+
+  @Test
+  public void testBuildSettingValue_defaultFallback() throws Exception {
+    writeIntFlagBuildSettingFiles();
+
+    ConfiguredTarget buildSetting = getConfiguredTarget("//test:int_flag");
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.create(buildSetting.getLabel().getPackageIdentifier(), "build_setting.bzl"),
+            "BuildSettingInfo");
+    StructImpl buildSettingInfo = (StructImpl) buildSetting.get(key);
+
+    assertThat(buildSettingInfo.getValue("value")).isEqualTo(42);
+  }
+
+  @Test
+  public void testBuildSettingValue_nonBuildSettingRule() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _impl(ctx):",
+        "  foo = ctx.build_setting_value",
+        "  return []",
+        "non_build_setting = rule(implementation = _impl)");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'non_build_setting')",
+        "non_build_setting(name = 'my_non_build_setting')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_non_build_setting");
+    assertContainsEvent(
+        "attempting to access 'build_setting_value' of non-build setting "
+            + "//test:my_non_build_setting");
+  }
+
+  private void createToolchains() throws Exception {
+    scratch.file(
+        "rule/test_toolchain.bzl",
+        "def _impl(ctx):",
+        "    value = ctx.attr.value",
+        "    toolchain = platform_common.ToolchainInfo(value = value)",
+        "    return [toolchain]",
+        "test_toolchain = rule(",
+        "    implementation = _impl,",
+        "    attrs = {'value': attr.string()},",
+        ")");
+    scratch.file(
+        "rule/test_rule.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "    toolchain = ctx.toolchains['//rule:toolchain_type']",
+        "    return [result(",
+        "        value_from_toolchain = toolchain.value,",
+        "    )]",
+        "test_rule = rule(",
+        "    implementation = _impl,",
+        "    toolchains = ['//rule:toolchain_type'],",
+        ")");
+    scratch.file(
+        "rule/BUILD",
+        "exports_files(['test_toolchain/bzl', 'test_rule.bzl'])",
+        "toolchain_type(name = 'toolchain_type')");
+    scratch.file(
+        "toolchain/BUILD",
+        "load('//rule:test_toolchain.bzl', 'test_toolchain')",
+        "test_toolchain(",
+        "    name = 'foo',",
+        "    value = 'foo',",
+        ")",
+        "toolchain(",
+        "    name = 'foo_toolchain',",
+        "    toolchain_type = '//rule:toolchain_type',",
+        "    target_compatible_with = ['//platform:constraint_1'],",
+        "    toolchain = ':foo',",
+        ")",
+        "test_toolchain(",
+        "    name = 'bar',",
+        "    value = 'bar',",
+        ")",
+        "toolchain(",
+        "    name = 'bar_toolchain',",
+        "    toolchain_type = '//rule:toolchain_type',",
+        "    target_compatible_with = ['//platform:constraint_2'],",
+        "    toolchain = ':bar',",
+        ")");
+  }
+
+  private void createPlatforms() throws Exception {
+    scratch.file(
+        "platform/BUILD",
+        "constraint_setting(name = 'setting')",
+        "constraint_value(",
+        "    name = 'constraint_1',",
+        "    constraint_setting = ':setting',",
+        ")",
+        "constraint_value(",
+        "    name = 'constraint_2',",
+        "    constraint_setting = ':setting',",
+        ")",
+        "platform(",
+        "    name = 'platform_1',",
+        "    constraint_values = [':constraint_1'],",
+        ")",
+        "platform(",
+        "    name = 'platform_2',",
+        "    constraint_values = [':constraint_2'],",
+        ")");
+  }
+
+  private String getToolchainResult(String targetName) throws Exception {
+    ConfiguredTarget myRuleTarget = getConfiguredTarget(targetName);
+    StructImpl info =
+        (StructImpl)
+            myRuleTarget.get(
+                new StarlarkProvider.Key(
+                    Label.parseAbsolute("//rule:test_rule.bzl", ImmutableMap.of()), "result"));
+
+    assertThat(info).isNotNull();
+    return (String) info.getValue("value_from_toolchain");
+  }
+
+  @Test
+  public void testToolchains() throws Exception {
+    createToolchains();
+    createPlatforms();
+    scratch.file(
+        "demo/BUILD",
+        "load('//rule:test_rule.bzl', 'test_rule')",
+        "test_rule(",
+        "    name = 'demo',",
+        ")");
+
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain",
+        "--platforms=//platform:platform_1");
+    String value = getToolchainResult("//demo");
+    assertThat(value).isEqualTo("foo");
+
+    // Re-test with the other platform.
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain",
+        "--platforms=//platform:platform_2");
+    value = getToolchainResult("//demo");
+    assertThat(value).isEqualTo("bar");
+  }
+
+  @Test
+  public void testTargetPlatformHasConstraint() throws Exception {
+    createPlatforms();
+
+    scratch.file(
+        "demo/test_rule.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "    constraint = ctx.attr._constraint[platform_common.ConstraintValueInfo]",
+        "    has_constraint = ctx.target_platform_has_constraint(constraint)",
+        "    return [result(",
+        "        has_constraint = has_constraint,",
+        "    )]",
+        "test_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "        '_constraint': attr.label(default = '//platform:constraint_1'),",
+        "    },",
+        ")");
+    scratch.file(
+        "demo/BUILD",
+        "load(':test_rule.bzl', 'test_rule')",
+        "test_rule(",
+        "    name = 'demo',",
+        ")");
+
+    useConfiguration("--platforms=//platform:platform_1");
+
+    ConfiguredTarget myRuleTarget = getConfiguredTarget("//demo");
+    StructImpl info =
+        (StructImpl)
+            myRuleTarget.get(
+                new StarlarkProvider.Key(
+                    Label.parseAbsolute("//demo:test_rule.bzl", ImmutableMap.of()), "result"));
+
+    assertThat(info).isNotNull();
+    boolean hasConstraint = (boolean) info.getValue("has_constraint");
+    assertThat(hasConstraint).isTrue();
+
+    // Re-test with the other platform.
+    useConfiguration("--platforms=//platform:platform_2");
+    myRuleTarget = getConfiguredTarget("//demo");
+    info =
+        (StructImpl)
+            myRuleTarget.get(
+                new StarlarkProvider.Key(
+                    Label.parseAbsolute("//demo:test_rule.bzl", ImmutableMap.of()), "result"));
+
+    assertThat(info).isNotNull();
+    hasConstraint = (boolean) info.getValue("has_constraint");
+    assertThat(hasConstraint).isFalse();
+  }
+
+  private void writeExecGroups() throws Exception {
+    createToolchains();
+    createPlatforms();
+    scratch.file(
+        "something/defs.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "  exec_groups = ctx.exec_groups",
+        "  toolchain = ctx.exec_groups['dragonfruit'].toolchains['//rule:toolchain_type']",
+        "  return [result(",
+        "    toolchain_value = toolchain.value,",
+        "    exec_groups = exec_groups,",
+        "  )]",
+        "use_exec_groups = rule(",
+        "  implementation = _impl,",
+        "  exec_groups = {",
+        "    'dragonfruit': exec_group(toolchains = ['//rule:toolchain_type']),",
+        "  },",
+        ")");
+    scratch.file(
+        "something/BUILD",
+        "load('//something:defs.bzl', 'use_exec_groups')",
+        "use_exec_groups(name = 'nectarine')");
+    setStarlarkSemanticsOptions("--experimental_exec_groups=true");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain",
+        "--platforms=//platform:platform_1");
+  }
+
+  @Test
+  public void testExecGroup_toolchain() throws Exception {
+    writeExecGroups();
+
+    ConfiguredTarget target = getConfiguredTarget("//something:nectarine");
+    StructImpl info =
+        (StructImpl)
+            target.get(
+                new StarlarkProvider.Key(
+                    Label.parseAbsoluteUnchecked("//something:defs.bzl"), "result"));
+    assertThat(info).isNotNull();
+    assertThat(info.getValue("toolchain_value")).isEqualTo("foo");
+    assertThat(info.getValue("exec_groups")).isInstanceOf(ExecGroupCollection.class);
+    ImmutableMap<String, ResolvedToolchainContext> toolchainContexts =
+        ((ExecGroupCollection) info.getValue("exec_groups")).getToolchainCollectionForTesting();
+    assertThat(toolchainContexts.keySet()).containsExactly(DEFAULT_EXEC_GROUP_NAME, "dragonfruit");
+    assertThat(toolchainContexts.get(DEFAULT_EXEC_GROUP_NAME).requiredToolchainTypes()).isEmpty();
+    assertThat(toolchainContexts.get("dragonfruit").resolvedToolchainLabels())
+        .containsExactly(Label.parseAbsoluteUnchecked("//toolchain:foo"));
+  }
+
+  // Tests for an error that occurs when two exec groups have different requirements (toolchain
+  // types and exec constraints), but have the same toolchain type. This also requires the toolchain
+  // transition to be enabled.
+  @Test
+  public void testExecGroup_duplicateToolchainType() throws Exception {
+    createToolchains();
+    createPlatforms();
+    scratch.file(
+        "something/defs.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "  exec_groups = ctx.exec_groups",
+        "  toolchain = ctx.exec_groups['dragonfruit'].toolchains['//rule:toolchain_type']",
+        "  return [result(",
+        "    toolchain_value = toolchain.value,",
+        "    exec_groups = exec_groups,",
+        "  )]",
+        "use_exec_groups = rule(",
+        "  implementation = _impl,",
+        "  exec_groups = {",
+        "    'dragonfruit': exec_group(toolchains = ['//rule:toolchain_type']),",
+        "    'passionfruit': exec_group(",
+        "      toolchains = ['//rule:toolchain_type'],",
+        "      exec_compatible_with = ['//something:extra'],",
+        "    ),",
+        "  },",
+        "  incompatible_use_toolchain_transition = True,",
+        ")");
+    scratch.file(
+        "something/BUILD",
+        "constraint_setting(name = 'setting', default_constraint_value = ':extra')",
+        "constraint_value(name = 'extra', constraint_setting = ':setting')",
+        "load('//something:defs.bzl', 'use_exec_groups')",
+        "use_exec_groups(name = 'nectarine')");
+    setStarlarkSemanticsOptions("--experimental_exec_groups=true");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain,//toolchain:bar_toolchain",
+        "--platforms=//platform:platform_1");
+
+    ConfiguredTarget target = getConfiguredTarget("//something:nectarine");
+    StructImpl info =
+        (StructImpl)
+            target.get(
+                new StarlarkProvider.Key(
+                    Label.parseAbsoluteUnchecked("//something:defs.bzl"), "result"));
+    assertThat(info).isNotNull();
+    assertThat(info.getValue("toolchain_value")).isEqualTo("foo");
+    assertThat(info.getValue("exec_groups")).isInstanceOf(ExecGroupCollection.class);
+    ImmutableMap<String, ResolvedToolchainContext> toolchainContexts =
+        ((ExecGroupCollection) info.getValue("exec_groups")).getToolchainCollectionForTesting();
+    assertThat(toolchainContexts.keySet())
+        .containsExactly(DEFAULT_EXEC_GROUP_NAME, "dragonfruit", "passionfruit");
+    assertThat(toolchainContexts.get(DEFAULT_EXEC_GROUP_NAME).requiredToolchainTypes()).isEmpty();
+    assertThat(toolchainContexts.get("dragonfruit").resolvedToolchainLabels())
+        .containsExactly(Label.parseAbsoluteUnchecked("//toolchain:foo"));
+    assertThat(toolchainContexts.get("passionfruit").resolvedToolchainLabels())
+        .containsExactly(Label.parseAbsoluteUnchecked("//toolchain:foo"));
+  }
+
+  @Test
+  public void testInvalidExecGroup() throws Exception {
+    writeExecGroups();
+
+    scratch.overwriteFile(
+        "something/defs.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "  exec_groups = ctx.exec_groups",
+        "  toolchain = ctx.exec_groups['unknown_fruit']",
+        "  return []",
+        "use_exec_groups = rule(",
+        "  implementation = _impl,",
+        "  exec_groups = {",
+        "    'dragonfruit': exec_group(toolchains = ['//rule:toolchain_type']),",
+        "  },",
+        ")");
+
+    assertThrows(AssertionError.class, () -> getConfiguredTarget("//something:nectarine"));
+    assertContainsEvent(
+        "unrecognized exec group 'unknown_fruit' requested. Available exec groups: [dragonfruit]");
+  }
+
+  @Test
+  public void testCannotAccessDefaultGroupViaExecGroups() throws Exception {
+    writeExecGroups();
+
+    scratch.overwriteFile(
+        "something/defs.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "  exec_groups = ctx.exec_groups",
+        "  toolchain = ctx.exec_groups['" + DEFAULT_EXEC_GROUP_NAME + "']",
+        "  return []",
+        "use_exec_groups = rule(",
+        "  implementation = _impl,",
+        "  exec_groups = {",
+        "    'dragonfruit': exec_group(toolchains = ['//rule:toolchain_type']),",
+        "  },",
+        ")");
+
+    assertThrows(AssertionError.class, () -> getConfiguredTarget("//something:nectarine"));
+    assertContainsEvent(
+        "unrecognized exec group '"
+            + DEFAULT_EXEC_GROUP_NAME
+            + "' requested. Available exec groups: [dragonfruit]");
+  }
+
+  @Test
+  public void testInvalidExecGroupName() throws Exception {
+    writeExecGroups();
+    String badName = "1bad-stuff-name";
+
+    scratch.overwriteFile(
+        "something/defs.bzl",
+        "result = provider()",
+        "def _impl(ctx):",
+        "  exec_groups = ctx.exec_groups",
+        "  toolchain = ctx.exec_groups['" + badName + "']",
+        "  return []",
+        "use_exec_groups = rule(",
+        "  implementation = _impl,",
+        "  exec_groups = {",
+        "    '" + badName + "': exec_group(toolchains = ['//rule:toolchain_type']),",
+        "  },",
+        ")");
+
+    assertThrows(AssertionError.class, () -> getConfiguredTarget("//something:nectarine"));
+    assertContainsEvent("Exec group name '" + badName + "' is not a valid name.");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleImplementationFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleImplementationFunctionsTest.java
new file mode 100644
index 0000000..00bbdad
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleImplementationFunctionsTest.java
@@ -0,0 +1,3263 @@
+// Copyright 2014 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.ActionLookupKey;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
+import com.google.devtools.build.lib.actions.Artifact.ArtifactExpanderImpl;
+import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact;
+import com.google.devtools.build.lib.actions.CommandLine;
+import com.google.devtools.build.lib.actions.CommandLineExpansionException;
+import com.google.devtools.build.lib.actions.CompositeRunfilesSupplier;
+import com.google.devtools.build.lib.actions.RunfilesSupplier;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DefaultInfo;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.analysis.actions.Substitution;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.starlark.Args;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.Provider;
+import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.StructImpl;
+import com.google.devtools.build.lib.starlark.util.BazelEvaluationTestCase;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.Sequence;
+import com.google.devtools.build.lib.syntax.Starlark;
+import com.google.devtools.build.lib.syntax.StarlarkList;
+import com.google.devtools.build.lib.syntax.StarlarkThread;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import net.starlark.java.annot.Param;
+import net.starlark.java.annot.StarlarkGlobalLibrary;
+import net.starlark.java.annot.StarlarkMethod;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for Starlark functions relating to rule implementation. */
+@RunWith(JUnit4.class)
+@StarlarkGlobalLibrary // needed for CallUtils.getBuiltinCallable, sadly
+public class StarlarkRuleImplementationFunctionsTest extends BuildViewTestCase {
+
+  private final BazelEvaluationTestCase ev = new BazelEvaluationTestCase();
+
+  private StarlarkRuleContext createRuleContext(String label) throws Exception {
+    return new StarlarkRuleContext(
+        getRuleContextForStarlark(getConfiguredTarget(label)), null, getStarlarkSemantics());
+  }
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  // def mock(mandatory, optional=None, *, mandatory_key, optional_key='x')
+  @StarlarkMethod(
+      name = "mock",
+      documented = false,
+      parameters = {
+        @Param(name = "mandatory", doc = "", named = true),
+        @Param(name = "optional", doc = "", defaultValue = "None", noneable = true, named = true),
+        @Param(name = "mandatory_key", doc = "", positional = false, named = true),
+        @Param(
+            name = "optional_key",
+            doc = "",
+            defaultValue = "'x'",
+            positional = false,
+            named = true)
+      },
+      useStarlarkThread = true)
+  public Object mock(
+      Object mandatory,
+      Object optional,
+      Object mandatoryKey,
+      Object optionalKey,
+      StarlarkThread thread) {
+    Map<String, Object> m = new HashMap<>();
+    m.put("mandatory", mandatory);
+    m.put("optional", optional);
+    m.put("mandatory_key", mandatoryKey);
+    m.put("optional_key", optionalKey);
+    return m;
+  }
+
+  @Before
+  public final void createBuildFile() throws Exception {
+    scratch.file("myinfo/myinfo.bzl", "MyInfo = provider()");
+
+    scratch.file("myinfo/BUILD");
+
+    scratch.file(
+        "foo/BUILD",
+        "genrule(name = 'foo',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = ['a.txt', 'b.img'],",
+        "  tools = ['t.exe'],",
+        "  outs = ['c.txt'])",
+        "genrule(name = 'bar',",
+        "  cmd = 'dummy_cmd',",
+        "  srcs = [':jl', ':gl'],",
+        "  outs = ['d.txt'])",
+        "genrule(name = 'baz',",
+        "  cmd = 'dummy_cmd',",
+        "  outs = ['e.txt'])",
+        "java_library(name = 'jl',",
+        "  srcs = ['a.java'])",
+        "genrule(name = 'gl',",
+        "  cmd = 'touch $(OUTS)',",
+        "  srcs = ['a.go'],",
+        "  outs = [ 'gl.a', 'gl.gcgox', ],",
+        "  output_to_bindir = 1,",
+        ")",
+        // The target below is used by testResolveCommand and testResolveTools
+        "sh_binary(name = 'mytool',",
+        "  srcs = ['mytool.sh'],",
+        "  data = ['file1.dat', 'file2.dat'],",
+        ")",
+        // The target below is used by testResolveCommand and testResolveTools
+        "genrule(name = 'resolve_me',",
+        "  cmd = 'aa',",
+        "  tools = [':mytool', 't.exe'],",
+        "  srcs = ['file3.dat', 'file4.dat'],",
+        "  outs = ['r1.txt', 'r2.txt'],",
+        ")");
+  }
+
+  private void setRuleContext(StarlarkRuleContext ctx) throws Exception {
+    ev.update("ruleContext", ctx);
+  }
+
+  private static void assertArtifactFilenames(Iterable<Artifact> artifacts, String... expected) {
+    ImmutableList.Builder<String> filenames = ImmutableList.builder();
+    for (Artifact a : artifacts) {
+      filenames.add(a.getFilename());
+    }
+    assertThat(filenames.build()).containsAtLeastElementsIn(Lists.newArrayList(expected));
+  }
+
+  private StructImpl getMyInfoFromTarget(ConfiguredTarget configuredTarget) throws Exception {
+    Provider.Key key =
+        new StarlarkProvider.Key(
+            Label.parseAbsolute("//myinfo:myinfo.bzl", ImmutableMap.of()), "MyInfo");
+    return (StructImpl) configuredTarget.get(key);
+  }
+
+  // Defines all @StarlarkCallable-annotated methods (mock, throw, ...) in the environment.
+  private void defineTestMethods() throws Exception {
+    ImmutableMap.Builder<String, Object> env = ImmutableMap.builder();
+    Starlark.addMethods(env, this);
+    for (Map.Entry<String, Object> entry : env.build().entrySet()) {
+      ev.update(entry.getKey(), entry.getValue());
+    }
+  }
+
+  private void checkStarlarkFunctionError(String errorSubstring, String line) throws Exception {
+    defineTestMethods();
+    EvalException e = assertThrows(EvalException.class, () -> ev.exec(line));
+    assertThat(e).hasMessageThat().contains(errorSubstring);
+  }
+
+  // TODO(adonovan): move these tests of the interpreter core into lib.syntax.
+
+  @Test
+  public void testStarlarkFunctionPosArgs() throws Exception {
+    defineTestMethods();
+    ev.exec("a = mock('a', 'b', mandatory_key='c')");
+    Map<?, ?> params = (Map<?, ?>) ev.lookup("a");
+    assertThat(params.get("mandatory")).isEqualTo("a");
+    assertThat(params.get("optional")).isEqualTo("b");
+    assertThat(params.get("mandatory_key")).isEqualTo("c");
+    assertThat(params.get("optional_key")).isEqualTo("x");
+  }
+
+  @Test
+  public void testStarlarkFunctionKwArgs() throws Exception {
+    defineTestMethods();
+    ev.exec("a = mock(optional='b', mandatory='a', mandatory_key='c')");
+    Map<?, ?> params = (Map<?, ?>) ev.lookup("a");
+    assertThat(params.get("mandatory")).isEqualTo("a");
+    assertThat(params.get("optional")).isEqualTo("b");
+    assertThat(params.get("mandatory_key")).isEqualTo("c");
+    assertThat(params.get("optional_key")).isEqualTo("x");
+  }
+
+  @Test
+  public void testStarlarkFunctionTooFewArguments() throws Exception {
+    checkStarlarkFunctionError(
+        "missing 1 required positional argument: mandatory", "mock(mandatory_key='y')");
+  }
+
+  @Test
+  public void testStarlarkFunctionTooManyArguments() throws Exception {
+    checkStarlarkFunctionError(
+        "mock() accepts no more than 2 positional arguments but got 3",
+        "mock('a', 'b', 'c', mandatory_key='y')");
+  }
+
+  @Test
+  public void testStarlarkFunctionAmbiguousArguments() throws Exception {
+    checkStarlarkFunctionError(
+        "mock() got multiple values for argument 'mandatory'",
+        "mock('by position', mandatory='by_key', mandatory_key='c')");
+  }
+
+  @Test
+  public void testCreateSpawnActionCreatesSpawnAction() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    createTestSpawnAction(ruleContext);
+    ActionAnalysisMetadata action =
+        Iterables.getOnlyElement(
+            ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action).isInstanceOf(SpawnAction.class);
+  }
+
+  @Test
+  public void testArtifactPath() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    String result = (String) ev.eval("ruleContext.files.tools[0].path");
+    assertThat(result).isEqualTo("foo/t.exe");
+  }
+
+  @Test
+  public void testArtifactShortPath() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    String result = (String) ev.eval("ruleContext.files.tools[0].short_path");
+    assertThat(result).isEqualTo("foo/t.exe");
+  }
+
+  @Test
+  public void testCreateSpawnActionArgumentsWithCommand() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    createTestSpawnAction(ruleContext);
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertArtifactFilenames(action.getInputs().toList(), "a.txt", "b.img");
+    assertArtifactFilenames(action.getOutputs(), "a.txt", "b.img");
+    MoreAsserts.assertContainsSublist(
+        action.getArguments(), "-c", "dummy_command", "", "--a", "--b");
+    assertThat(action.getMnemonic()).isEqualTo("DummyMnemonic");
+    assertThat(action.getProgressMessage()).isEqualTo("dummy_message");
+    assertThat(action.getIncompleteEnvironmentForTesting())
+        .isEqualTo(targetConfig.getLocalShellEnvironment());
+  }
+
+  @Test
+  public void testCreateSpawnActionArgumentsWithExecutable() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['--a','--b'],",
+        "  executable = ruleContext.files.tools[0])");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertArtifactFilenames(action.getInputs().toList(), "a.txt", "b.img", "t.exe");
+    assertArtifactFilenames(action.getOutputs(), "a.txt", "b.img");
+    MoreAsserts.assertContainsSublist(action.getArguments(), "foo/t.exe", "--a", "--b");
+  }
+
+  @Test
+  public void testCreateActionWithDepsetInput() throws Exception {
+    // Same test as above, with depset as inputs.
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['--a','--b'],",
+        "  executable = ruleContext.files.tools[0])");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertArtifactFilenames(action.getInputs().toList(), "a.txt", "b.img", "t.exe");
+    assertArtifactFilenames(action.getOutputs(), "a.txt", "b.img");
+    MoreAsserts.assertContainsSublist(action.getArguments(), "foo/t.exe", "--a", "--b");
+  }
+
+  @Test
+  public void testCreateSpawnActionArgumentsBadExecutable() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "got value of type 'int', want 'File or string or FilesToRunProvider'",
+        "ruleContext.actions.run(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['--a','--b'],",
+        "  executable = 123)");
+  }
+
+  @Test
+  public void testCreateSpawnActionShellCommandList() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run_shell(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  mnemonic = 'DummyMnemonic',",
+        "  command = ['dummy_command', '--arg1', '--arg2'],",
+        "  progress_message = 'dummy_message')");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly("dummy_command", "--arg1", "--arg2")
+        .inOrder();
+  }
+
+  @Test
+  public void testCreateSpawnActionEnvAndExecInfo() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run_shell(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  env = {'a' : 'b'},",
+        "  execution_requirements = {'timeout' : '10', 'block-network' : 'foo'},",
+        "  mnemonic = 'DummyMnemonic',",
+        "  command = 'dummy_command',",
+        "  progress_message = 'dummy_message')");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getIncompleteEnvironmentForTesting()).containsExactly("a", "b");
+    // We expect "timeout" to be filtered by TargetUtils.
+    assertThat(action.getExecutionInfo()).containsExactly("block-network", "foo");
+  }
+
+  @Test
+  public void testCreateSpawnActionEnvAndExecInfo_withWorkerKeyMnemonic() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.run_shell(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  env = {'a' : 'b'},",
+        "  execution_requirements = {",
+        "    'supports-workers': '1',",
+        "    'worker-key-mnemonic': 'MyMnemonic',",
+        "  },",
+        "  mnemonic = 'DummyMnemonic',",
+        "  command = 'dummy_command',",
+        "  progress_message = 'dummy_message')");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getExecutionInfo())
+        .containsExactly("supports-workers", "1", "worker-key-mnemonic", "MyMnemonic");
+  }
+
+  @Test
+  public void testCreateSpawnActionUnknownParam() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "run() got unexpected keyword argument 'bad_param'",
+        "f = ruleContext.actions.declare_file('foo.sh')",
+        "ruleContext.actions.run(outputs=[], bad_param = 'some text', executable = f)");
+  }
+
+  private Object createTestSpawnAction(StarlarkRuleContext ruleContext) throws Exception {
+    setRuleContext(ruleContext);
+    return ev.eval(
+        "ruleContext.actions.run_shell(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['--a','--b'],",
+        "  mnemonic = 'DummyMnemonic',",
+        "  command = 'dummy_command',",
+        "  progress_message = 'dummy_message',",
+        "  use_default_shell_env = True)");
+  }
+
+  @Test
+  public void testCreateSpawnActionBadGenericArg() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "at index 0 of outputs, got element of type string, want File",
+        "l = ['a', 'b']",
+        "ruleContext.actions.run_shell(",
+        "  outputs = l,",
+        "  command = 'dummy_command')");
+  }
+
+  @Test
+  public void testRunShellArgumentsWithCommandSequence() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "'arguments' must be empty if 'command' is a sequence of strings",
+        "ruleContext.actions.run_shell(outputs = ruleContext.files.srcs,",
+        "  command = [\"echo\", \"'hello world'\", \"&&\", \"touch\"],",
+        "  arguments = [ruleContext.files.srcs[0].path])");
+  }
+
+  private void setupToolInInputsTest(String... ruleImpl) throws Exception {
+    ImmutableList.Builder<String> lines = ImmutableList.builder();
+    lines.add("def _main_rule_impl(ctx):");
+    for (String line : ruleImpl) {
+      lines.add("  " + line);
+    }
+    lines.add(
+        "my_rule = rule(",
+        "  _main_rule_impl,",
+        "  attrs = { ",
+        "    'exe' : attr.label(executable = True, allow_files = True, cfg='host'),",
+        "  },",
+        ")");
+    scratch.file("bar/bar.bzl", lines.build().toArray(new String[] {}));
+    scratch.file(
+        "bar/BUILD",
+        "load('//bar:bar.bzl', 'my_rule')",
+        "sh_binary(",
+        "  name = 'mytool',",
+        "  srcs = ['mytool.sh'],",
+        "  data = ['file1.dat', 'file2.dat'],",
+        ")",
+        "my_rule(",
+        "  name = 'my_rule',",
+        "  exe = ':mytool',",
+        ")");
+  }
+
+  @Test
+  public void testCreateSpawnActionWithToolInInputsLegacy() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_no_support_tools_in_action_inputs=false");
+    setupToolInInputsTest(
+        "output = ctx.actions.declare_file('bar.out')",
+        "ctx.actions.run_shell(",
+        "  inputs = ctx.attr.exe.files,",
+        "  outputs = [output],",
+        "  command = 'boo bar baz',",
+        ")");
+    RuleConfiguredTarget target = (RuleConfiguredTarget) getConfiguredTarget("//bar:my_rule");
+    SpawnAction action = (SpawnAction) Iterables.getOnlyElement(target.getActions());
+    assertThat(action.getTools().toList()).isNotEmpty();
+  }
+
+  @Test
+  public void testCreateSpawnActionWithToolAttribute() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_no_support_tools_in_action_inputs=true");
+    setupToolInInputsTest(
+        "output = ctx.actions.declare_file('bar.out')",
+        "ctx.actions.run_shell(",
+        "  inputs = [],",
+        "  tools = ctx.attr.exe.files,",
+        "  outputs = [output],",
+        "  command = 'boo bar baz',",
+        ")");
+    RuleConfiguredTarget target = (RuleConfiguredTarget) getConfiguredTarget("//bar:my_rule");
+    SpawnAction action = (SpawnAction) Iterables.getOnlyElement(target.getActions());
+    assertThat(action.getTools().toList()).isNotEmpty();
+  }
+
+  @Test
+  public void testCreateSpawnActionWithToolAttributeIgnoresToolsInInputs() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_no_support_tools_in_action_inputs=true");
+    setupToolInInputsTest(
+        "output = ctx.actions.declare_file('bar.out')",
+        "ctx.actions.run_shell(",
+        "  inputs = ctx.attr.exe.files,",
+        "  tools = ctx.attr.exe.files,",
+        "  outputs = [output],",
+        "  command = 'boo bar baz',",
+        ")");
+    RuleConfiguredTarget target = (RuleConfiguredTarget) getConfiguredTarget("//bar:my_rule");
+    SpawnAction action = (SpawnAction) Iterables.getOnlyElement(target.getActions());
+    assertThat(action.getTools().toList()).isNotEmpty();
+  }
+
+  @Test
+  public void testCreateSpawnActionWithToolInInputsFailAtAnalysisTime() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_no_support_tools_in_action_inputs=true");
+    setupToolInInputsTest(
+        "output = ctx.actions.declare_file('bar.out')",
+        "ctx.actions.run_shell(",
+        "  inputs = ctx.attr.exe.files,",
+        "  outputs = [output],",
+        "  command = 'boo bar baz',",
+        ")");
+    try {
+      getConfiguredTarget("//bar:my_rule");
+    } catch (Throwable t) {
+      // Expected
+    }
+    assertThat(eventCollector).hasSize(1);
+    assertThat(eventCollector.iterator().next().getMessage())
+        .containsMatch("Found tool\\(s\\) '.*' in inputs");
+  }
+
+  @Test
+  public void testCreateFileAction() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.write(",
+        "  output = ruleContext.files.srcs[0],",
+        "  content = 'hello world',",
+        "  is_executable = False)");
+    FileWriteAction action =
+        (FileWriteAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(Iterables.getOnlyElement(action.getOutputs()).getExecPathString())
+        .isEqualTo("foo/a.txt");
+    assertThat(action.getFileContents()).isEqualTo("hello world");
+    assertThat(action.makeExecutable()).isFalse();
+  }
+
+  @Test
+  public void testEmptyAction() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    checkEmptyAction("mnemonic = 'test'");
+    checkEmptyAction("mnemonic = 'test', inputs = ruleContext.files.srcs");
+    checkEmptyAction("mnemonic = 'test', inputs = depset(ruleContext.files.srcs)");
+
+    ev.checkEvalErrorContains(
+        "do_nothing() missing 1 required named argument: mnemonic",
+        "ruleContext.actions.do_nothing(inputs = ruleContext.files.srcs)");
+  }
+
+  private void checkEmptyAction(String namedArgs) throws Exception {
+    assertThat(ev.eval(String.format("ruleContext.actions.do_nothing(%s)", namedArgs)))
+        .isEqualTo(Starlark.NONE);
+  }
+
+  @Test
+  public void testEmptyActionWithExtraAction() throws Exception {
+    scratch.file(
+        "test/empty.bzl",
+        "def _impl(ctx):",
+        "  ctx.actions.do_nothing(",
+        "      inputs = ctx.files.srcs,",
+        "      mnemonic = 'EA',",
+        "  )",
+        "empty_action_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+
+    scratch.file(
+        "test/BUILD",
+        "load('//test:empty.bzl', 'empty_action_rule')",
+        "empty_action_rule(name = 'my_empty_action',",
+        "                srcs = ['foo.in', 'other_foo.in'])",
+        "action_listener(name = 'listener',",
+        "                mnemonics = ['EA'],",
+        "                extra_actions = [':extra'])",
+        "extra_action(name = 'extra',",
+        "             cmd='')");
+
+    getPseudoActionViaExtraAction("//test:my_empty_action", "//test:listener");
+  }
+
+  @Test
+  public void testExpandLocation() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:bar");
+    setRuleContext(ruleContext);
+
+    // If there is only a single target, both "location" and "locations" should work
+    runExpansion("location :jl", "[blaze]*-out/.*/bin/foo/libjl.jar");
+    runExpansion("locations :jl", "[blaze]*-out/.*/bin/foo/libjl.jar");
+
+    runExpansion("location //foo:jl", "[blaze]*-out/.*/bin/foo/libjl.jar");
+
+    // Multiple targets and "location" should result in an error
+    checkReportedErrorStartsWith(
+        "in genrule rule //foo:bar: label '//foo:gl' "
+            + "in $(location) expression expands to more than one file, please use $(locations "
+            + "//foo:gl) instead.",
+        "ruleContext.expand_location('$(location :gl)')");
+
+    // We have to use "locations" for multiple targets
+    runExpansion(
+        "locations :gl",
+        "[blaze]*-out/.*/bin/foo/gl.a [blaze]*-out/.*/bin/foo/gl.gcgox");
+
+    // LocationExpander just returns the input string if there is no label
+    runExpansion("location", "\\$\\(location\\)");
+
+    checkReportedErrorStartsWith(
+        "in genrule rule //foo:bar: label '//foo:abc' in $(locations) expression "
+            + "is not a declared prerequisite of this rule",
+        "ruleContext.expand_location('$(locations :abc)')");
+  }
+
+  /** Regression test to check that expand_location allows ${var} and $$. */
+  @Test
+  public void testExpandLocationWithDollarSignsAndCurlys() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:bar");
+    setRuleContext(ruleContext);
+    assertThat((String) ev.eval("ruleContext.expand_location('${abc} $(echo) $$ $')"))
+        .isEqualTo("${abc} $(echo) $$ $");
+  }
+
+  /**
+   * Invokes ctx.expand_location() with the given parameters and checks whether this led to the
+   * expected result
+   *
+   * @param command Either "location" or "locations". This only matters when the label has multiple
+   *     targets
+   * @param expectedPattern Regex pattern that matches the expected result
+   */
+  private void runExpansion(String command, String expectedPattern) throws Exception {
+    assertMatches(
+        "Expanded string",
+        expectedPattern,
+        (String) ev.eval(String.format("ruleContext.expand_location('$(%s)')", command)));
+  }
+
+  private void assertMatches(String description, String expectedPattern, String computedValue)
+      throws Exception {
+    assertWithMessage(
+            Starlark.format(
+                "%s %r did not match pattern '%s'", description, computedValue, expectedPattern))
+        .that(Pattern.matches(expectedPattern, computedValue))
+        .isTrue();
+  }
+
+  @Test
+  public void testResolveCommandMakeVariables() throws Exception {
+    setRuleContext(createRuleContext("//foo:resolve_me"));
+    ev.exec(
+        "inputs, argv, manifests = ruleContext.resolve_command(",
+        "  command='I got the $(HELLO) on a $(DAVE)', ",
+        "  make_variables={'HELLO': 'World', 'DAVE': type('')})");
+    @SuppressWarnings("unchecked")
+    List<String> argv = (List<String>) (List<?>) (StarlarkList) ev.lookup("argv");
+    assertThat(argv).hasSize(3);
+    assertMatches("argv[0]", "^.*/bash" + OsUtils.executableExtension() + "$", argv.get(0));
+    assertThat(argv.get(1)).isEqualTo("-c");
+    assertThat(argv.get(2)).isEqualTo("I got the World on a string");
+  }
+
+  @Test
+  public void testResolveCommandInputs() throws Exception {
+    setRuleContext(createRuleContext("//foo:resolve_me"));
+    ev.exec(
+        "inputs, argv, input_manifests = ruleContext.resolve_command(",
+        "   tools=ruleContext.attr.tools)");
+    @SuppressWarnings("unchecked")
+    List<Artifact> inputs = (List<Artifact>) (List<?>) (StarlarkList) ev.lookup("inputs");
+    assertArtifactFilenames(
+        inputs,
+        "mytool.sh",
+        "mytool",
+        "foo_Smytool" + OsUtils.executableExtension() + "-runfiles",
+        "t.exe");
+    @SuppressWarnings("unchecked")
+    RunfilesSupplier runfilesSupplier =
+        CompositeRunfilesSupplier.fromSuppliers(
+            (List<RunfilesSupplier>) ev.lookup("input_manifests"));
+    assertThat(runfilesSupplier.getMappings()).hasSize(1);
+  }
+
+  @Test
+  public void testResolveCommandExpandLocations() throws Exception {
+    setRuleContext(createRuleContext("//foo:resolve_me"));
+    ev.exec(
+        "def foo():", // no for loops at top-level
+        "  label_dict = {}",
+        "  all = []",
+        "  for dep in ruleContext.attr.srcs + ruleContext.attr.tools:",
+        "    all.extend(dep.files.to_list())",
+        "    label_dict[dep.label] = dep.files.to_list()",
+        "  return ruleContext.resolve_command(",
+        "    command='A$(locations //foo:mytool) B$(location //foo:file3.dat)',",
+        "    attribute='cmd', expand_locations=True, label_dict=label_dict)",
+        "inputs, argv, manifests = foo()");
+    @SuppressWarnings("unchecked")
+    List<String> argv = (List<String>) (List<?>) (StarlarkList) ev.lookup("argv");
+    assertThat(argv).hasSize(3);
+    assertMatches("argv[0]", "^.*/bash" + OsUtils.executableExtension() + "$", argv.get(0));
+    assertThat(argv.get(1)).isEqualTo("-c");
+    assertMatches("argv[2]", "A.*/mytool .*/mytool.sh B.*file3.dat", argv.get(2));
+  }
+
+  @Test
+  public void testResolveCommandExecutionRequirements() throws Exception {
+    // Tests that requires-darwin execution requirements result in the usage of /bin/bash.
+    setRuleContext(createRuleContext("//foo:resolve_me"));
+    ev.exec(
+        "inputs, argv, manifests = ruleContext.resolve_command(",
+        "  execution_requirements={'requires-darwin': ''})");
+    @SuppressWarnings("unchecked")
+    List<String> argv = (List<String>) (List<?>) (StarlarkList) ev.lookup("argv");
+    assertMatches("argv[0]", "^/bin/bash$", argv.get(0));
+  }
+
+  @Test
+  public void testResolveCommandScript() throws Exception {
+    setRuleContext(createRuleContext("//foo:resolve_me"));
+    ev.exec(
+        "def foo():", // no for loops at top-level
+        "  s = 'a'",
+        "  for i in range(1,17): s = s + s", // 2**17 > CommandHelper.maxCommandLength (=64000)
+        "  return ruleContext.resolve_command(",
+        "    command=s)",
+        "argv = foo()[1]");
+    @SuppressWarnings("unchecked")
+    List<String> argv = (List<String>) (List<?>) (StarlarkList) ev.lookup("argv");
+    assertThat(argv).hasSize(2);
+    assertMatches("argv[0]", "^.*/bash" + OsUtils.executableExtension() + "$", argv.get(0));
+    assertMatches("argv[1]", "^.*/resolve_me[.][a-z0-9]+[.]script[.]sh$", argv.get(1));
+  }
+
+  @Test
+  public void testResolveTools() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:resolve_me");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "inputs, input_manifests = ruleContext.resolve_tools(tools=ruleContext.attr.tools)",
+        "ruleContext.actions.run(",
+        "    outputs = [ruleContext.actions.declare_file('x.out')],",
+        "    inputs = inputs,",
+        "    input_manifests = input_manifests,",
+        "    executable = 'dummy',",
+        ")");
+    assertArtifactFilenames(
+        ((Depset) ev.lookup("inputs")).getSet(Artifact.class).toList(),
+        "mytool.sh",
+        "mytool",
+        "foo_Smytool" + OsUtils.executableExtension() + "-runfiles",
+        "t.exe");
+    @SuppressWarnings("unchecked")
+    RunfilesSupplier runfilesSupplier =
+        CompositeRunfilesSupplier.fromSuppliers(
+            (List<RunfilesSupplier>) ev.lookup("input_manifests"));
+    assertThat(runfilesSupplier.getMappings()).hasSize(1);
+
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(ActionsTestUtil.baseArtifactNames(action.getInputs()))
+        .containsAtLeast(
+            "mytool.sh",
+            "mytool",
+            "foo_Smytool" + OsUtils.executableExtension() + "-runfiles",
+            "t.exe");
+  }
+
+  @Test
+  public void testBadParamTypeErrorMessage() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "got value of type 'int', want 'string or Args'",
+        "ruleContext.actions.write(",
+        "  output = ruleContext.files.srcs[0],",
+        "  content = 1,",
+        "  is_executable = False)");
+  }
+
+  @Test
+  public void testCreateTemplateAction() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.expand_template(",
+        "  template = ruleContext.files.srcs[0],",
+        "  output = ruleContext.files.srcs[1],",
+        "  substitutions = {'a': 'b'},",
+        "  is_executable = False)");
+
+    TemplateExpansionAction action = (TemplateExpansionAction) Iterables.getOnlyElement(
+        ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getInputs().getSingleton().getExecPathString()).isEqualTo("foo/a.txt");
+    assertThat(Iterables.getOnlyElement(action.getOutputs()).getExecPathString())
+        .isEqualTo("foo/b.img");
+    assertThat(Iterables.getOnlyElement(action.getSubstitutions()).getKey()).isEqualTo("a");
+    assertThat(Iterables.getOnlyElement(action.getSubstitutions()).getValue()).isEqualTo("b");
+    assertThat(action.makeExecutable()).isFalse();
+  }
+
+  /**
+   * Simulates the fact that the Parser currently uses Latin1 to read BUILD files, while users
+   * usually write those files using UTF-8 encoding. Currently, the string-valued 'substitutions'
+   * parameter of the template_action function contains a hack that assumes its input is a UTF-8
+   * encoded string which has been ingested as Latin 1. The hack converts the string to its
+   * "correct" UTF-8 value. Once Blaze starts calling {@link
+   * com.google.devtools.build.lib.syntax.ParserInput#fromUTF8} instead of {@code fromLatin1} and
+   * the hack for the substituations parameter is removed, this test will fail.
+   */
+  @Test
+  public void testCreateTemplateActionWithWrongEncoding() throws Exception {
+    // The following array contains bytes that represent a string of length two when treated as
+    // UTF-8 and a string of length four when treated as ISO-8859-1 (a.k.a. Latin 1).
+    byte[] bytesToDecode = {(byte) 0xC2, (byte) 0xA2, (byte) 0xC2, (byte) 0xA2};
+    Charset latin1 = StandardCharsets.ISO_8859_1;
+    Charset utf8 = StandardCharsets.UTF_8;
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "ruleContext.actions.expand_template(",
+        "  template = ruleContext.files.srcs[0],",
+        "  output = ruleContext.files.srcs[1],",
+        "  substitutions = {'a': '" + new String(bytesToDecode, latin1) + "'},",
+        "  is_executable = False)");
+    TemplateExpansionAction action = (TemplateExpansionAction) Iterables.getOnlyElement(
+        ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    List<Substitution> substitutions = action.getSubstitutions();
+    assertThat(substitutions).hasSize(1);
+    assertThat(substitutions.get(0).getValue()).isEqualTo(new String(bytesToDecode, utf8));
+  }
+
+  @Test
+  public void testRunfilesAddFromDependencies() throws Exception {
+    setRuleContext(createRuleContext("//foo:bar"));
+    Object result = ev.eval("ruleContext.runfiles(collect_default = True)");
+    assertThat(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)))
+        .contains("libjl.jar");
+  }
+
+  @Test
+  public void testRunfilesBadListGenericType() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "at index 0 of files, got element of type string, want File",
+        "ruleContext.runfiles(files = ['some string'])");
+  }
+
+  @Test
+  public void testRunfilesBadSetGenericType() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "got a depset of 'int', expected a depset of 'File'",
+        "ruleContext.runfiles(transitive_files=depset([1, 2, 3]))");
+  }
+
+  @Test
+  public void testRunfilesBadMapGenericType() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "got dict<int, File> for 'symlinks', want dict<string, File>",
+        "ruleContext.runfiles(symlinks = {123: ruleContext.files.srcs[0]})");
+    ev.checkEvalErrorContains(
+        "got dict<string, int> for 'symlinks', want dict<string, File>",
+        "ruleContext.runfiles(symlinks = {'some string': 123})");
+    ev.checkEvalErrorContains(
+        "got dict<int, File> for 'root_symlinks', want dict<string, File>",
+        "ruleContext.runfiles(root_symlinks = {123: ruleContext.files.srcs[0]})");
+    ev.checkEvalErrorContains(
+        "got dict<string, int> for 'root_symlinks', want dict<string, File>",
+        "ruleContext.runfiles(root_symlinks = {'some string': 123})");
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromArtifact() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.runfiles(files = ruleContext.files.tools)");
+    assertThat(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result))).contains("t.exe");
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromIterableArtifacts() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.runfiles(files = ruleContext.files.srcs)");
+    assertThat(ImmutableList.of("a.txt", "b.img"))
+        .isEqualTo(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)));
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromNestedSetArtifacts() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result =
+        ev.eval("ruleContext.runfiles(transitive_files = depset(ruleContext.files.srcs))");
+    assertThat(ImmutableList.of("a.txt", "b.img"))
+        .isEqualTo(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)));
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromDefaultAndFiles() throws Exception {
+    setRuleContext(createRuleContext("//foo:bar"));
+    // It would be nice to write [DEFAULT] + ruleContext.files.srcs, but artifacts
+    // is an ImmutableList and Starlark interprets it as a tuple.
+    Object result =
+        ev.eval("ruleContext.runfiles(collect_default = True, files = ruleContext.files.srcs)");
+    // From DEFAULT only libjl.jar comes, see testRunfilesAddFromDependencies().
+    assertThat(ImmutableList.of("libjl.jar", "gl.a", "gl.gcgox"))
+        .isEqualTo(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)));
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromSymlink() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.runfiles(symlinks = {'sym1': ruleContext.files.srcs[0]})");
+    assertThat(ImmutableList.of("a.txt"))
+        .isEqualTo(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)));
+  }
+
+  @Test
+  public void testRunfilesArtifactsFromRootSymlink() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result =
+        ev.eval("ruleContext.runfiles(root_symlinks = {'sym1': ruleContext.files.srcs[0]})");
+    assertThat(ImmutableList.of("a.txt"))
+        .isEqualTo(ActionsTestUtil.baseArtifactNames(getRunfileArtifacts(result)));
+  }
+
+  @Test
+  public void testRunfilesSymlinkConflict() throws Exception {
+    // Two different artifacts mapped to same path in runfiles
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.exec("prefix = ruleContext.workspace_name + '/' if ruleContext.workspace_name else ''");
+    Object result =
+        ev.eval(
+            "ruleContext.runfiles(",
+            "  root_symlinks = {prefix + 'sym1': ruleContext.files.srcs[0]},",
+            "  symlinks = {'sym1': ruleContext.files.srcs[1]})");
+    Runfiles runfiles = (Runfiles) result;
+    reporter.removeHandler(failFastHandler); // So it doesn't throw an exception.
+    runfiles.getRunfilesInputs(reporter, null);
+    assertContainsEvent("ERROR <no location>: overwrote runfile");
+  }
+
+  private static Iterable<Artifact> getRunfileArtifacts(Object runfiles) {
+    return ((Runfiles) runfiles).getAllArtifacts().toList();
+  }
+
+  @Test
+  public void testRunfilesBadKeywordArguments() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "runfiles() got unexpected keyword argument 'bad_keyword'",
+        "ruleContext.runfiles(bad_keyword = '')");
+  }
+
+  @Test
+  public void testNsetContainsList() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "depset elements must not be mutable values", "depset([[ruleContext.files.srcs]])");
+  }
+
+  @Test
+  public void testCmdJoinPaths() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("cmd_helper.join_paths(':', depset(ruleContext.files.srcs))");
+    assertThat(result).isEqualTo("foo/a.txt:foo/b.img");
+  }
+
+  @Test
+  public void testStructPlusArtifactErrorMessage() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "unsupported binary operation: File + struct",
+        "ruleContext.files.tools[0] + struct(a = 1)");
+  }
+
+  @Test
+  public void testNoSuchProviderErrorMessage() throws Exception {
+    setRuleContext(createRuleContext("//foo:bar"));
+    ev.checkEvalErrorContains(
+        "<target //foo:jl> (rule 'java_library') doesn't have provider 'my_provider'",
+        "ruleContext.attr.srcs[0].my_provider");
+  }
+
+  @Test
+  public void testFilesForRuleConfiguredTarget() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    Object result = ev.eval("ruleContext.attr.srcs[0].files");
+    assertThat(ActionsTestUtil.baseNamesOf(((Depset) result).getSet(Artifact.class)))
+        .isEqualTo("a.txt");
+  }
+
+  @Test
+  public void testDefaultProvider() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    default = DefaultInfo(",
+        "        runfiles=ctx.runfiles(ctx.files.runs),",
+        "    )",
+        "    foo = foo_provider()",
+        "    return [foo, default]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'runs': attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    provider = ctx.attr.deps[0][DefaultInfo]",
+        "    return [MyInfo(",
+        "        is_provided = DefaultInfo in ctx.attr.deps[0],",
+        "        provider = provider,",
+        "        dir = str(sorted(dir(provider))),",
+        "        rule_data_runfiles = provider.data_runfiles,",
+        "        rule_default_runfiles = provider.default_runfiles,",
+        "        rule_files = provider.files,",
+        "        rule_files_to_run = provider.files_to_run,",
+        "        rule_file_executable = provider.files_to_run.executable",
+        "    )]",
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule', runs = ['run.file', 'run2.file'])",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule', 'file.txt'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    StructImpl myInfo = getMyInfoFromTarget(configuredTarget);
+    assertThat((Boolean) myInfo.getValue("is_provided")).isTrue();
+
+    Object provider = myInfo.getValue("provider");
+    assertThat(provider).isInstanceOf(DefaultInfo.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(DefaultInfo.PROVIDER.getKey());
+
+    assertThat(myInfo.getValue("dir"))
+        .isEqualTo(
+            "[\"data_runfiles\", \"default_runfiles\", \"files\", \"files_to_run\", \"to_json\", "
+                + "\"to_proto\"]");
+
+    assertThat(myInfo.getValue("rule_data_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("rule_data_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .containsExactly(
+            "File:[/workspace[source]]test/run.file", "File:[/workspace[source]]test/run2.file");
+
+    assertThat(myInfo.getValue("rule_default_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("rule_default_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .containsExactly(
+            "File:[/workspace[source]]test/run.file", "File:[/workspace[source]]test/run2.file");
+
+    assertThat(myInfo.getValue("rule_files")).isInstanceOf(Depset.class);
+    assertThat(myInfo.getValue("rule_files_to_run")).isInstanceOf(FilesToRunProvider.class);
+    assertThat(myInfo.getValue("rule_file_executable")).isEqualTo(Starlark.NONE);
+  }
+
+  @Test
+  public void testDefaultProviderInStruct() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    default = DefaultInfo(",
+        "        runfiles=ctx.runfiles(ctx.files.runs),",
+        "    )",
+        "    foo = foo_provider()",
+        "    return [foo, default]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'runs': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    provider = ctx.attr.deps[0][DefaultInfo]",
+        "    return [MyInfo(",
+        "        is_provided = DefaultInfo in ctx.attr.deps[0],",
+        "        provider = provider,",
+        "        dir = str(sorted(dir(provider))),",
+        "        rule_data_runfiles = provider.data_runfiles,",
+        "        rule_default_runfiles = provider.default_runfiles,",
+        "        rule_files = provider.files,",
+        "        rule_files_to_run = provider.files_to_run,",
+        "    )]",
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule', runs = ['run.file', 'run2.file'])",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule', 'file.txt'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    StructImpl myInfo = getMyInfoFromTarget(configuredTarget);
+
+    assertThat((Boolean) myInfo.getValue("is_provided")).isTrue();
+
+    Object provider = myInfo.getValue("provider");
+    assertThat(provider).isInstanceOf(DefaultInfo.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(DefaultInfo.PROVIDER.getKey());
+
+    assertThat(myInfo.getValue("dir"))
+        .isEqualTo(
+            "[\"data_runfiles\", \"default_runfiles\", \"files\", \"files_to_run\", \"to_json\", "
+                + "\"to_proto\"]");
+
+    assertThat(myInfo.getValue("rule_data_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("rule_data_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .containsExactly(
+            "File:[/workspace[source]]test/run.file", "File:[/workspace[source]]test/run2.file");
+
+    assertThat(myInfo.getValue("rule_default_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("rule_default_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .containsExactly(
+            "File:[/workspace[source]]test/run.file", "File:[/workspace[source]]test/run2.file");
+
+    assertThat(myInfo.getValue("rule_files")).isInstanceOf(Depset.class);
+    assertThat(myInfo.getValue("rule_files_to_run")).isInstanceOf(FilesToRunProvider.class);
+  }
+
+  @Test
+  public void testDefaultProviderInvalidConfiguration() throws Exception {
+    setStarlarkSemanticsOptions("--incompatible_disallow_struct_provider_syntax=false");
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    default = DefaultInfo(",
+        "        runfiles=ctx.runfiles(ctx.files.runs),",
+        "    )",
+        "    foo = foo_provider()",
+        "    return struct(providers=[foo, default], files=depset([]))",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'runs': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "foo_rule(name = 'my_rule', runs = ['run.file', 'run2.file'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "Provider 'files' should be specified in DefaultInfo "
+                + "if it's provided explicitly.");
+  }
+
+  @Test
+  public void testDefaultProviderOnFileTarget() throws Exception {
+    scratch.file(
+        "test/bar.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    provider = ctx.attr.deps[0][DefaultInfo]",
+        "    return [MyInfo(",
+        "        is_provided = DefaultInfo in ctx.attr.deps[0],",
+        "        provider = provider,",
+        "        dir = str(sorted(dir(provider))),",
+        "        file_data_runfiles = provider.data_runfiles,",
+        "        file_default_runfiles = provider.default_runfiles,",
+        "        file_files = provider.files,",
+        "        file_files_to_run = provider.files_to_run,",
+        "    )]",
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':bar.bzl', 'bar_rule')",
+        "bar_rule(name = 'my_rule', deps = ['file.txt'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    StructImpl myInfo = getMyInfoFromTarget(configuredTarget);
+
+    assertThat((Boolean) myInfo.getValue("is_provided")).isTrue();
+
+    Object provider = myInfo.getValue("provider");
+    assertThat(provider).isInstanceOf(DefaultInfo.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(DefaultInfo.PROVIDER.getKey());
+
+    assertThat(myInfo.getValue("dir"))
+        .isEqualTo(
+            "[\"data_runfiles\", \"default_runfiles\", \"files\", \"files_to_run\", \"to_json\", "
+                + "\"to_proto\"]");
+
+    assertThat(myInfo.getValue("file_data_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("file_data_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .isEmpty();
+
+    assertThat(myInfo.getValue("file_default_runfiles")).isInstanceOf(Runfiles.class);
+    assertThat(
+            Iterables.transform(
+                ((Runfiles) myInfo.getValue("file_default_runfiles")).getAllArtifacts().toList(),
+                String::valueOf))
+        .isEmpty();
+
+    assertThat(myInfo.getValue("file_files")).isInstanceOf(Depset.class);
+    assertThat(myInfo.getValue("file_files_to_run")).isInstanceOf(FilesToRunProvider.class);
+  }
+
+  @Test
+  public void testDefaultProviderProvidedImplicitly() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep[DefaultInfo]", // The goal is to test this object
+        "    return [MyInfo(", // so we return it here
+        "        default = provider,",
+        "    )]",
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    Object provider = getMyInfoFromTarget(configuredTarget).getValue("default");
+    assertThat(provider).isInstanceOf(DefaultInfo.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(DefaultInfo.PROVIDER.getKey());
+  }
+
+  @Test
+  public void testDefaultProviderUnknownFields() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    default = DefaultInfo(",
+        "        foo=ctx.runfiles(),",
+        "    )",
+        "    return [default]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "foo_rule(name = 'my_rule')"
+    );
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains("DefaultInfo() got unexpected keyword argument 'foo'");
+  }
+
+  @Test
+  public void testDeclaredProviders() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "foobar_provider = provider()",
+        "def _impl(ctx):",
+        "    foo = foo_provider()",
+        "    foobar = foobar_provider()",
+        "    return [foo, foobar]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep[foo_provider]", // The goal is to test this object
+        "    return [MyInfo(proxy = provider)]", // so we return it here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    Object provider = getMyInfoFromTarget(configuredTarget).getValue("proxy");
+    assertThat(provider).isInstanceOf(StructImpl.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(
+            new StarlarkProvider.Key(
+                Label.parseAbsolute("//test:foo.bzl", ImmutableMap.of()), "foo_provider"));
+  }
+
+  @Test
+  public void testAdvertisedProviders() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "FooInfo = provider()",
+        "BarInfo = provider()",
+        "def _impl(ctx):",
+        "    foo = FooInfo()",
+        "    bar = BarInfo()",
+        "    return [foo, bar]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    provides = [FooInfo, BarInfo]",
+        ")");
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'FooInfo')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    proxy = dep[FooInfo]", // The goal is to test this object
+        "    return [MyInfo(proxy = proxy)]", // so we return it here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    Object provider = getMyInfoFromTarget(configuredTarget).getValue("proxy");
+    assertThat(provider).isInstanceOf(StructImpl.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(
+            new StarlarkProvider.Key(
+                Label.parseAbsolute("//test:foo.bzl", ImmutableMap.of()), "FooInfo"));
+  }
+
+  @Test
+  public void testLacksAdvertisedDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "FooInfo = provider()",
+        "def _impl(ctx):",
+        "    default = DefaultInfo(",
+        "        runfiles=ctx.runfiles(ctx.files.runs),",
+        "    )",
+        "    return [default]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'runs': attr.label_list(allow_files=True),",
+        "    },",
+        "    provides = [FooInfo, DefaultInfo]",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "foo_rule(name = 'my_rule', runs = ['run.file', 'run2.file'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains("rule advertised the 'FooInfo' provider, "
+            + "but this provider was not among those returned");
+  }
+
+  @Test
+  public void testLacksAdvertisedNativeProvider() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "FooInfo = provider()",
+        "def _impl(ctx):",
+        "    MyFooInfo = FooInfo()",
+        "    return [MyFooInfo]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    provides = [FooInfo, JavaInfo]",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "foo_rule(name = 'my_rule')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains("rule advertised the 'JavaInfo' provider, "
+            + "but this provider was not among those returned");
+  }
+
+  @Test
+  public void testBadlySpecifiedProvides() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "def _impl(ctx):",
+        "    return []",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    provides = [1]",
+        ")");
+    scratch.file("test/BUILD", "load(':foo.bzl', 'foo_rule')", "foo_rule(name = 'my_rule')");
+
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "element in 'provides' is of unexpected type. "
+                + "Should be list of providers, but got item of type int");
+  }
+
+  @Test
+  public void testSingleDeclaredProvider() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    return foo_provider(a=123)",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep[foo_provider]", // The goal is to test this object
+        "    return [MyInfo(proxy = provider)]", // so we return it here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    Object provider = getMyInfoFromTarget(configuredTarget).getValue("proxy");
+    assertThat(provider).isInstanceOf(StructImpl.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(
+            new StarlarkProvider.Key(
+                Label.parseAbsolute("//test:foo.bzl", ImmutableMap.of()), "foo_provider"));
+    assertThat(((StructImpl) provider).getValue("a")).isEqualTo(123);
+  }
+
+  @Test
+  public void testDeclaredProvidersAliasTarget() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "foobar_provider = provider()",
+        "def _impl(ctx):",
+        "    foo = foo_provider()",
+        "    foobar = foobar_provider()",
+        "    return [foo, foobar]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'foo_provider')",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep[foo_provider]", // The goal is to test this object
+        "    return [MyInfo(proxy = provider)]", // so we return it here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'foo_rule')",
+        "alias(name = 'dep_rule', actual=':foo_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    Object provider = getMyInfoFromTarget(configuredTarget).getValue("proxy");
+    assertThat(provider).isInstanceOf(StructImpl.class);
+    assertThat(((StructImpl) provider).getProvider().getKey())
+        .isEqualTo(
+            new StarlarkProvider.Key(
+                Label.parseAbsolute("//test:foo.bzl", ImmutableMap.of()), "foo_provider"));
+  }
+
+  @Test
+  public void testDeclaredProvidersWrongKey() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "unused_provider = provider()",
+        "def _impl(ctx):",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "load(':foo.bzl', 'unused_provider')",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep[unused_provider]",  // Should throw an error here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "<target //test:dep_rule> (rule 'foo_rule') doesn't contain "
+                + "declared provider 'unused_provider'");
+  }
+
+  @Test
+  public void testDeclaredProvidersInvalidKey() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "def _impl(ctx):",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "foo_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/bar.bzl",
+        "def _impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    provider = dep['foo_provider']",  // Should throw an error here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "       'deps': attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'foo_rule')",
+        "load(':bar.bzl', 'bar_rule')",
+        "foo_rule(name = 'dep_rule')",
+        "bar_rule(name = 'my_rule', deps = [':dep_rule'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains("Type Target only supports indexing by object constructors, got string instead");
+  }
+
+  @Test
+  public void testDeclaredProvidersFileTarget() throws Exception {
+    scratch.file(
+        "test/bar.bzl",
+        "unused_provider = provider()",
+        "def _impl(ctx):",
+        "    src = ctx.attr.srcs[0]",
+        "    provider = src[unused_provider]",  // Should throw an error here
+        "bar_rule = rule(",
+        "    implementation = _impl,",
+        "    attrs = {",
+        "       'srcs': attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':bar.bzl', 'bar_rule')",
+        "bar_rule(name = 'my_rule', srcs = ['input.txt'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "<input file target //test:input.txt> doesn't contain "
+                + "declared provider 'unused_provider'");
+  }
+
+  @Test
+  public void testDeclaredProvidersInOperator() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "foo_provider = provider()",
+        "bar_provider = provider()",
+        "",
+        "def _inner_impl(ctx):",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "inner_rule = rule(",
+        "    implementation = _inner_impl,",
+        ")",
+        "",
+        "def _outer_impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    return [MyInfo(",
+        "        foo = (foo_provider in dep),", // Should be true
+        "        bar = (bar_provider in dep),", // Should be false
+        "    )]",
+        "outer_rule = rule(",
+        "    implementation = _outer_impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(),",
+        "    }",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'inner_rule', 'outer_rule')",
+        "inner_rule(name = 'dep_rule')",
+        "outer_rule(name = 'my_rule', deps = [':dep_rule'])");
+
+    ConfiguredTarget configuredTarget = getConfiguredTarget("//test:my_rule");
+    StructImpl myInfo = getMyInfoFromTarget(configuredTarget);
+
+    Object foo = myInfo.getValue("foo");
+    assertThat(foo).isInstanceOf(Boolean.class);
+    assertThat((Boolean) foo).isTrue();
+    Object bar = myInfo.getValue("bar");
+    assertThat(bar).isInstanceOf(Boolean.class);
+    assertThat((Boolean) bar).isFalse();
+  }
+
+  @Test
+  public void testDeclaredProvidersInOperatorInvalidKey() throws Exception {
+    scratch.file(
+        "test/foo.bzl",
+        "foo_provider = provider()",
+        "bar_provider = provider()",
+        "",
+        "def _inner_impl(ctx):",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "inner_rule = rule(",
+        "    implementation = _inner_impl,",
+        ")",
+        "",
+        "def _outer_impl(ctx):",
+        "    dep = ctx.attr.deps[0]",
+        "    'foo_provider' in dep",  // Should throw an error here
+        "outer_rule = rule(",
+        "    implementation = _outer_impl,",
+        "    attrs = {",
+        "       'deps': attr.label_list(),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load(':foo.bzl', 'inner_rule', 'outer_rule')",
+        "inner_rule(name = 'dep_rule')",
+        "outer_rule(name = 'my_rule', deps = [':dep_rule'])");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains("Type Target only supports querying by object constructors, got string instead");
+  }
+
+  @Test
+  public void testReturnNonExportedProvider() throws Exception {
+    scratch.file(
+        "test/my_rule.bzl",
+        "def _rule_impl(ctx):",
+        "    foo_provider = provider()",
+        "    foo = foo_provider()",
+        "    return [foo]",
+        "",
+        "my_rule = rule(",
+        "    implementation = _rule_impl,",
+        ")");
+    scratch.file("test/BUILD", "load(':my_rule.bzl', 'my_rule')", "my_rule(name = 'my_rule')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:my_rule"));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "cannot return a non-exported provider instance from a rule implementation function.");
+  }
+
+  @Test
+  public void testFilesForFileConfiguredTarget() throws Exception {
+    setRuleContext(createRuleContext("//foo:bar"));
+    Object result = ev.eval("ruleContext.attr.srcs[0].files");
+    assertThat(ActionsTestUtil.baseNamesOf(((Depset) result).getSet(Artifact.class)))
+        .isEqualTo("libjl.jar");
+  }
+
+  @Test
+  public void testCtxStructFieldsCustomErrorMessages() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains("No attribute 'foo' in attr.", "ruleContext.attr.foo");
+    ev.checkEvalErrorContains("No attribute 'foo' in outputs.", "ruleContext.outputs.foo");
+    ev.checkEvalErrorContains("No attribute 'foo' in files.", "ruleContext.files.foo");
+    ev.checkEvalErrorContains("No attribute 'foo' in file.", "ruleContext.file.foo");
+    ev.checkEvalErrorContains("No attribute 'foo' in executable.", "ruleContext.executable.foo");
+  }
+
+  @Test
+  public void testBinDirPath() throws Exception {
+    StarlarkRuleContext ctx = createRuleContext("//foo:bar");
+    setRuleContext(ctx);
+    Object result = ev.eval("ruleContext.bin_dir.path");
+    assertThat(result).isEqualTo(ctx.getConfiguration().getBinFragment().getPathString());
+  }
+
+  @Test
+  public void testEmptyLabelListTypeAttrInCtx() throws Exception {
+    setRuleContext(createRuleContext("//foo:baz"));
+    Object result = ev.eval("ruleContext.attr.srcs");
+    assertThat(result).isEqualTo(StarlarkList.empty());
+  }
+
+  @Test
+  public void testDefinedMakeVariable() throws Exception {
+    useConfiguration("--define=FOO=bar");
+    setRuleContext(createRuleContext("//foo:baz"));
+    String foo = (String) ev.eval("ruleContext.var['FOO']");
+    assertThat(foo).isEqualTo("bar");
+  }
+
+  @Test
+  public void testCodeCoverageConfigurationAccess() throws Exception {
+    StarlarkRuleContext ctx = createRuleContext("//foo:baz");
+    setRuleContext(ctx);
+    boolean coverage = (Boolean) ev.eval("ruleContext.configuration.coverage_enabled");
+    assertThat(ctx.getRuleContext().getConfiguration().isCodeCoverageEnabled()).isEqualTo(coverage);
+  }
+
+  /** Checks whether the given (invalid) statement leads to the expected error */
+  private void checkReportedErrorStartsWith(String errorMsg, String... statements)
+      throws Exception {
+    // If the component under test relies on Reporter and EventCollector for error handling, any
+    // error would lead to an asynchronous AssertionFailedError thanks to failFastHandler in
+    // FoundationTestCase.
+    //
+    // Consequently, we disable failFastHandler and check all events for the expected error message
+    reporter.removeHandler(failFastHandler);
+
+    Object result = ev.eval(statements);
+
+    String first = null;
+    int count = 0;
+
+    try {
+      for (Event evt : eventCollector) {
+        if (evt.getMessage().startsWith(errorMsg)) {
+          return;
+        }
+
+        ++count;
+        first = evt.getMessage();
+      }
+
+      if (count == 0) {
+        fail(
+            String.format(
+                "checkReportedErrorStartsWith(): There was no error; the result is '%s'", result));
+      } else {
+        fail(
+            String.format(
+                "Found %d error(s), but none with the expected message '%s'. First error: '%s'",
+                count, errorMsg, first));
+      }
+    } finally {
+      eventCollector.clear();
+    }
+  }
+
+  @StarlarkMethod(name = "throw1", documented = false)
+  public Object throw1() throws Exception {
+    class ThereIsNoMessageException extends EvalException {
+      ThereIsNoMessageException() {
+        super(null, "This is not the message you are looking for."); // Unused dummy message
+      }
+
+      @Override
+      public String getMessage() {
+        return "";
+      }
+    }
+    throw new ThereIsNoMessageException();
+  }
+
+  @Test
+  public void testStackTraceWithoutOriginalMessage() throws Exception {
+    defineTestMethods();
+    ev.checkEvalErrorContains(
+        "There Is No Message: StarlarkRuleImplementationFunctionsTest", "throw1()");
+  }
+
+  @StarlarkMethod(name = "throw2", documented = false)
+  public Object throw2() throws Exception {
+    throw new InterruptedException();
+  }
+
+  @Test
+  public void testNoStackTraceOnInterrupt() throws Exception {
+    defineTestMethods();
+    assertThrows(InterruptedException.class, () -> ev.eval("throw2()"));
+  }
+
+  @Test
+  public void testGlobInImplicitOutputs() throws Exception {
+    scratch.file(
+        "test/glob.bzl",
+        "def _impl(ctx):",
+        "  ctx.actions.do_nothing(",
+        "    inputs = [],",
+        "  )",
+        "def _foo():",
+        "  return native.glob(['*'])",
+        "glob_rule = rule(",
+        "  implementation = _impl,",
+        "  outputs = _foo,",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:glob.bzl', 'glob_rule')",
+        "glob_rule(name = 'my_glob',",
+        "  srcs = ['foo.bar', 'other_foo.bar'])");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:my_glob");
+    assertContainsEvent("The native module can be accessed only from a BUILD thread.");
+  }
+
+  @Test
+  public void testRuleFromBzlFile() throws Exception {
+    scratch.file("test/rule.bzl", "def _impl(ctx): return", "foo = rule(implementation = _impl)");
+    scratch.file("test/ext.bzl", "load('//test:rule.bzl', 'foo')", "a = 1", "foo(name = 'x')");
+    scratch.file("test/BUILD", "load('//test:ext.bzl', 'a')");
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:x");
+    assertContainsEvent("Cannot instantiate a rule when loading a .bzl file");
+  }
+
+  @Test
+  public void testImplicitOutputsFromGlob() throws Exception {
+    scratch.file(
+        "test/glob.bzl",
+        "def _impl(ctx):",
+        "  outs = ctx.outputs",
+        "  for i in ctx.attr.srcs:",
+        "    o = getattr(outs, 'foo_' + i.label.name)",
+        "    ctx.actions.write(",
+        "      output = o,",
+        "      content = 'hoho')",
+        "",
+        "def _foo(srcs):",
+        "  outs = {}",
+        "  for i in srcs:",
+        "    outs['foo_' + i.name] = i.name + '.out'",
+        "  return outs",
+        "",
+        "glob_rule = rule(",
+        "    attrs = {",
+        "        'srcs': attr.label_list(allow_files = True),",
+        "    },",
+        "    outputs = _foo,",
+        "    implementation = _impl,",
+        ")");
+    scratch.file("test/a.bar", "a");
+    scratch.file("test/b.bar", "b");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:glob.bzl', 'glob_rule')",
+        "glob_rule(name = 'my_glob', srcs = glob(['*.bar']))");
+    ConfiguredTarget ct = getConfiguredTarget("//test:my_glob");
+    assertThat(ct).isNotNull();
+    assertThat(getGeneratingAction(getBinArtifact("a.bar.out", ct))).isNotNull();
+    assertThat(getGeneratingAction(getBinArtifact("b.bar.out", ct))).isNotNull();
+  }
+
+  @Test
+  public void testBuiltInFunctionAsRuleImplementation() throws Exception {
+    // Using built-in functions as rule implementations shouldn't cause runtime errors
+    scratch.file(
+        "test/rule.bzl",
+        "silly_rule = rule(",
+        "    implementation = int,",
+        "    attrs = {",
+        "       \"srcs\": attr.label_list(allow_files=True),",
+        "    }",
+        ")"
+    );
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rule.bzl', 'silly_rule')",
+        "silly_rule(name = 'silly')");
+    thrown.handleAssertionErrors(); // Compatibility with JUnit 4.11
+    thrown.expect(AssertionError.class);
+    // This confusing message shows why we should distinguish
+    // built-ins and Starlark functions in their repr strings.
+    thrown.expectMessage(
+        "in call to rule(), parameter 'implementation' got value of type 'function', want"
+            + " 'function'");
+    getConfiguredTarget("//test:silly");
+  }
+
+  @Test
+  public void testArgsScalarAdd() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "args.add('--foo')",
+        "args.add('-')",
+        "args.add('foo', format='format%s')",
+        "args.add('-')",
+        "args.add('--foo', 'val')",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly("foo/t.exe", "--foo", "-", "formatfoo", "-", "--foo", "val")
+        .inOrder();
+  }
+
+  @Test
+  public void testArgsScalarAddThrowsWithVectorArg() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "Args.add() doesn't accept vectorized arguments",
+        "args = ruleContext.actions.args()",
+        "args.add([1, 2])",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+  }
+
+  @Test
+  public void testArgsAddAll() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "args.add_all([1, 2])",
+        "args.add('-')",
+        "args.add_all('--foo', [1, 2])",
+        "args.add('-')",
+        "args.add_all([1, 2], before_each='-before')",
+        "args.add('-')",
+        "args.add_all([1, 2], format_each='format/%s')",
+        "args.add('-')",
+        "args.add_all(ruleContext.files.srcs)",
+        "args.add('-')",
+        "args.add_all(ruleContext.files.srcs, format_each='format/%s')",
+        "args.add('-')",
+        "args.add_all([1, 2], terminate_with='--terminator')",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly(
+            "foo/t.exe",
+            "1",
+            "2",
+            "-",
+            "--foo",
+            "1",
+            "2",
+            "-",
+            "-before",
+            "1",
+            "-before",
+            "2",
+            "-",
+            "format/1",
+            "format/2",
+            "-",
+            "foo/a.txt",
+            "foo/b.img",
+            "-",
+            "format/foo/a.txt",
+            "format/foo/b.img",
+            "-",
+            "1",
+            "2",
+            "--terminator")
+        .inOrder();
+  }
+
+  @Test
+  public void testArgsAddAllWithMapEach() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "def add_one(val): return str(val + 1)",
+        "def expand_to_many(val): return ['hey', 'hey']",
+        "args = ruleContext.actions.args()",
+        "args.add_all([1, 2], map_each=add_one)",
+        "args.add('-')",
+        "args.add_all([1, 2], map_each=expand_to_many)",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly("foo/t.exe", "2", "3", "-", "hey", "hey", "hey", "hey")
+        .inOrder();
+  }
+
+  @Test
+  public void testOmitIfEmpty() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "def add_one(val): return str(val + 1)",
+        "def filter(val): return None",
+        "args = ruleContext.actions.args()",
+        "args.add_joined([], join_with=',')",
+        "args.add('-')",
+        "args.add_joined([], join_with=',', omit_if_empty=False)",
+        "args.add('-')",
+        "args.add_all('--foo', [])",
+        "args.add('-')",
+        "args.add_all('--foo', [], omit_if_empty=False)",
+        "args.add('-')",
+        "args.add_all('--foo', [1], map_each=filter, terminate_with='hello')",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly(
+            "foo/t.exe",
+            // Nothing
+            "-",
+            "", // Empty string was joined and added
+            "-",
+            // Nothing
+            "-",
+            "--foo", // Arg added regardless
+            "-"
+            // Nothing, all values were filtered
+            )
+        .inOrder();
+  }
+
+  @Test
+  public void testUniquify() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "def add_one(val): return str(val + 1)",
+        "args = ruleContext.actions.args()",
+        "args.add_all(['a', 'b', 'a'])",
+        "args.add('-')",
+        "args.add_all(['a', 'b', 'a', 'c', 'b'], uniquify=True)",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly("foo/t.exe", "a", "b", "a", "-", "a", "b", "c")
+        .inOrder();
+  }
+
+  @Test
+  public void testArgsAddJoined() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "def add_one(val): return str(val + 1)",
+        "args = ruleContext.actions.args()",
+        "args.add_joined([1, 2], join_with=':')",
+        "args.add('-')",
+        "args.add_joined([1, 2], join_with=':', format_each='format/%s')",
+        "args.add('-')",
+        "args.add_joined([1, 2], join_with=':', format_each='format/%s', format_joined='--foo=%s')",
+        "args.add('-')",
+        "args.add_joined([1, 2], join_with=':', map_each=add_one)",
+        "args.add('-')",
+        "args.add_joined(ruleContext.files.srcs, join_with=':')",
+        "args.add('-')",
+        "args.add_joined(ruleContext.files.srcs, join_with=':', format_each='format/%s')",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly(
+            "foo/t.exe",
+            "1:2",
+            "-",
+            "format/1:format/2",
+            "-",
+            "--foo=format/1:format/2",
+            "-",
+            "2:3",
+            "-",
+            "foo/a.txt:foo/b.img",
+            "-",
+            "format/foo/a.txt:format/foo/b.img")
+        .inOrder();
+  }
+
+  @Test
+  public void testMultipleLazyArgsMixedWithStrings() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "foo_args = ruleContext.actions.args()",
+        "foo_args.add('--foo')",
+        "bar_args = ruleContext.actions.args()",
+        "bar_args.add('--bar')",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = ['hello', foo_args, 'world', bar_args, 'works'],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments())
+        .containsExactly("foo/t.exe", "hello", "--foo", "world", "--bar", "works")
+        .inOrder();
+  }
+
+  @Test
+  public void testLazyArgsWithParamFile() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  args = ctx.actions.args()",
+        "  args.add('--foo')",
+        "  args.use_param_file('--file=%s', use_always=True)",
+        "  output=ctx.actions.declare_file('out')",
+        "  ctx.actions.run_shell(",
+        "    inputs = [output],",
+        "    outputs = [output],",
+        "    arguments = [args],",
+        "    command = 'touch out',",
+        "  )",
+        "main_rule = rule(implementation = _impl)");
+    scratch.file(
+        "test/BUILD", "load('//test:main_rule.bzl', 'main_rule')", "main_rule(name='main')");
+    ConfiguredTarget ct = getConfiguredTarget("//test:main");
+    Artifact output = getBinArtifact("out", ct);
+    SpawnAction action = (SpawnAction) getGeneratingAction(output);
+    assertThat(paramFileArgsForAction(action)).containsExactly("--foo");
+  }
+
+  @Test
+  public void testWriteArgsToParamFile() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "args.add('--foo')",
+        "output=ruleContext.actions.declare_file('out')",
+        "ruleContext.actions.write(",
+        "  output=output,",
+        "  content=args,",
+        ")");
+    List<ActionAnalysisMetadata> actions =
+        ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions();
+    Optional<ActionAnalysisMetadata> action =
+        actions.stream().filter(a -> a instanceof ParameterFileWriteAction).findFirst();
+    assertThat(action.isPresent()).isTrue();
+    ParameterFileWriteAction paramAction = (ParameterFileWriteAction) action.get();
+    assertThat(paramAction.getArguments()).containsExactly("--foo");
+  }
+
+  @Test
+  public void testLazyArgsWithParamFileInvalidFormatString() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "Invalid value for parameter \"param_file_arg\": Expected string with a single \"--file=\"",
+        "args = ruleContext.actions.args()\n" + "args.use_param_file('--file=')");
+    ev.checkEvalErrorContains(
+        "Invalid value for parameter \"param_file_arg\": "
+            + "Expected string with a single \"--file=%s%s\"",
+        "args = ruleContext.actions.args()\n" + "args.use_param_file('--file=%s%s')");
+  }
+
+  @Test
+  public void testLazyArgsWithParamFileInvalidFormat() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "Invalid value for parameter \"format\": Expected one of \"shell\", \"multiline\"",
+        "args = ruleContext.actions.args()\n" + "args.set_param_file_format('illegal')");
+  }
+
+  @Test
+  public void testArgsAddInvalidTypesForArgAndValues() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "expected value of type 'string' for arg name, got 'Integer'",
+        "args = ruleContext.actions.args()",
+        "args.add(1, 'value')");
+    ev.checkEvalErrorContains(
+        "expected value of type 'string' for arg name, got 'Integer'",
+        "args = ruleContext.actions.args()",
+        "args.add_all(1, [1, 2])");
+    ev.checkEvalErrorContains(
+        "expected value of type 'sequence or depset' for values, got 'Integer'",
+        "args = ruleContext.actions.args()",
+        "args.add_all(1)");
+    ev.checkEvalErrorContains(
+        "in call to add_all(), parameter 'values' got value of type 'int', want 'sequence or"
+            + " depset'",
+        "args = ruleContext.actions.args()",
+        "args.add_all('--foo', 1)");
+  }
+
+  @Test
+  public void testLazyArgIllegalFormatString() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "Invalid value for parameter \"format\": Expected string with a single \"%s\"",
+        "args = ruleContext.actions.args()",
+        "args.add('foo', format='illegal_format')", // Expects two args, will only be given one
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+  }
+
+  @Test
+  public void testMapEachAcceptsBuiltinFunction() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    // map_each accepts a non-Starlark built-in function such as str.
+    ev.exec("ruleContext.actions.args().add_all(['foo'], map_each = str)");
+  }
+
+  @Test
+  public void testLazyArgMapEachThrowsError() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "def bad_fn(val): 'hello'.nosuchmethod()",
+        "args.add_all([1, 2], map_each=bad_fn)",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    CommandLineExpansionException e =
+        assertThrows(CommandLineExpansionException.class, () -> action.getArguments());
+    assertThat(e).hasMessageThat().contains("'string' value has no field or method 'nosuchmethod'");
+  }
+
+  @Test
+  public void testLazyArgMapEachReturnsNone() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "def none_fn(val): return None if val == 'nokeep' else val",
+        "args.add_all(['keep', 'nokeep'], map_each=none_fn)",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    assertThat(action.getArguments()).containsExactly("foo/t.exe", "keep").inOrder();
+  }
+
+  @Test
+  public void testLazyArgMapEachReturnsWrongType() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "def bad_fn(val): return 1",
+        "args.add_all([1, 2], map_each=bad_fn)",
+        "ruleContext.actions.run(",
+        "  inputs = depset(ruleContext.files.srcs),",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  executable = ruleContext.files.tools[0],",
+        ")");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    CommandLineExpansionException e =
+        assertThrows(CommandLineExpansionException.class, () -> action.getArguments());
+    assertThat(e.getMessage())
+        .contains("Expected map_each to return string, None, or list of strings, found Integer");
+  }
+
+  @Test
+  public void createShellWithLazyArgs() throws Exception {
+    StarlarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    setRuleContext(ruleContext);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "args.add('--foo')",
+        "ruleContext.actions.run_shell(",
+        "  inputs = ruleContext.files.srcs,",
+        "  outputs = ruleContext.files.srcs,",
+        "  arguments = [args],",
+        "  mnemonic = 'DummyMnemonic',",
+        "  command = 'dummy_command',",
+        "  progress_message = 'dummy_message',",
+        "  use_default_shell_env = True)");
+    SpawnAction action =
+        (SpawnAction)
+            Iterables.getOnlyElement(
+                ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+    List<String> args = action.getArguments();
+    // We don't need to assert the entire arg list, just check that
+    // the dummy empty string is inserted followed by '--foo'
+    assertThat(args.get(args.size() - 2)).isEmpty();
+    assertThat(Iterables.getLast(args)).isEqualTo("--foo");
+  }
+
+  @Test
+  public void testLazyArgsObjectImmutability() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "load('//test:rules.bzl', 'main_rule', 'dep_rule')",
+        "dep_rule(name = 'dep')",
+        "main_rule(name = 'main', deps = [':dep'])");
+    scratch.file(
+        "test/rules.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _main_impl(ctx):",
+        "  dep = ctx.attr.deps[0]",
+        "  args = dep[MyInfo].dep_arg",
+        "  args.add('hello')",
+        "main_rule = rule(",
+        "  implementation = _main_impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list()",
+        "  },",
+        "  outputs = {'file': 'output.txt'},",
+        ")",
+        "def _dep_impl(ctx):",
+        "  args = ctx.actions.args()",
+        "  return [MyInfo(dep_arg = args)]",
+        "dep_rule = rule(implementation = _dep_impl)");
+    AssertionError e = assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+    assertThat(e).hasMessageThat().contains("trying to mutate a frozen Args value");
+  }
+
+  @Test
+  public void testConfigurationField_StarlarkSplitTransitionProhibited() throws Exception {
+    scratch.file(
+        "tools/allowlists/function_transition_allowlist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_allowlist',",
+        "    packages = [",
+        "        '//...',",
+        "    ],",
+        ")");
+
+    scratch.file(
+        "test/rule.bzl",
+        "def _foo_impl(ctx):",
+        "  return []",
+        "",
+        "def _foo_transition_impl(settings):",
+        "  return {'t1': {}, 't2': {}}",
+        "foo_transition = transition(implementation=_foo_transition_impl, inputs=[], outputs=[])",
+        "",
+        "foo = rule(",
+        "  implementation = _foo_impl,",
+        "  attrs = {",
+        "    '_allowlist_function_transition': attr.label(",
+        "        default = '//tools/allowlists/function_transition_allowlist'),",
+        "    '_attr': attr.label(",
+        "        cfg = foo_transition,",
+        "        default = configuration_field(fragment='cpp', name = 'cc_toolchain'))})");
+
+    scratch.file("test/BUILD", "load('//test:rule.bzl', 'foo')", "foo(name='foo')");
+
+    setStarlarkSemanticsOptions("--experimental_starlark_config_transitions=true");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:foo");
+    assertContainsEvent("late-bound attributes must not have a split configuration transition");
+  }
+
+  @Test
+  public void testConfigurationField_NativeSplitTransitionProviderProhibited() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _foo_impl(ctx):",
+        "  return []",
+        "",
+        "foo = rule(",
+        "  implementation = _foo_impl,",
+        "  attrs = {",
+        "    '_attr': attr.label(",
+        "        cfg = apple_common.multi_arch_split,",
+        "        default = configuration_field(fragment='cpp', name = 'cc_toolchain'))})");
+
+    scratch.file("test/BUILD", "load('//test:rule.bzl', 'foo')", "foo(name='foo')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:foo");
+    assertContainsEvent("late-bound attributes must not have a split configuration transition");
+  }
+
+  @Test
+  public void testConfigurationField_NativeSplitTransitionProhibited() throws Exception {
+    scratch.file(
+        "test/rule.bzl",
+        "def _foo_impl(ctx):",
+        "  return []",
+        "",
+        "foo = rule(",
+        "  implementation = _foo_impl,",
+        "  attrs = {",
+        "    '_attr': attr.label(",
+        "        cfg = android_common.multi_cpu_configuration,",
+        "        default = configuration_field(fragment='cpp', name = 'cc_toolchain'))})");
+    setStarlarkSemanticsOptions("--experimental_google_legacy_api");
+
+    scratch.file("test/BUILD", "load('//test:rule.bzl', 'foo')", "foo(name='foo')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test:foo");
+    assertContainsEvent("late-bound attributes must not have a split configuration transition");
+  }
+
+  @Test
+  public void testConfigurationField_invalidFragment() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'notarealfragment', name = 'method_name')),",
+        "    },",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+    assertThat(expected).hasMessageThat()
+        .contains("invalid configuration fragment name 'notarealfragment'");
+  }
+
+  @Test
+  public void testConfigurationField_doesNotChangeFragmentAccess() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def _impl(ctx):",
+        "  return [MyInfo(platform = ctx.fragments.apple.single_arch_platform)]",
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = [],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("has to declare 'apple' as a required fragment in target configuration");
+  }
+
+  @Test
+  public void testConfigurationField_invalidFieldName() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'notarealfield')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("invalid configuration field name 'notarealfield' on fragment 'apple'");
+  }
+
+  // Verifies that configuration_field can only be used on 'private' attributes.
+  @Test
+  public void testConfigurationField_invalidVisibility() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { 'myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("When an attribute value is a function, "
+            + "the attribute must be private (i.e. start with '_')");
+  }
+
+  @Test
+  public void testFilesToRunInActionsRun() throws Exception {
+    scratch.file(
+        "a/a.bzl",
+        "def _impl(ctx):",
+        "    f = ctx.actions.declare_file('output')",
+        "    ctx.actions.run(",
+        "        inputs = [],",
+        "        outputs = [f],",
+        "        executable = ctx.attr._tool[DefaultInfo].files_to_run)",
+        "    return [DefaultInfo(files=depset([f]))]",
+        "r = rule(implementation=_impl, attrs = {'_tool': attr.label(default='//a:tool')})");
+
+    scratch.file(
+        "a/BUILD",
+        "load(':a.bzl', 'r')",
+        "r(name='r')",
+        "sh_binary(name='tool', srcs=['tool.sh'], data=['data'])");
+
+    ConfiguredTarget r = getConfiguredTarget("//a:r");
+    Action action =
+        getGeneratingAction(r.getProvider(FileProvider.class).getFilesToBuild().getSingleton());
+    assertThat(ActionsTestUtil.baseArtifactNames(action.getRunfilesSupplier().getArtifacts()))
+        .containsAtLeast("tool", "tool.sh", "data");
+  }
+
+  @Test
+  public void testFilesToRunInActionsTools() throws Exception {
+    scratch.file(
+        "a/a.bzl",
+        "def _impl(ctx):",
+        "    f = ctx.actions.declare_file('output')",
+        "    ctx.actions.run(",
+        "        inputs = [],",
+        "        outputs = [f],",
+        "        tools = [ctx.attr._tool[DefaultInfo].files_to_run],",
+        "        executable = 'a/tool')",
+        "    return [DefaultInfo(files=depset([f]))]",
+        "r = rule(implementation=_impl, attrs = {'_tool': attr.label(default='//a:tool')})");
+
+    scratch.file(
+        "a/BUILD",
+        "load(':a.bzl', 'r')",
+        "r(name='r')",
+        "sh_binary(name='tool', srcs=['tool.sh'], data=['data'])");
+
+    ConfiguredTarget r = getConfiguredTarget("//a:r");
+    Action action =
+        getGeneratingAction(r.getProvider(FileProvider.class).getFilesToBuild().getSingleton());
+    assertThat(ActionsTestUtil.baseArtifactNames(action.getRunfilesSupplier().getArtifacts()))
+        .containsAtLeast("tool", "tool.sh", "data");
+  }
+
+  // Verifies that configuration_field can only be used on 'label' attributes.
+  @Test
+  public void testConfigurationField_invalidAttributeType() throws Exception {
+    scratch.file(
+        "test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.int(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "in call to int(), parameter 'default' got value of type 'LateBoundDefault', want"
+                + " 'int'");
+  }
+
+  @Test
+  public void testStarlarkCustomCommandLineKeyComputation() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    ImmutableList.Builder<CommandLine> commandLines = ImmutableList.builder();
+
+    commandLines.add(getCommandLine("args = ruleContext.actions.args()"));
+    commandLines.add(getCommandLine("args = ruleContext.actions.args()", "args.add('foo')"));
+    commandLines.add(
+        getCommandLine("args = ruleContext.actions.args()", "args.add('--foo', 'foo')"));
+    commandLines.add(
+        getCommandLine("args = ruleContext.actions.args()", "args.add('foo', format='--foo=%s')"));
+    commandLines.add(
+        getCommandLine("args = ruleContext.actions.args()", "args.add_all(['foo', 'bar'])"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()", "args.add_all('-foo', ['foo', 'bar'])"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "args.add_all(['foo', 'bar'], format_each='format%s')"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()", "args.add_all(['foo', 'bar'], before_each='-I')"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()", "args.add_all(['boing', 'boing', 'boing'])"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "args.add_all(['boing', 'boing', 'boing'], uniquify=True)"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "args.add_all(['foo', 'bar'], terminate_with='baz')"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()", "args.add_joined(['foo', 'bar'], join_with=',')"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "args.add_joined(['foo', 'bar'], join_with=',', format_joined='--foo=%s')"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _map_each(s): return s + '_mapped'",
+            "args.add_all(['foo', 'bar'], map_each=_map_each)"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "values = depset(['a', 'b'])",
+            "args.add_all(values)"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _map_each(s): return s + '_mapped'",
+            "values = depset(['a', 'b'])",
+            "args.add_all(values, map_each=_map_each)"));
+    commandLines.add(
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _map_each(s): return s + '_mapped_again'",
+            "values = depset(['a', 'b'])",
+            "args.add_all(values, map_each=_map_each)"));
+
+    // Ensure all these command lines have distinct keys
+    Map<String, CommandLine> digests = new HashMap<>();
+    for (CommandLine commandLine : commandLines.build()) {
+      String digest = getDigest(commandLine);
+      CommandLine previous = digests.putIfAbsent(digest, commandLine);
+      if (previous != null) {
+        fail(
+            String.format(
+                "Found two command lines with identical digest %s: '%s' and '%s'",
+                digest,
+                Joiner.on(' ').join(previous.arguments()),
+                Joiner.on(' ').join(commandLine.arguments())));
+      }
+    }
+
+    // Ensure errors are handled
+    CommandLine commandLine =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _bad_fn(s): return s.doesnotexist()",
+            "values = depset(['a', 'b'])",
+            "args.add_all(values, map_each=_bad_fn)");
+    assertThrows(
+        CommandLineExpansionException.class,
+        () ->
+            commandLine.addToFingerprint(
+                actionKeyContext, /*artifactExpander=*/ null, new Fingerprint()));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputation_differentMapEach() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _fun1(arg): return 'val1'",
+            "def _fun2(arg): return 'val2'",
+            "args.add_all(['a'], map_each=_fun1)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _fun1(arg): return 'val1'",
+            "def _fun2(arg): return 'val2'",
+            "args.add_all(['a'], map_each=_fun2)");
+
+    assertThat(getDigest(commandLine1)).isNotEqualTo(getDigest(commandLine2));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputation_differentArg() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _fun(arg): return arg",
+            "args.add_all(['a'], map_each=_fun)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "def _fun(arg): return arg",
+            "args.add_all(['b'], map_each=_fun)");
+
+    assertThat(getDigest(commandLine1)).isNotEqualTo(getDigest(commandLine2));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_equivalentMapEach_sameKey()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "args.add_joined([directory], join_with=',', map_each=str, expand_directories=True)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def mystr(file): return str(file)",
+            "args.add_joined([directory], join_with=',', map_each=mystr, expand_directories=True)");
+
+    ArtifactExpander expander = createArtifactExpander("foo/dir", "file");
+    assertThat(getDigest(commandLine1, expander)).isEqualTo(getDigest(commandLine2, expander));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_mapEachConstantForDir()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value1'",
+            "args.add_all([directory], map_each=_constant_for_dir, expand_directories=True)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value2'",
+            "args.add_all([directory], map_each=_constant_for_dir, expand_directories=True)");
+
+    ArtifactExpander expander = createArtifactExpander("foo/dir", "file");
+    assertThat(getDigest(commandLine1, expander)).isNotEqualTo(getDigest(commandLine2, expander));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_constantForDirWithNestedSet()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "dir = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value1'",
+            "args.add_all(depset([dir]), map_each=_constant_for_dir, expand_directories=True)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "dir = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value2'",
+            "args.add_all(depset([dir]), map_each=_constant_for_dir, expand_directories=True)");
+
+    ArtifactExpander expander = createArtifactExpander("foo/dir", "file");
+    assertThat(getDigest(commandLine1, expander)).isNotEqualTo(getDigest(commandLine2, expander));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_mapEachFailsForDir()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "ruleContext.actions.run_shell(outputs=[directory], command='')",
+            "def _fail_for_dir(file):",
+            "   if file.path.endswith('dir'): fail('hello')",
+            "   return 'value1'",
+            "args.add_all([directory], map_each=_fail_for_dir, expand_directories=True)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "ruleContext.actions.run_shell(outputs=[directory], command='')",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _fail_for_dir(file):",
+            "   if file.path.endswith('dir'): fail('hello')",
+            "   return 'value2'",
+            "args.add_all([directory], map_each=_fail_for_dir, expand_directories=True)");
+
+    ArtifactExpander expander = createArtifactExpander("foo/dir", "file");
+    assertThat(getDigest(commandLine1, expander)).isNotEqualTo(getDigest(commandLine2, expander));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_differentExpansion()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    CommandLine commandLine =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "ruleContext.actions.run_shell(outputs=[directory], command='')",
+            "def _get_path(file): return file.path",
+            "args.add_all([directory], map_each=_get_path, expand_directories=True)");
+
+    ArtifactExpander expander1 = createArtifactExpander("foo/dir", "file1");
+    ArtifactExpander expander2 = createArtifactExpander("foo/dir", "file2");
+    assertThat(getDigest(commandLine, expander1)).isNotEqualTo(getDigest(commandLine, expander2));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_differentExpansionNoMapEach()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    CommandLine commandLine =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "args.add_all([directory])");
+
+    ArtifactExpander expander1 = createArtifactExpander("foo/dir", "file1");
+    ArtifactExpander expander2 = createArtifactExpander("foo/dir", "file2");
+    assertThat(getDigest(commandLine, expander1)).isNotEqualTo(getDigest(commandLine, expander2));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_extraFileInExpansionNoMapEach()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    CommandLine commandLine =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "args.add_all([directory])");
+
+    ArtifactExpander expander1 = createArtifactExpander("foo/dir", "file1");
+    ArtifactExpander expander2 = createArtifactExpander("foo/dir", "file1", "file2");
+    assertThat(getDigest(commandLine, expander1)).isNotEqualTo(getDigest(commandLine, expander2));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputationWithExpander_constantForDirAddJoined()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value1'",
+            "args.add_joined([directory], join_with=',', map_each=_constant_for_dir,"
+                + " expand_directories=True)");
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _constant_for_dir(f): return 'constant' if f.path.endswith('dir') else 'value2'",
+            "args.add_joined([directory], join_with=',', map_each=_constant_for_dir,"
+                + " expand_directories=True)");
+
+    ArtifactExpander expander = createArtifactExpander("foo/dir", "file");
+    assertThat(getDigest(commandLine1, expander)).isNotEqualTo(getDigest(commandLine2, expander));
+  }
+
+  @Test
+  public void starlarkCustomCommandLineKeyComputation_inconsequentialChangeToStarlarkSemantics()
+      throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    CommandLine commandLine1 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _path(f): return f.path",
+            "args.add_all([directory], map_each=_path)");
+
+    ev.setSemantics("--incompatible_run_shell_command_string");
+    // setStarlarkSemanticsOptions reinitializes the thread -- set the ruleContext on the new one.
+    setRuleContext(createRuleContext("//foo:foo"));
+
+    CommandLine commandLine2 =
+        getCommandLine(
+            "args = ruleContext.actions.args()",
+            "directory = ruleContext.actions.declare_directory('dir')",
+            "def _path(f): return f.path",
+            "args.add_all([directory], map_each=_path)");
+
+    assertThat(getDigest(commandLine1)).isEqualTo(getDigest(commandLine2));
+  }
+
+  private static ArtifactExpander createArtifactExpander(String dirRelativePath, String... files) {
+    return (artifact, output) -> {
+      Preconditions.checkArgument(
+          artifact.getRootRelativePath().equals(PathFragment.create(dirRelativePath)));
+      for (String file : files) {
+        output.add(
+            new DerivedArtifact(
+                artifact.getRoot(),
+                artifact.getExecPath().getRelative(file),
+                (ActionLookupKey) artifact.getArtifactOwner()));
+      }
+    };
+  }
+
+  private String getDigest(CommandLine commandLine) throws CommandLineExpansionException {
+    return getDigest(commandLine, /*artifactExpander=*/ null);
+  }
+
+  private String getDigest(CommandLine commandLine, ArtifactExpander artifactExpander)
+      throws CommandLineExpansionException {
+    Fingerprint fingerprint = new Fingerprint();
+    commandLine.addToFingerprint(actionKeyContext, artifactExpander, fingerprint);
+    return fingerprint.hexDigestAndReset();
+  }
+
+  private CommandLine getCommandLine(String... lines) throws Exception {
+    ev.exec(lines);
+    return ((Args) ev.eval("args")).build();
+  }
+
+  @Test
+  public void testPrintArgs() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.exec("args = ruleContext.actions.args()", "args.add_all(['--foo', '--bar'])");
+    Args args = (Args) ev.eval("args");
+    assertThat(Printer.getPrinter().debugPrint(args).toString()).isEqualTo("--foo --bar");
+  }
+
+  @Test
+  public void testDirectoryInArgs() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "directory = ruleContext.actions.declare_directory('dir')",
+        "def _short_path(f): return f.short_path", // For easier assertions
+        "args.add_all([directory], map_each=_short_path)");
+    Sequence<?> result = (Sequence<?>) ev.eval("args, directory");
+    Args args = (Args) result.get(0);
+    Artifact directory = (Artifact) result.get(1);
+    CommandLine commandLine = args.build();
+
+    // When asking for arguments without an artifact expander we just return the directory
+    assertThat(commandLine.arguments()).containsExactly("foo/dir");
+
+    // Now ask for one with an expanded directory
+    Artifact file1 = getBinArtifactWithNoOwner("foo/dir/file1");
+    Artifact file2 = getBinArtifactWithNoOwner("foo/dir/file2");
+    ArtifactExpanderImpl artifactExpander =
+        new ArtifactExpanderImpl(
+            ImmutableMap.of(directory, ImmutableList.of(file1, file2)), ImmutableMap.of());
+    assertThat(commandLine.arguments(artifactExpander))
+        .containsExactly("foo/dir/file1", "foo/dir/file2");
+  }
+
+  @Test
+  public void testDirectoryInArgsExpandDirectories() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "directory = ruleContext.actions.declare_directory('dir')",
+        "def _short_path(f): return f.short_path", // For easier assertions
+        "args.add_all([directory], map_each=_short_path, expand_directories=True)",
+        "args.add_all([directory], map_each=_short_path, expand_directories=False)");
+    Sequence<?> result = (Sequence<?>) ev.eval("args, directory");
+    Args args = (Args) result.get(0);
+    Artifact directory = (Artifact) result.get(1);
+    CommandLine commandLine = args.build();
+
+    Artifact file1 = getBinArtifactWithNoOwner("foo/dir/file1");
+    Artifact file2 = getBinArtifactWithNoOwner("foo/dir/file2");
+    ArtifactExpanderImpl artifactExpander =
+        new ArtifactExpanderImpl(
+            ImmutableMap.of(directory, ImmutableList.of(file1, file2)), ImmutableMap.of());
+    // First expanded, then not expanded (two separate calls)
+    assertThat(commandLine.arguments(artifactExpander))
+        .containsExactly("foo/dir/file1", "foo/dir/file2", "foo/dir");
+  }
+
+  @Test
+  public void testDirectoryInScalarArgsFails() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.checkEvalErrorContains(
+        "Cannot add directories to Args#add",
+        "args = ruleContext.actions.args()",
+        "directory = ruleContext.actions.declare_directory('dir')",
+        "args.add(directory)");
+  }
+
+  @Test
+  public void testParamFileHasDirectoryAsInput() throws Exception {
+    StarlarkRuleContext ctx = createRuleContext("//foo:foo");
+    setRuleContext(ctx);
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "directory = ruleContext.actions.declare_directory('dir')",
+        "args.add_all([directory])",
+        "params = ruleContext.actions.declare_file('params')",
+        "ruleContext.actions.write(params, args)");
+    Sequence<?> result = (Sequence<?>) ev.eval("params, directory");
+    Artifact params = (Artifact) result.get(0);
+    Artifact directory = (Artifact) result.get(1);
+    ActionAnalysisMetadata action =
+        ctx.getRuleContext().getAnalysisEnvironment().getLocalGeneratingAction(params);
+    assertThat(action.getInputs().toList()).contains(directory);
+  }
+
+  @Test
+  public void testDirectoryExpansionInArgs() throws Exception {
+    setRuleContext(createRuleContext("//foo:foo"));
+    ev.exec(
+        "args = ruleContext.actions.args()",
+        "directory = ruleContext.actions.declare_directory('dir')",
+        "file3 = ruleContext.actions.declare_file('file3')",
+        "def _expand_dirs(artifact, dir_expander):",
+        "  return [f.short_path for f in dir_expander.expand(artifact)]",
+        "args.add_all([directory, file3], map_each=_expand_dirs)");
+    Args args = (Args) ev.eval("args");
+    Artifact directory = (Artifact) ev.eval("directory");
+    CommandLine commandLine = args.build();
+
+    Artifact file1 = getBinArtifactWithNoOwner("foo/dir/file1");
+    Artifact file2 = getBinArtifactWithNoOwner("foo/dir/file2");
+    ArtifactExpanderImpl artifactExpander =
+        new ArtifactExpanderImpl(
+            ImmutableMap.of(directory, ImmutableList.of(file1, file2)), ImmutableMap.of());
+    assertThat(commandLine.arguments(artifactExpander))
+        .containsExactly("foo/dir/file1", "foo/dir/file2", "foo/file3");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkStringRepresentationsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkStringRepresentationsTest.java
new file mode 100644
index 0000000..b1f2764
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkStringRepresentationsTest.java
@@ -0,0 +1,377 @@
+// Copyright 2017 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.starlark;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for string representations of Starlark objects. */
+@RunWith(JUnit4.class)
+public class StarlarkStringRepresentationsTest extends BuildViewTestCase {
+
+  // Different ways to format objects, these suffixes are used in the `prepare_params` function
+  private static final ImmutableList<String> SUFFIXES =
+      ImmutableList.of("_str", "_repr", "_format", "_str_perc", "_repr_perc");
+
+  private Object starlarkLoadingEval(String code) throws Exception {
+    return starlarkLoadingEval(code, "");
+  }
+
+  /**
+   * Evaluates {@code code} in the loading phase in a .bzl file
+   *
+   * @param code The code to execute
+   * @param definition Additional code to define necessary variables
+   */
+  private Object starlarkLoadingEval(String code, String definition) throws Exception {
+    scratch.overwriteFile("eval/BUILD", "load(':eval.bzl', 'eval')", "eval(name='eval')");
+    scratch.overwriteFile(
+        "eval/eval.bzl",
+        definition,
+        String.format("x = %s", code), // Should be placed here to execute during the loading phase
+        "def _impl(ctx):",
+        "  return struct(result = x)",
+        "eval = rule(implementation = _impl)");
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter,
+        new ModifiedFileSet.Builder()
+            .modify(PathFragment.create("eval/BUILD"))
+            .modify(PathFragment.create("eval/eval.bzl"))
+            .build(),
+        Root.fromPath(rootDirectory));
+
+    ConfiguredTarget target = getConfiguredTarget("//eval");
+    return target.get("result");
+  }
+
+  /**
+   * Evaluates {@code code} in the loading phase in a BUILD file. {@code code} must return a string.
+   *
+   * @param code The code to execute
+   */
+  private Object starlarkLoadingEvalInBuildFile(String code) throws Exception {
+    scratch.overwriteFile("eval/BUILD",
+        "load(':eval.bzl', 'eval')",
+        String.format("eval(name='eval', param = %s)", code));
+    scratch.overwriteFile(
+        "eval/eval.bzl",
+        "def _impl(ctx):",
+        "  return struct(result = ctx.attr.param)",
+        "eval = rule(implementation = _impl, attrs = {'param': attr.string()})");
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter,
+        new ModifiedFileSet.Builder()
+            .modify(PathFragment.create("eval/BUILD"))
+            .modify(PathFragment.create("eval/eval.bzl"))
+            .build(),
+        Root.fromPath(rootDirectory));
+
+    ConfiguredTarget target = getConfiguredTarget("//eval");
+    return target.get("result");
+  }
+
+  /**
+   * Asserts that all 5 different ways to convert an object to a string of {@code expression}
+   * ({@code str}, {@code repr}, {@code '%s'}, {@code '%r'}, {@code '{}'.format} return the correct
+   * {@code representation}. Not applicable for objects that have different {@code str} and {@code
+   * repr} representations.
+   *
+   * @param expression the expression to evaluate a string representation of
+   * @param representation desired string representation
+   */
+  private void assertStringRepresentationInBuildFile(
+      String expression, String representation) throws Exception {
+    assertThat(starlarkLoadingEvalInBuildFile(String.format("str(%s)", expression)))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEvalInBuildFile(String.format("repr(%s)", expression)))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEvalInBuildFile(String.format("'%%s' %% (%s,)", expression)))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEvalInBuildFile(String.format("'%%r' %% (%s,)", expression)))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEvalInBuildFile(String.format("'{}'.format(%s)", expression)))
+        .isEqualTo(representation);
+  }
+
+  /**
+   * Asserts that all 5 different ways to convert an object to a string of {@code expression}
+   * ({@code str}, {@code repr}, {@code '%s'}, {@code '%r'}, {@code '{}'.format} return the correct
+   * {@code representation}. Not applicable for objects that have different {@code str} and {@code
+   * repr} representations.
+   *
+   * @param definition optional definition required to evaluate the {@code expression}
+   * @param expression the expression to evaluate a string representation of
+   * @param representation desired string representation
+   */
+  private void assertStringRepresentation(
+      String definition, String expression, String representation) throws Exception {
+    assertThat(starlarkLoadingEval(String.format("str(%s)", expression), definition))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEval(String.format("repr(%s)", expression), definition))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEval(String.format("'%%s' %% (%s,)", expression), definition))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEval(String.format("'%%r' %% (%s,)", expression), definition))
+        .isEqualTo(representation);
+    assertThat(starlarkLoadingEval(String.format("'{}'.format(%s)", expression), definition))
+        .isEqualTo(representation);
+  }
+
+  private void assertStringRepresentation(String expression, String representation)
+      throws Exception {
+    assertStringRepresentation("", expression, representation);
+  }
+
+  /**
+   * Creates a set of BUILD and .bzl files that gathers objects of many different types available in
+   * Starlark and creates their string representations by calling `str` and `repr` on them. The
+   * strings are available in the configured target for //test/starlark:check
+   */
+  private void generateFilesToTestStrings() throws Exception {
+    // Generate string representations of Starlark rule contexts, targets, and files.
+    // Objects are gathered in the implementation of the `check` rule.
+    // prepare_params(objects) converts a dict of objects to a dict of their string representations.
+
+    scratch.file(
+        "test/starlark/rules.bzl",
+        "aspect_ctx_provider = provider()",
+        "def prepare_params(objects):",
+        "  params = {}",
+        "  for k, v in objects.items():",
+        "    params[k + '_str'] = str(v)",
+        "    params[k + '_repr'] = repr(v)",
+        "    params[k + '_format'] = '{}'.format(v)",
+        "    params[k + '_str_perc'] = '%s' % (v,)",
+        "    params[k + '_repr_perc'] = '%r' % (v,)",
+        "  return params",
+        "",
+        "def _impl_aspect(target, ctx):",
+        "  return [aspect_ctx_provider(ctx = ctx, rule = ctx.rule)]",
+        "my_aspect = aspect(implementation = _impl_aspect)",
+        "",
+        "def _impl(ctx): pass",
+        "dep = rule(implementation = _impl)",
+        "",
+        "def _genfile_impl(ctx):",
+        "  ctx.actions.write(output = ctx.outputs.my_output, content = 'foo')",
+        "genfile = rule(",
+        "  implementation = _genfile_impl,",
+        "  outputs = {'my_output': '%{name}.txt'},",
+        ")",
+        "",
+        "def _check_impl(ctx):",
+        "  source_file = ctx.attr.srcs[0].files.to_list()[0]",
+        "  generated_file = ctx.attr.srcs[1].files.to_list()[0]",
+        "  objects = {",
+        "    'target': ctx.attr.deps[0],",
+        "    'alias_target': ctx.attr.deps[1],",
+        "    'aspect_target': ctx.attr.asp_deps[0],",
+        "    'input_target': ctx.attr.srcs[0],",
+        "    'output_target': ctx.attr.srcs[1],",
+        "    'rule_ctx': ctx,",
+        "    'aspect_ctx': ctx.attr.asp_deps[0][aspect_ctx_provider].ctx,",
+        "    'aspect_ctx.rule': ctx.attr.asp_deps[0][aspect_ctx_provider].rule,",
+        "    'source_file': source_file,",
+        "    'generated_file': generated_file,",
+        "    'source_root': source_file.root,",
+        "    'generated_root': generated_file.root,",
+        "  }",
+        "  return struct(**prepare_params(objects))",
+        "check = rule(",
+        "  implementation = _check_impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(),",
+        "    'asp_deps': attr.label_list(aspects = [my_aspect]),",
+        "    'srcs': attr.label_list(allow_files = True),",
+        "  },",
+        ")");
+
+    scratch.file(
+        "test/starlark/BUILD",
+        "load(':rules.bzl', 'check', 'dep', 'genfile')",
+        "",
+        "dep(name = 'foo')",
+        "dep(name = 'bar')",
+        "alias(name = 'foobar', actual = ':foo')",
+        "genfile(name = 'output')",
+        "check(",
+        "  name = 'check',",
+        "  deps = [':foo', ':foobar'],",
+        "  asp_deps = [':bar'],",
+        "  srcs = ['input.txt', 'output.txt'],",
+        ")");
+  }
+
+  @Test
+  public void testStringRepresentations_Strings() throws Exception {
+    assertThat(starlarkLoadingEval("str('foo')")).isEqualTo("foo");
+    assertThat(starlarkLoadingEval("'%s' % 'foo'")).isEqualTo("foo");
+    assertThat(starlarkLoadingEval("'{}'.format('foo')")).isEqualTo("foo");
+    assertThat(starlarkLoadingEval("repr('foo')")).isEqualTo("\"foo\"");
+    assertThat(starlarkLoadingEval("'%r' % 'foo'")).isEqualTo("\"foo\"");
+  }
+
+  @Test
+  public void testStringRepresentations_Labels() throws Exception {
+    assertThat(starlarkLoadingEval("str(Label('//foo:bar'))")).isEqualTo("//foo:bar");
+    assertThat(starlarkLoadingEval("'%s' % Label('//foo:bar')")).isEqualTo("//foo:bar");
+    assertThat(starlarkLoadingEval("'{}'.format(Label('//foo:bar'))")).isEqualTo("//foo:bar");
+    assertThat(starlarkLoadingEval("repr(Label('//foo:bar'))")).isEqualTo("Label(\"//foo:bar\")");
+    assertThat(starlarkLoadingEval("'%r' % Label('//foo:bar')")).isEqualTo("Label(\"//foo:bar\")");
+
+    assertThat(starlarkLoadingEval("'{}'.format([Label('//foo:bar')])"))
+        .isEqualTo("[Label(\"//foo:bar\")]");
+  }
+
+  @Test
+  public void testStringRepresentations_Primitives() throws Exception {
+    // Strings are tested in a separate test case as they have different str and repr values.
+    assertStringRepresentation("1543", "1543");
+    assertStringRepresentation("True", "True");
+    assertStringRepresentation("False", "False");
+  }
+
+  @Test
+  public void testStringRepresentations_Containers() throws Exception {
+    assertStringRepresentation("['a', 'b']", "[\"a\", \"b\"]");
+    assertStringRepresentation("('a', 'b')", "(\"a\", \"b\")");
+    assertStringRepresentation("{'a': 'b', 'c': 'd'}", "{\"a\": \"b\", \"c\": \"d\"}");
+    assertStringRepresentation("struct(d = 4, c = 3)", "struct(c = 3, d = 4)");
+  }
+
+  @Test
+  public void testStringRepresentations_Functions() throws Exception {
+    assertStringRepresentation("all", "<built-in function all>");
+    assertStringRepresentation("def f(): pass", "f", "<function f from //eval:eval.bzl>");
+  }
+
+  @Test
+  public void testStringRepresentations_Rules() throws Exception {
+    assertStringRepresentation("native.cc_library", "<built-in rule cc_library>");
+    assertStringRepresentation("def f(): pass", "rule(implementation=f)", "<rule>");
+  }
+
+  @Test
+  public void testStringRepresentations_Aspects() throws Exception {
+    assertStringRepresentation("def f(): pass", "aspect(implementation=f)", "<aspect>");
+  }
+
+  @Test
+  public void testStringRepresentations_Providers() throws Exception {
+    assertStringRepresentation("provider()", "<provider>");
+    assertStringRepresentation(
+        "p = provider()", "p(b = 'foo', a = 1)", "struct(a = 1, b = \"foo\")");
+  }
+
+  @Test
+  public void testStringRepresentations_Select() throws Exception {
+    assertStringRepresentation(
+        "select({'//foo': ['//bar']}) + select({'//foo2': ['//bar2']})",
+        "select({\"//foo\": [\"//bar\"]}) + select({\"//foo2\": [\"//bar2\"]})");
+  }
+
+  @Test
+  public void testStringRepresentations_RuleContext() throws Exception {
+    generateFilesToTestStrings();
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:check");
+
+    for (String suffix : SUFFIXES) {
+      assertThat(target.get("rule_ctx" + suffix))
+          .isEqualTo("<rule context for //test/starlark:check>");
+      assertThat(target.get("aspect_ctx" + suffix))
+          .isEqualTo("<aspect context for //test/starlark:bar>");
+      assertThat(target.get("aspect_ctx.rule" + suffix))
+          .isEqualTo("<rule collection for //test/starlark:bar>");
+    }
+  }
+
+  @Test
+  public void testStringRepresentations_Files() throws Exception {
+    generateFilesToTestStrings();
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:check");
+
+    for (String suffix : SUFFIXES) {
+      assertThat(target.get("source_file" + suffix))
+          .isEqualTo("<source file test/starlark/input.txt>");
+      assertThat(target.get("generated_file" + suffix))
+          .isEqualTo("<generated file test/starlark/output.txt>");
+    }
+  }
+
+  @Test
+  public void testStringRepresentations_Root() throws Exception {
+    generateFilesToTestStrings();
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:check");
+
+    for (String suffix : SUFFIXES) {
+      assertThat(target.get("source_root" + suffix)).isEqualTo("<source root>");
+      assertThat(target.get("generated_root" + suffix)).isEqualTo("<derived root>");
+    }
+  }
+
+  @Test
+  public void testStringRepresentations_Glob() throws Exception {
+    scratch.file("eval/one.txt");
+    scratch.file("eval/two.txt");
+    scratch.file("eval/three.txt");
+
+    assertStringRepresentationInBuildFile(
+        "glob(['*.txt'])",
+        "[\"one.txt\", \"three.txt\", \"two.txt\"]");
+  }
+
+  @Test
+  public void testStringRepresentations_Attr() throws Exception {
+    assertStringRepresentation("attr", "<attr>");
+    assertStringRepresentation("attr.int()", "<attr.int>");
+    assertStringRepresentation("attr.string()", "<attr.string>");
+    assertStringRepresentation("attr.label()", "<attr.label>");
+    assertStringRepresentation("attr.string_list()", "<attr.string_list>");
+    assertStringRepresentation("attr.int_list()", "<attr.int_list>");
+    assertStringRepresentation("attr.label_list()", "<attr.label_list>");
+    assertStringRepresentation("attr.label_keyed_string_dict()", "<attr.label_keyed_string_dict>");
+    assertStringRepresentation("attr.bool()", "<attr.bool>");
+    assertStringRepresentation("attr.output()", "<attr.output>");
+    assertStringRepresentation("attr.output_list()", "<attr.output_list>");
+    assertStringRepresentation("attr.string_dict()", "<attr.string_dict>");
+    assertStringRepresentation("attr.string_list_dict()", "<attr.string_list_dict>");
+  }
+
+  @Test
+  public void testStringRepresentations_Targets() throws Exception {
+    generateFilesToTestStrings();
+    ConfiguredTarget target = getConfiguredTarget("//test/starlark:check");
+
+    for (String suffix : SUFFIXES) {
+      assertThat(target.get("target" + suffix)).isEqualTo("<target //test/starlark:foo>");
+      assertThat(target.get("input_target" + suffix))
+          .isEqualTo("<input file target //test/starlark:input.txt>");
+      assertThat(target.get("output_target" + suffix))
+          .isEqualTo("<output file target //test/starlark:output.txt>");
+      assertThat(target.get("alias_target" + suffix))
+          .isEqualTo("<alias target //test/starlark:foobar of //test/starlark:foo>");
+      assertThat(target.get("aspect_target" + suffix))
+          .isEqualTo("<merged target //test/starlark:bar>");
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/util/BUILD b/src/test/java/com/google/devtools/build/lib/starlark/util/BUILD
new file mode 100644
index 0000000..4effb55
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/util/BUILD
@@ -0,0 +1,37 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["**"]),
+    visibility = ["//src:__subpackages__"],
+)
+
+java_library(
+    name = "util",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:keep-going-option",
+        "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/events",
+        "//src/main/java/com/google/devtools/build/lib/packages",
+        "//src/main/java/com/google/devtools/build/lib/packages:starlark_semantics_options",
+        "//src/main/java/com/google/devtools/build/lib/pkgcache",
+        "//src/main/java/com/google/devtools/build/lib/rules/platform",
+        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/test/java/com/google/devtools/build/lib/analysis/util",
+        "//src/test/java/com/google/devtools/build/lib/syntax/util",
+        "//src/test/java/com/google/devtools/build/lib/testutil:TestConstants",
+        "//third_party:guava",
+        "//third_party:junit4",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/util/BazelEvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/starlark/util/BazelEvaluationTestCase.java
new file mode 100644
index 0000000..5c4ac84
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/util/BazelEvaluationTestCase.java
@@ -0,0 +1,68 @@
+// Copyright 2014 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.starlark.util;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.starlark.StarlarkModules;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.BazelModuleContext;
+import com.google.devtools.build.lib.packages.BazelStarlarkContext;
+import com.google.devtools.build.lib.packages.SymbolGenerator;
+import com.google.devtools.build.lib.rules.platform.PlatformCommon;
+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;
+import com.google.devtools.build.lib.testutil.TestConstants;
+
+/**
+ * BazelEvaluationTestCase is a subclass of EvaluationTestCase that defines various Bazel built-ins
+ * in the environment.
+ */
+// TODO(adonovan): this is (and has always been) a mess, and an abuse of inheritance.
+// Once the production code API has disentangled Thread and Module, make this rational.
+public final class BazelEvaluationTestCase extends EvaluationTestCase {
+
+  @Override
+  protected Object newModuleHook(ImmutableMap.Builder<String, Object> predeclared) {
+    StarlarkModules.addStarlarkGlobalsToBuilder(predeclared);
+    Starlark.addModule(predeclared, new PlatformCommon());
+
+    // Return the module's client data. (This one uses dummy values for tests.)
+    return BazelModuleContext.create(
+        Label.parseAbsoluteUnchecked("//test:label", /*defaultToMain=*/ false),
+        "test/label.bzl",
+        /*loads=*/ ImmutableMap.of(),
+        /*bzlTransitiveDigest=*/ new byte[0]);
+  }
+
+  @Override
+  protected void newThreadHook(StarlarkThread thread) {
+    // This StarlarkThread has no PackageContext, so attempts to create a rule will fail.
+    // Rule creation is tested by StarlarkIntegrationTest.
+
+    // This is a poor approximation to the thread that Blaze would create
+    // for testing rule implementation functions. It has phase LOADING, for example.
+    // TODO(adonovan): stop creating threads in tests. This is the responsibility of the
+    // production code. Tests should provide only files and commands.
+    new BazelStarlarkContext(
+            BazelStarlarkContext.Phase.LOADING,
+            TestConstants.TOOLS_REPOSITORY,
+            /*fragmentNameToClass=*/ null,
+            /*repoMapping=*/ ImmutableMap.of(),
+            new SymbolGenerator<>(new Object()),
+            /*analysisRuleLabel=*/ null) // dummy value for tests
+        .storeInThread(thread);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/util/StarlarkOptionsTestCase.java b/src/test/java/com/google/devtools/build/lib/starlark/util/StarlarkOptionsTestCase.java
new file mode 100644
index 0000000..ff4b909
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/starlark/util/StarlarkOptionsTestCase.java
@@ -0,0 +1,100 @@
+// Copyright 2019 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.starlark.util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.StarlarkSemanticsOptions;
+import com.google.devtools.build.lib.pkgcache.LoadingOptions;
+import com.google.devtools.build.lib.pkgcache.PackageOptions;
+import com.google.devtools.build.lib.runtime.ClientOptions;
+import com.google.devtools.build.lib.runtime.CommonCommandOptions;
+import com.google.devtools.build.lib.runtime.KeepGoingOption;
+import com.google.devtools.build.lib.runtime.StarlarkOptionsParser;
+import com.google.devtools.build.lib.runtime.UiOptions;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingResult;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+
+/** Helper base class for testing the use of Starlark-style flags. */
+public class StarlarkOptionsTestCase extends BuildViewTestCase {
+
+  private static final List<Class<? extends OptionsBase>> requiredOptionsClasses =
+      ImmutableList.of(
+          PackageOptions.class,
+          StarlarkSemanticsOptions.class,
+          KeepGoingOption.class,
+          LoadingOptions.class,
+          ClientOptions.class,
+          UiOptions.class,
+          CommonCommandOptions.class);
+  private StarlarkOptionsParser starlarkOptionsParser;
+
+  @Before
+  public void setUp() throws Exception {
+    optionsParser =
+        OptionsParser.builder()
+            .optionsClasses(
+                Iterables.concat(
+                    requiredOptionsClasses, ruleClassProvider.getConfigurationOptions()))
+            .build();
+    starlarkOptionsParser =
+        StarlarkOptionsParser.newStarlarkOptionsParserForTesting(
+            skyframeExecutor, reporter, PathFragment.EMPTY_FRAGMENT, optionsParser);
+  }
+
+  protected OptionsParsingResult parseStarlarkOptions(String options) throws Exception {
+    starlarkOptionsParser.setResidueForTesting(Arrays.asList(options.split(" ")));
+    starlarkOptionsParser.parse(new StoredEventHandler());
+    return starlarkOptionsParser.getNativeOptionsParserFortesting();
+  }
+
+  private void writeBuildSetting(String type, String defaultValue, boolean isFlag)
+      throws Exception {
+    String flag = isFlag ? "True" : "False";
+
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        type + "_setting = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config." + type + "(flag=" + flag + ")",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', '" + type + "_setting')",
+        type
+            + "_setting(name = 'my_"
+            + type
+            + "_setting', build_setting_default = "
+            + defaultValue
+            + ")");
+  }
+
+  protected void writeBasicIntFlag() throws Exception {
+    writeBuildSetting("int", "42", true);
+  }
+
+  protected void writeBasicBoolFlag() throws Exception {
+    writeBuildSetting("bool", "True", true);
+  }
+}