Open-source some query2 and skyframe tests

Open-source these files:
- lib/query2/LexerTest.java
- lib/query2/QueryParserTest.java
- lib/skyframe/CollectPackagesUnderDirectoryCodecTest.java
- lib/skyframe/PrepareDepsOfPatternFunctionTest.java
- lib/skyframe/PrepareDepsOfTargetsUnderDirectoryKeyCodecTest.java
- lib/skyframe/SequencedSkyframeExecutorTest.java
- lib/skyframe/SkylarkImportLookupKeyCodecTest.java
- lib/skyframe/TargetPatternSequenceCodecTest.java
- lib/skyframe/TestSuiteExpansionKeyCodecTest.java

PiperOrigin-RevId: 293569075
diff --git a/src/test/java/com/google/devtools/build/lib/query2/engine/BUILD b/src/test/java/com/google/devtools/build/lib/query2/engine/BUILD
index 64d6e6b..a38a7d4 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/engine/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/query2/engine/BUILD
@@ -31,5 +31,7 @@
         "//third_party:guava",
         "//third_party:jsr305",
         "//third_party:junit4",
+        "//third_party:mockito",
+        "//third_party:truth",
     ],
 )
diff --git a/src/test/java/com/google/devtools/build/lib/query2/engine/LexerTest.java b/src/test/java/com/google/devtools/build/lib/query2/engine/LexerTest.java
new file mode 100644
index 0000000..a764537
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/query2/engine/LexerTest.java
@@ -0,0 +1,145 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.query2.engine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the query expression lexer. */
+@RunWith(JUnit4.class)
+public final class LexerTest {
+
+  private String asString(Lexer.Token[] tokens) {
+    StringBuilder buffer = new StringBuilder();
+    for (Lexer.Token token : tokens) {
+      if (buffer.length() > 0) {
+        buffer.append(' ');
+      }
+      buffer.append(token);
+    }
+    return buffer.toString();
+  }
+
+  private Lexer.Token[] scan(String input) throws QueryException {
+    return Lexer.scan(input).toArray(new Lexer.Token[0]);
+  }
+
+  @Test
+  public void testBasics() throws QueryException {
+    assertThat(asString(scan(""))).isEqualTo("EOF");
+  }
+
+  @Test
+  public void testWordsAndKeywords() throws QueryException {
+    Lexer.Token[] tokens = scan("foo bar wiz intersect");
+    assertThat(asString(tokens)).isEqualTo("foo bar wiz intersect EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[3].kind).isEqualTo(Lexer.TokenKind.INTERSECT);
+    assertThat(tokens[4].kind).isEqualTo(Lexer.TokenKind.EOF);
+  }
+
+  @Test
+  public void testPunctuationAndWordBoundaries() throws QueryException {
+    assertThat(asString(scan("foo(bar,wiz)deps=intersect")))
+        .isEqualTo("foo ( bar , wiz ) deps = intersect EOF");
+    assertThat(asString(scan("deps(//pkg:target)"))).isEqualTo("deps ( //pkg:target ) EOF");
+  }
+
+  @Test
+  public void testWordsMayContainDashOrStarButNotStartWithThem()
+      throws QueryException {
+    assertThat(asString(scan("* foo*"))).isEqualTo("* foo* EOF");
+    assertThat(asString(scan("-foo foo-bar"))).isEqualTo("- foo foo-bar EOF");
+  }
+
+  @Test
+  public void testDotDotDot() throws QueryException {
+    assertThat(asString(scan("..."))).isEqualTo("... EOF");
+  }
+
+  @Test
+  public void testQuotation() throws QueryException {
+    Lexer.Token[] tokens = scan("foo bar 'foo bar'");
+    assertThat(asString(tokens)).isEqualTo("foo bar foo bar EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[2].word).isEqualTo("foo bar");
+  }
+
+  @Test
+  public void testQuotedWordsAreNotIdentifiers() throws QueryException {
+    Lexer.Token[] tokens = scan("set 'set' \"set\"");
+    assertThat(asString(tokens)).isEqualTo("set set set EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.SET);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+  }
+
+  @Test
+  public void testUnterminatedQuotation() {
+    QueryException e = assertThrows(QueryException.class, () -> scan("'foo"));
+    assertThat(e).hasMessageThat().isEqualTo("unclosed quotation");
+  }
+
+  @Test
+  public void testOperatorWithSpecialCharacters() throws QueryException {
+    Lexer.Token[] tokens = scan("set(//foo_bar:.*@4)");
+    assertThat(asString(tokens)).isEqualTo("set ( //foo_bar:.*@4 ) EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.SET);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.LPAREN);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[3].kind).isEqualTo(Lexer.TokenKind.RPAREN);
+  }
+
+  @Test
+  public void testOperatorWithQuotedExprWithSpecialCharacters() throws QueryException {
+    Lexer.Token[] tokens = scan("set(\"//foo_bar:.*@4\")");
+    assertThat(asString(tokens)).isEqualTo("set ( //foo_bar:.*@4 ) EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.SET);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.LPAREN);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[3].kind).isEqualTo(Lexer.TokenKind.RPAREN);
+  }
+
+  @Test
+  public void testOperatorWithQuotedExprWithMoreSpecialCharacters() throws QueryException {
+    Lexer.Token[] tokens = scan("set(\"//foo:foo=base/2~123+asd\")");
+    assertThat(asString(tokens)).isEqualTo("set ( //foo:foo=base/2~123+asd ) EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.SET);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.LPAREN);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[3].kind).isEqualTo(Lexer.TokenKind.RPAREN);
+  }
+
+  @Test
+  public void testOperatorWithUnquotedExprWithSpecialCharacters() throws QueryException {
+    Lexer.Token[] tokens = scan("set(//a:b=bar./@_:~-*$123+asd)");
+    assertThat(asString(tokens)).isEqualTo("set ( //a:b = bar./@_:~-*$123 + asd ) EOF");
+    assertThat(tokens[0].kind).isEqualTo(Lexer.TokenKind.SET);
+    assertThat(tokens[1].kind).isEqualTo(Lexer.TokenKind.LPAREN);
+    assertThat(tokens[2].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[3].kind).isEqualTo(Lexer.TokenKind.EQUALS);
+    assertThat(tokens[4].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[5].kind).isEqualTo(Lexer.TokenKind.PLUS);
+    assertThat(tokens[6].kind).isEqualTo(Lexer.TokenKind.WORD);
+    assertThat(tokens[7].kind).isEqualTo(Lexer.TokenKind.RPAREN);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/query2/engine/QueryParserTest.java b/src/test/java/com/google/devtools/build/lib/query2/engine/QueryParserTest.java
new file mode 100644
index 0000000..e7917ed9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/query2/engine/QueryParserTest.java
@@ -0,0 +1,265 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.query2.engine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryTaskFuture;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests of parser and pretty-printer. */
+@RunWith(JUnit4.class)
+public final class QueryParserTest {
+  private static class MockFunction implements QueryFunction {
+    private final String name;
+    private final int mandatoryArguments;
+    private final List<ArgumentType> arguments;
+
+    private MockFunction(String name, int mandatoryArguments, ArgumentType... arguments) {
+      this.name = name;
+      this.mandatoryArguments = mandatoryArguments;
+      this.arguments = ImmutableList.copyOf(arguments);
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public int getMandatoryArguments() {
+      return mandatoryArguments;
+    }
+
+    @Override
+    public List<ArgumentType> getArgumentTypes() {
+      return arguments;
+    }
+
+    @Override
+    public <T> QueryTaskFuture<Void> eval(
+        QueryEnvironment<T> env,
+        QueryExpressionContext<T> context,
+        QueryExpression expression,
+        List<Argument> args,
+        Callback<T> callback) {
+      throw new IllegalStateException();
+    }
+  }
+
+  private static QueryEnvironment<?> mockEnvironment() {
+    ImmutableList.Builder<QueryFunction> functions = ImmutableList.builder();
+    functions.addAll(QueryEnvironment.DEFAULT_QUERY_FUNCTIONS);
+    functions.add(new MockFunction("opt", 2,
+        ArgumentType.WORD, ArgumentType.WORD, ArgumentType.WORD));
+
+    QueryEnvironment<?> result = mock(QueryEnvironment.class);
+    when(result.getFunctions()).thenReturn(functions.build());
+    return result;
+  }
+
+  // Asserts that 'query' parses, and that when pretty-printed, yields 'query'.
+  private static String checkPrettyPrint(String query) throws Exception {
+    return checkPrettyPrint(query, query);
+  }
+
+  // Asserts that 'query' parses, and that when pretty-printed, yields
+  // 'expectedPrettyPrintOutput'.
+  private static String checkPrettyPrint(String expectedPrettyPrintOutput, String query)
+      throws Exception {
+    assertThat(QueryExpression.parse(query, mockEnvironment()).toString())
+        .isEqualTo(expectedPrettyPrintOutput);
+    return expectedPrettyPrintOutput;
+  }
+
+  public static void checkParseFails(String query, String expectedError) {
+    QueryException e =
+        assertThrows(QueryException.class, () -> QueryExpression.parse(query, mockEnvironment()));
+    assertThat(e).hasMessageThat().isEqualTo(expectedError);
+  }
+
+  @Test
+  public void testOptionalArguments() throws Exception {
+    checkPrettyPrint("opt('foo', 'bar')");
+    checkPrettyPrint("opt('foo', 'bar', 'qux')");
+    checkParseFails("opt('foo', 'bar', 'qux', 'zyc')", "syntax error at ', zyc )'");
+    checkParseFails("opt('foo')", "syntax error at ')'");
+  }
+
+  @Test
+  public void testTargetLiterals() throws Exception {
+    checkPrettyPrint("x");
+    checkPrettyPrint("//x");
+    checkPrettyPrint("//x:y");
+    checkPrettyPrint("x/...:all-targets");
+    checkPrettyPrint("\"set\""); // reserved word
+    checkPrettyPrint("\"\"");
+  }
+
+  @Test
+  public void checkParseErrors() {
+    checkParseFails("foo(a)", "syntax error at '( a )'");
+    checkParseFails("deps(", "premature end of input");
+    checkParseFails("deps(a, ", "premature end of input");
+    checkParseFails("deps(a, b, c, d)", "expected an integer literal: 'b'");
+    checkParseFails("set(a, ", "syntax error at ','");
+    checkParseFails("set(a b", "premature end of input");
+  }
+
+  @Test
+  public void testBinaryOperators() throws Exception {
+    checkParseFails("foo intersect", "premature end of input");
+
+    checkPrettyPrint("(a - b)", "a - b");
+
+    checkPrettyPrint("(a intersect b)", "a intersect b");
+    checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c");
+    checkPrettyPrint("(a union b)", "a union b");
+    checkPrettyPrint("(a union b union c)", "a union b union c");
+    checkPrettyPrint("(a except b)", "a except b");
+    checkPrettyPrint("(a except b except c)", "a except b except c");
+    checkPrettyPrint("((a union b) except c)", "a union b except c");
+    checkPrettyPrint("((a except b) union c)", "a except b union c");
+  }
+
+  @Test
+  public void testOperators() throws Exception {
+    checkPrettyPrint("some(x)");
+    checkPrettyPrint("somepath(x, y)");
+    checkPrettyPrint("allpaths(x, y)");
+    checkPrettyPrint("deps(x)");
+    checkPrettyPrint("deps(x, 1)");
+    checkPrettyPrint("rdeps(x, y)");
+    checkPrettyPrint("rdeps(x, y, 1)");
+    checkPrettyPrint("kind('rule', x)", "kind(rule, x)");
+    checkPrettyPrint("kind('source file', x)");
+    checkPrettyPrint("kind('.*', x)");
+    checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)");
+    checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)");
+    checkPrettyPrint("let x = e1 in e2");
+    checkPrettyPrint("labels('srcs', x)");
+    checkPrettyPrint("tests(x)");
+    checkPrettyPrint("set()");
+    checkPrettyPrint("set(//a)");
+    checkPrettyPrint("set(//a //b)");
+  }
+
+  @Test
+  public void testMultipleOperatorParsing() throws Exception {
+    checkPrettyPrint(checkPrettyPrint("kind('rule', x)", "kind(rule, x)"));
+    checkPrettyPrint(checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)"));
+    checkPrettyPrint(checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)"));
+  }
+
+  @Test
+  public void testMultipleBinaryOperatorParsing() throws Exception {
+    checkPrettyPrint(checkPrettyPrint("((a union b) except c)", "a union b except c"));
+    checkPrettyPrint(checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c"));
+    checkPrettyPrint(checkPrettyPrint("(a union b union c)", "a union b union c"));
+    checkPrettyPrint(
+        checkPrettyPrint(
+            "((((a union b) intersect c) except d) intersect e)",
+            "a union b intersect c except d intersect e"));
+  }
+
+  @Test
+  public void testMultipleTargetLiteralParsing() throws Exception {
+    checkPrettyPrint(checkPrettyPrint("//foo:.*@4", "\"//foo:.*@4\""));
+    checkPrettyPrint(checkPrettyPrint("set(//foo)", "set(\"//foo\")"));
+    checkPrettyPrint("\"set(//foo)\"");
+    checkPrettyPrint("\"set('//foo')\"");
+  }
+
+  @Test
+  public void testQuotedAndUnquotedMetacharacters() throws Exception {
+    checkPrettyPrint("\"//foo:xx+xx\"");
+    checkPrettyPrint(checkPrettyPrint("(//foo:xx + xx)", "//foo:xx+xx"));
+    checkPrettyPrint("\"//foo:xx=xx\"");
+    checkParseFails("//foo:xx=xx", "unexpected token '=' after query expression '//foo:xx'");
+  }
+
+  @Test
+  public void testQuotedSpecialCharacters() throws Exception {
+    checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"", "'foo[]^$asd.|asd?*+{})_asd()2'");
+    checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"");
+    checkPrettyPrint("\" #&()+,;<=>?[]{|}\"");
+  }
+
+  @Test
+  public void testUnquotedSpecialCharacters() throws Exception {
+    // All special characters in the Lexer#scanWord ./@_:~-*$
+    checkPrettyPrint("a.b");
+    checkPrettyPrint("a/b");
+    checkPrettyPrint("a@b");
+    checkPrettyPrint("a_b");
+    checkPrettyPrint("a:b");
+    checkPrettyPrint("a~b");
+    checkPrettyPrint("a-b");
+    checkPrettyPrint("a*b");
+    checkPrettyPrint("a$b");
+  }
+
+  @Test
+  public void testPreserveQuoting() throws Exception {
+    checkPrettyPrint(checkPrettyPrint("\"a+b\""));
+    // this should preserve quoting without being quoted in TargetLiteral#toString
+    checkPrettyPrint(checkPrettyPrint("aaa", "\"aaa\""));
+  }
+
+  @Test
+  public void testQuotedIllegalCharacters() throws Exception {
+    checkParseFails("\"-x\"", "target literal must not begin with (-): -x");
+    checkParseFails("\"*x\"", "target literal must not begin with (*): *x");
+  }
+
+  @Test
+  public void testIllegalQuoting() throws Exception {
+    checkParseFails("\"a", "unclosed quotation");
+    checkParseFails("\'a", "unclosed quotation");
+    checkParseFails("a\"a", "unclosed quotation");
+    checkParseFails("a\'a", "unclosed quotation");
+    checkParseFails("a\'\"a", "unclosed quotation");
+    checkParseFails("a\"\'a", "unclosed quotation");
+    checkParseFails("\'a\"\'a\'", "unclosed quotation");
+    checkParseFails("\"a\'\"a\"", "unclosed quotation");
+    checkParseFails(
+        "\'\"a\" + \'a\'\'", "unexpected token 'a' after query expression ''\"a\" + ''");
+    checkParseFails(
+        "\"\'a\' + \"a\"\"", "unexpected token 'a' after query expression '\"'a' + \"'");
+    checkParseFails(
+        "\"set(\"//foo\" + \"bar\")\"",
+        "unexpected token '//foo' after query expression '\"set(\"'");
+    checkParseFails(
+        "'set('//foo' + 'bar')'", "unexpected token '//foo' after query expression '\"set(\"'");
+  }
+
+  @Test
+  public void testUsingCorrectQuotingInTargetLiteralToString() throws Exception {
+    // These tests all fall into the needsQuoting == true use case in TargetLiteral#toString
+    checkPrettyPrint("'set(\"//foo\" + \"bar\")'");
+    checkPrettyPrint("\"set('//foo' + 'bar')\"");
+    checkPrettyPrint("\"a'a\"");
+    checkPrettyPrint("\'a\"a\'");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index bb45e60..ad6db15 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -111,6 +111,7 @@
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/protobuf:action_cache_java_proto",
+        "//src/main/protobuf:analysis_java_proto",
         "//src/test/java/com/google/devtools/build/lib:actions_testutil",
         "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
         "//src/test/java/com/google/devtools/build/lib:default_test_build_rules",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryCodecTest.java
new file mode 100644
index 0000000..0472323
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/CollectPackagesUnderDirectoryCodecTest.java
@@ -0,0 +1,68 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.skyframe.CollectPackagesUnderDirectoryValue.NoErrorCollectPackagesUnderDirectoryValue;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.TestUtils;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for codec for {@link CollectPackagesUnderDirectoryValue}. */
+@RunWith(JUnit4.class)
+public final class CollectPackagesUnderDirectoryCodecTest {
+
+  @Test
+  public void testCodec() throws Exception {
+    new SerializationTester(
+            NoErrorCollectPackagesUnderDirectoryValue.EMPTY,
+            CollectPackagesUnderDirectoryValue.ofNoError(
+                true,
+                ImmutableMap.of(
+                    rootedPath("/a", "b"), true,
+                    rootedPath("/c", "d"), false)),
+            CollectPackagesUnderDirectoryValue.ofNoError(
+                false,
+                ImmutableMap.of(
+                    rootedPath("/a", "b"), false,
+                    rootedPath("/c", "d"), true)),
+            CollectPackagesUnderDirectoryValue.ofError(
+                "my error message",
+                ImmutableMap.of(
+                    rootedPath("/a", "b"), false,
+                    rootedPath("/c", "d"), true)))
+        .addDependency(FileSystem.class, FsUtils.TEST_FILESYSTEM)
+        .runTests();
+  }
+
+  @Test
+  public void testEmptyDeserializesToSingletonValue() throws Exception {
+    assertThat(TestUtils.roundTrip(NoErrorCollectPackagesUnderDirectoryValue.EMPTY))
+        .isSameInstanceAs(NoErrorCollectPackagesUnderDirectoryValue.EMPTY);
+  }
+
+  private static RootedPath rootedPath(String root, String relativePath) {
+    return RootedPath.toRootedPath(
+        Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath(root)), PathFragment.create(relativePath));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunctionTest.java
new file mode 100644
index 0000000..b8ef67d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternFunctionTest.java
@@ -0,0 +1,142 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.skyframe.WalkableGraphUtils.exists;
+
+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.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.skyframe.PrepareDepsOfPatternValue.PrepareDepsOfPatternSkyKeysAndExceptions;
+import com.google.devtools.build.skyframe.EvaluationContext;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.WalkableGraph;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PrepareDepsOfTargetsUnderDirectoryFunction}. */
+@RunWith(JUnit4.class)
+public final class PrepareDepsOfPatternFunctionTest extends BuildViewTestCase {
+
+  private PrepareDepsOfPatternSkyKeysAndExceptions createPrepDepsKeysMaybe(
+      ImmutableList<String> patterns) {
+    return PrepareDepsOfPatternValue.keys(patterns, "");
+  }
+
+  private SkyKey createPrepDepsKey(String pattern) {
+    PrepareDepsOfPatternSkyKeysAndExceptions keysAndExceptions =
+        PrepareDepsOfPatternValue.keys(ImmutableList.of(pattern), "");
+    assertThat(keysAndExceptions.getExceptions()).isEmpty();
+    return Iterables.getOnlyElement(keysAndExceptions.getValues()).getSkyKey();
+  }
+
+  private EvaluationResult<PrepareDepsOfPatternValue> getEvaluationResult(SkyKey key)
+      throws InterruptedException {
+    EvaluationContext evaluationContext =
+        EvaluationContext.newBuilder()
+            .setKeepGoing(false)
+            .setNumThreads(SequencedSkyframeExecutor.DEFAULT_THREAD_COUNT)
+            .setEventHander(reporter)
+            .build();
+    EvaluationResult<PrepareDepsOfPatternValue> evaluationResult =
+        skyframeExecutor.getDriver().evaluate(ImmutableList.of(key), evaluationContext);
+    Preconditions.checkState(!evaluationResult.hasError());
+    return evaluationResult;
+  }
+
+  @Test
+  public void testUnparsablePattern() {
+    // Given an string that can't be parsed,
+    String unparsablePattern = "Not a//parsable/.../pattern/..//";
+    ImmutableList<String> unparsablePatternList = ImmutableList.of(unparsablePattern);
+
+    // When PrepareDepsOfPatternValue.keys is called with that string as an argument,
+    PrepareDepsOfPatternSkyKeysAndExceptions keysAndExceptionsResult =
+        createPrepDepsKeysMaybe(unparsablePatternList);
+
+    // Then it returns a wrapped TargetParsingException.
+    assertThat(keysAndExceptionsResult.getValues()).isEmpty();
+    assertThat(
+        Iterables.getOnlyElement(keysAndExceptionsResult.getExceptions()).getOriginalPattern())
+        .isEqualTo(unparsablePattern);
+  }
+
+  @Test
+  public void testSingleTargetPatternEvaluationAndTransitiveLoading() throws Exception {
+    evaluatePatternAndCheckTransitiveLoading("//a", /*adExists=*/ false);
+  }
+
+  @Test
+  public void testTargetsBelowDirectoryPatternEvaluationAndTransitiveLoading() throws Exception {
+    evaluatePatternAndCheckTransitiveLoading("//a/...", /*adExists=*/ true);
+  }
+
+  private void evaluatePatternAndCheckTransitiveLoading(String pattern, boolean adExists)
+      throws IOException, InterruptedException, LabelSyntaxException {
+    // Given a package "a" with a genrule "a" that depends on a target "b.txt" in a created
+    // package "b", and a package "c" with a genrule "c", and a package "a/d" with a genrule "d".
+    createPackages();
+
+    // When PrepareDepsOfPatternFunction is evaluated for the provided pattern,
+    SkyKey key = createPrepDepsKey(pattern);
+    EvaluationResult<PrepareDepsOfPatternValue> evaluationResult =
+        getEvaluationResult(key);
+    WalkableGraph graph = Preconditions.checkNotNull(evaluationResult.getWalkableGraph());
+
+    // Then the result is not null,
+    Preconditions.checkNotNull(evaluationResult.get(key));
+
+    // And the TransitiveTraversalValue for "a:a" is evaluated,
+    SkyKey aaKey = TransitiveTraversalValue.key(Label.parseAbsolute("@//a:a", ImmutableMap.of()));
+    assertThat(exists(aaKey, graph)).isTrue();
+
+    // And that TransitiveTraversalValue depends on "b:b.txt".
+    Iterable<SkyKey> depsOfAa =
+        Iterables.getOnlyElement(graph.getDirectDeps(ImmutableList.of(aaKey)).values());
+    SkyKey bTxtKey =
+        TransitiveTraversalValue.key(Label.parseAbsolute("@//b:b.txt", ImmutableMap.of()));
+    assertThat(depsOfAa).contains(bTxtKey);
+
+    // And the TransitiveTraversalValue for "b:b.txt" is evaluated.
+    assertThat(exists(bTxtKey, graph)).isTrue();
+
+    // And the TransitiveTraversalValue for "c:c" is NOT evaluated.
+    SkyKey ccKey = TransitiveTraversalValue.key(Label.parseAbsolute("@//c:c", ImmutableMap.of()));
+    assertThat(exists(ccKey, graph)).isFalse();
+
+    // And the TransitiveTraversalValue for "a/d:d" is or is not evaluated depending on the provided
+    // expectation.
+    SkyKey adKey = TransitiveTraversalValue.key(Label.parseAbsolute("@//a/d:d", ImmutableMap.of()));
+    assertThat(exists(adKey, graph)).isEqualTo(adExists);
+  }
+
+  /**
+   * Creates a package "a" with a genrule "a" that depends on a target "b.txt" in a created
+   * package "b", and a package "c" with a genrule "c", and a package "a/d" with a genrule "d".
+   */
+  private void createPackages() throws IOException {
+    scratch.file("a/BUILD", "genrule(name='a', cmd='', srcs=['//b:b.txt'], outs=['a.out'])");
+    scratch.file("b/BUILD", "exports_files(['b.txt'])");
+    scratch.file("c/BUILD", "genrule(name='c', cmd='', srcs=['c.txt'], outs=['c.out'])");
+    scratch.file("a/d/BUILD", "genrule(name='d', cmd='', srcs=['d.txt'], outs=['d.out'])");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryKeyCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryKeyCodecTest.java
new file mode 100644
index 0000000..2c80f1d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfTargetsUnderDirectoryKeyCodecTest.java
@@ -0,0 +1,43 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.skyframe.PrepareDepsOfTargetsUnderDirectoryValue.PrepareDepsOfTargetsUnderDirectoryKey;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PrepareDepsOfTargetsUnderDirectoryKey}'s codec. */
+@RunWith(JUnit4.class)
+public final class PrepareDepsOfTargetsUnderDirectoryKeyCodecTest {
+
+  @Test
+  public void testCodec() throws Exception {
+    new SerializationTester(
+            PrepareDepsOfTargetsUnderDirectoryKey.create(
+                new RecursivePkgKey(
+                    RepositoryName.MAIN,
+                    FsUtils.TEST_ROOT,
+                    ImmutableSet.of(FsUtils.rootPathRelative("here"))),
+                FilteringPolicies.and(FilteringPolicies.NO_FILTER, FilteringPolicies.FILTER_TESTS)))
+        .addDependency(FileSystem.class, FsUtils.TEST_FILESYSTEM)
+        .runTests();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorTest.java
new file mode 100644
index 0000000..3640228
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorTest.java
@@ -0,0 +1,2373 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.actions.util.ActionCacheTestHelper.AMNESIAC_CACHE;
+import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertContainsEventRegex;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertEventCount;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertNotContainsEventRegex;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.eventbus.EventBus;
+import com.google.common.hash.HashCode;
+import com.google.common.testing.GcFinalization;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata.MiddlemanType;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
+import com.google.devtools.build.lib.actions.ActionKeyContext;
+import com.google.devtools.build.lib.actions.ActionLookupData;
+import com.google.devtools.build.lib.actions.ActionLookupValue;
+import com.google.devtools.build.lib.actions.ActionLookupValue.ActionLookupKey;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.ActionResult;
+import com.google.devtools.build.lib.actions.ActionTemplate;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Actions.GeneratingActions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
+import com.google.devtools.build.lib.actions.FileStateValue;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.util.DummyExecutor;
+import com.google.devtools.build.lib.actions.util.InjectedActionLookupKey;
+import com.google.devtools.build.lib.actions.util.TestAction;
+import com.google.devtools.build.lib.actions.util.TestAction.DummyAction;
+import com.google.devtools.build.lib.analysis.AnalysisOptions;
+import com.google.devtools.build.lib.analysis.AnalysisProtos;
+import com.google.devtools.build.lib.analysis.AnalysisProtos.ActionGraphContainer;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.OutputGroupInfo;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
+import com.google.devtools.build.lib.buildtool.SkyframeBuilder;
+import com.google.devtools.build.lib.clock.BlazeClock;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventCollector;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.BinTools;
+import com.google.devtools.build.lib.packages.Info;
+import com.google.devtools.build.lib.packages.NativeProvider;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Provider;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
+import com.google.devtools.build.lib.runtime.KeepGoingOption;
+import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey;
+import com.google.devtools.build.lib.skyframe.DirtinessCheckerUtils.BasicFilesystemDirtinessChecker;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ActionCompletedReceiver;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ProgressSupplier;
+import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.StarlarkSemantics;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.DeterministicHelper;
+import com.google.devtools.build.skyframe.Differencer.Diff;
+import com.google.devtools.build.skyframe.EvaluationContext;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.NotifyingHelper;
+import com.google.devtools.build.skyframe.NotifyingHelper.EventType;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.TaggedEvents;
+import com.google.devtools.build.skyframe.TrackingAwaiter;
+import com.google.devtools.build.skyframe.ValueWithMetadata;
+import com.google.devtools.common.options.OptionsParser;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link SequencedSkyframeExecutor}. */
+@RunWith(JUnit4.class)
+public final class SequencedSkyframeExecutorTest extends BuildViewTestCase {
+  private TransitivePackageLoader visitor;
+  private OptionsParser options;
+
+  @Before
+  public final void createSkyframeExecutorAndVisitor() throws Exception {
+    skyframeExecutor = getSkyframeExecutor();
+    skyframeExecutor.setRemoteOutputsMode(RemoteOutputsMode.ALL);
+    visitor = skyframeExecutor.pkgLoader();
+    options =
+        OptionsParser.builder()
+            .optionsClasses(
+                ImmutableList.of(
+                    KeepGoingOption.class, BuildRequestOptions.class, AnalysisOptions.class))
+            .build();
+    options.parse("--jobs=20");
+  }
+
+  @Test
+  public void testChangeFile() throws Exception {
+    analysisMock.pySupport().setup(mockToolsConfig);
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter, ModifiedFileSet.EVERYTHING_MODIFIED, Root.fromPath(rootDirectory));
+
+    String pathString = rootDirectory + "/python/hello/BUILD";
+    scratch.file(pathString, "py_binary(name = 'hello', srcs = ['hello.py'])");
+
+    // A dummy file that is never changed.
+    scratch.file(rootDirectory + "/misc/BUILD", "sh_binary(name = 'misc', srcs = ['hello.sh'])");
+
+    sync("//python/hello:hello", "//misc:misc");
+
+    // No changes yet.
+    assertThat(dirtyValues()).isEmpty();
+
+    // Make a change.
+    scratch.overwriteFile(pathString, "py_binary(name = 'hello', srcs = ['something_else.py'])");
+    assertThat(dirtyValues())
+        .containsExactly(
+            FileStateValue.key(
+                RootedPath.toRootedPath(
+                    Root.fromPath(rootDirectory), PathFragment.create("python/hello/BUILD"))));
+
+    // The method will continue returning the value until we invalidate it and re-evaluate.
+    assertThat(dirtyValues()).hasSize(1);
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter,
+        ModifiedFileSet.builder().modify(PathFragment.create("python/hello/BUILD")).build(),
+        Root.fromPath(rootDirectory));
+    sync("//python/hello:hello");
+    assertThat(dirtyValues()).isEmpty();
+  }
+
+  // Regression for b/13328517. clearAnalysisCache() method is call when --discard_analysis_cache
+  // is used. This saves about 10% of the memory during execution.
+  @Test
+  public void testClearAnalysisCache() throws Exception {
+    scratch.file(rootDirectory + "/discard/BUILD",
+        "genrule(name='x', srcs=['input'], outs=['out'], cmd='false')");
+    scratch.file(rootDirectory + "/discard/input", "foo");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter,
+            Label.parseAbsolute("@//discard:x", ImmutableMap.of()),
+            getTargetConfiguration());
+    assertThat(ct).isNotNull();
+    WeakReference<ConfiguredTarget> ref = new WeakReference<>(ct);
+    ct = null;
+    // Allow all values to be cleared by passing in empty set of top-level values, since we're not
+    // actually building.
+    skyframeExecutor.clearAnalysisCache(
+        ImmutableSet.<ConfiguredTarget>of(), ImmutableSet.<AspectValue>of());
+    GcFinalization.awaitClear(ref);
+  }
+
+  @Test
+  public void testChangeDirectory() throws Exception {
+    analysisMock.pySupport().setup(mockToolsConfig);
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter, ModifiedFileSet.EVERYTHING_MODIFIED, Root.fromPath(rootDirectory));
+
+    scratch.file("python/hello/BUILD",
+        "py_binary(name = 'hello', srcs = ['hello.py'], data = glob(['*.txt']))");
+    scratch.file("python/hello/foo.txt", "foo");
+
+    // A dummy directory that is not changed.
+    scratch.file("misc/BUILD",
+        "py_binary(name = 'misc', srcs = ['other.py'], data = glob(['*.txt']))");
+
+    sync("//python/hello:hello", "//misc:misc");
+
+    // No changes yet.
+    assertThat(dirtyValues()).isEmpty();
+
+    // Make a change.
+    scratch.file("python/hello/bar.txt", "bar");
+    assertThat(dirtyValues())
+        .containsExactly(
+            DirectoryListingStateValue.key(
+                RootedPath.toRootedPath(
+                    Root.fromPath(rootDirectory), PathFragment.create("python/hello"))));
+
+    // The method will continue returning the value until we invalidate it and re-evaluate.
+    assertThat(dirtyValues()).hasSize(1);
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter,
+        ModifiedFileSet.builder().modify(PathFragment.create("python/hello/bar.txt")).build(),
+        Root.fromPath(rootDirectory));
+    sync("//python/hello:hello");
+    assertThat(dirtyValues()).isEmpty();
+  }
+
+  @Test
+  public void testSetDeletedPackages() throws Exception {
+    ExtendedEventHandler eventHandler = NullEventHandler.INSTANCE;
+    scratch.file("foo/bar/BUILD", "cc_library(name = 'bar', hdrs = ['bar.h'])");
+    scratch.file("foo/baz/BUILD", "cc_library(name = 'baz', hdrs = ['baz.h'])");
+
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .isPackage(eventHandler, PackageIdentifier.createInMainRepo("foo/bar")))
+        .isTrue();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .getBuildFileForPackage(PackageIdentifier.createInMainRepo("foo/bar")))
+        .isNotNull();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .isPackage(eventHandler, PackageIdentifier.createInMainRepo("foo/baz")))
+        .isTrue();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .getBuildFileForPackage(PackageIdentifier.createInMainRepo("foo/baz")))
+        .isNotNull();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .isPackage(eventHandler, PackageIdentifier.createInMainRepo("not/a/package")))
+        .isFalse();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .getBuildFileForPackage(PackageIdentifier.createInMainRepo("not/a/package")))
+        .isNull();
+
+    skyframeExecutor.getPackageManager().getPackage(
+        eventHandler, PackageIdentifier.createInMainRepo("foo/bar"));
+    skyframeExecutor.getPackageManager().getPackage(
+        eventHandler, PackageIdentifier.createInMainRepo("foo/baz"));
+
+    assertThrows(
+        "non-existent package was incorrectly thought to exist",
+        NoSuchPackageException.class,
+        () ->
+            skyframeExecutor
+                .getPackageManager()
+                .getPackage(eventHandler, PackageIdentifier.createInMainRepo("not/a/package")));
+
+    ImmutableSet<PackageIdentifier> deletedPackages = ImmutableSet.of(
+        PackageIdentifier.createInMainRepo("foo/bar"));
+    skyframeExecutor.setDeletedPackages(deletedPackages);
+
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .isPackage(eventHandler, PackageIdentifier.createInMainRepo("foo/bar")))
+        .isFalse();
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .getBuildFileForPackage(PackageIdentifier.createInMainRepo("foo/bar")))
+        .isNull();
+    assertThrows(
+        "deleted package was incorrectly thought to exist",
+        NoSuchPackageException.class,
+        () ->
+            skyframeExecutor
+                .getPackageManager()
+                .getPackage(eventHandler, PackageIdentifier.createInMainRepo("foo/bar")));
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .isPackage(eventHandler, PackageIdentifier.createInMainRepo("foo/baz")))
+        .isTrue();
+  }
+
+  // Directly tests that PackageFunction adds a dependency on the PackageLookupValue for
+  // (potential) subpackages. This is tested indirectly in several places (e.g.
+  // LabelVisitorTest#testSubpackageBoundaryAdd and
+  // PackageDeletionTest#testUnsuccessfulBuildAfterDeletion) but those tests are also indirectly
+  // testing the behavior of TargetFunction when the target has a '/'.
+  @Test
+  public void testDependencyOnPotentialSubpackages() throws Exception {
+    ExtendedEventHandler eventHandler = NullEventHandler.INSTANCE;
+    scratch.file("x/BUILD",
+        "sh_library(name = 'x', deps = ['//x:y/z'])",
+        "sh_library(name = 'y/z')");
+
+    Package pkgBefore = skyframeExecutor.getPackageManager().getPackage(
+        eventHandler, PackageIdentifier.createInMainRepo("x"));
+    assertThat(pkgBefore.containsErrors()).isFalse();
+
+    scratch.file("x/y/BUILD",
+        "sh_library(name = 'z')");
+    ModifiedFileSet modifiedFiles = ModifiedFileSet.builder()
+        .modify(PathFragment.create("x"))
+        .modify(PathFragment.create("x/y"))
+        .modify(PathFragment.create("x/y/BUILD"))
+        .build();
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter, modifiedFiles, Root.fromPath(rootDirectory));
+
+    // The package lookup for "x" should now fail because it's invalid.
+    reporter.removeHandler(failFastHandler); // expect errors
+    assertThat(
+            skyframeExecutor
+                .getPackageManager()
+                .getPackage(eventHandler, PackageIdentifier.createInMainRepo("x"))
+                .containsErrors())
+        .isTrue();
+
+    scratch.deleteFile("x/y/BUILD");
+    skyframeExecutor.invalidateFilesUnderPathForTesting(
+        reporter, modifiedFiles, Root.fromPath(rootDirectory));
+
+    // The package lookup for "x" should now succeed again.
+    reporter.addHandler(failFastHandler); // no longer expect errors
+    Package pkgAfter = skyframeExecutor.getPackageManager().getPackage(
+        eventHandler, PackageIdentifier.createInMainRepo("x"));
+    assertThat(pkgAfter).isNotSameInstanceAs(pkgBefore);
+  }
+
+  @Test
+  public void testSkyframePackageManagerGetBuildFileForPackage() throws Exception {
+    PackageManager skyframePackageManager = skyframeExecutor.getPackageManager();
+
+    scratch.file("nobuildfile/foo.txt");
+    scratch.file("deletedpackage/BUILD");
+    skyframeExecutor.setDeletedPackages(ImmutableList.of(
+        PackageIdentifier.createInMainRepo("deletedpackage")));
+    scratch.file("invalidpackagename.42/BUILD");
+    Path everythingGoodBuildFilePath = scratch.file("everythinggood/BUILD");
+
+    assertThat(
+            skyframePackageManager.getBuildFileForPackage(
+                PackageIdentifier.createInMainRepo("nobuildfile")))
+        .isNull();
+    assertThat(
+            skyframePackageManager.getBuildFileForPackage(
+                PackageIdentifier.createInMainRepo("deletedpackage")))
+        .isNull();
+    assertThat(
+            skyframePackageManager.getBuildFileForPackage(
+                PackageIdentifier.createInMainRepo("everythinggood")))
+        .isEqualTo(everythingGoodBuildFilePath);
+  }
+
+  /**
+   * Indirect regression test for b/12543229: "The Skyframe error propagation model is
+   * problematic".
+   */
+  @Test
+  public void testPackageFunctionHandlesExceptionFromDependencies() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    Path badDirPath = scratch.dir("bad/dir");
+    // This will cause an IOException when trying to compute the glob, which is required to load
+    // the package.
+    badDirPath.setReadable(false);
+    scratch.file("bad/BUILD",
+        "filegroup(name='fg', srcs=glob(['**']))");
+    assertThrows(
+        NoSuchPackageException.class,
+        () ->
+            skyframeExecutor
+                .getPackageManager()
+                .getPackage(reporter, PackageIdentifier.createInMainRepo("bad")));
+  }
+
+  private Collection<SkyKey> dirtyValues() throws InterruptedException {
+    Diff diff =
+        new FilesystemValueChecker(
+                new TimestampGranularityMonitor(BlazeClock.instance()),
+                null)
+            .getDirtyKeys(skyframeExecutor.getEvaluatorForTesting().getValues(),
+                new BasicFilesystemDirtinessChecker());
+    return ImmutableList.<SkyKey>builder()
+        .addAll(diff.changedKeysWithoutNewValues())
+        .addAll(diff.changedKeysWithNewValues().keySet())
+        .build();
+  }
+
+  private void sync(String... labelStrings) throws Exception {
+    Set<Label> labels = new HashSet<>();
+    for (String labelString : labelStrings) {
+      labels.add(Label.parseAbsolute(labelString, ImmutableMap.of()));
+    }
+    visitor.sync(reporter, labels, /*keepGoing=*/ false, /*parallelThreads=*/ 200);
+  }
+
+  @Test
+  public void testInterruptLoadedTarget() throws Exception {
+    analysisMock.pySupport().setup(mockToolsConfig);
+    scratch.file("python/hello/BUILD",
+        "py_binary(name = 'hello', srcs = ['hello.py'], data = glob(['*.txt']))");
+    Thread.currentThread().interrupt();
+    LoadedPackageProvider packageProvider =
+        new LoadedPackageProvider(skyframeExecutor.getPackageManager(), reporter);
+    assertThrows(
+        InterruptedException.class,
+        () ->
+            packageProvider.getLoadedTarget(Label.parseAbsoluteUnchecked("//python/hello:hello")));
+    Target target = packageProvider.getLoadedTarget(
+        Label.parseAbsoluteUnchecked("//python/hello:hello"));
+    assertThat(target).isNotNull();
+  }
+
+  /**
+   * Generating the same output from two targets is ok if we build them on successive builds
+   * and invalidate the first target before we build the second target. This test is basically
+   * copied here from {@code AnalysisCachingTest} because here we can control the number of Skyframe
+   * update calls that we make. This prevents an intermediate update call from clearing the action
+   * and hiding the bug.
+   */
+  @Test
+  public void testNoActionConflictWithInvalidatedTarget() throws Exception {
+    scratch.file(
+        "conflict/BUILD",
+        "cc_library(name='x', srcs=['foo.cc'])",
+        "cc_binary(name='_objs/x/foo.o', srcs=['bar.cc'])");
+    ConfiguredTargetAndData conflict =
+        skyframeExecutor.getConfiguredTargetAndDataForTesting(
+            reporter,
+            Label.parseAbsolute("@//conflict:x", ImmutableMap.of()),
+            getTargetConfiguration());
+    assertThat(conflict).isNotNull();
+    ArtifactRoot root =
+        getTargetConfiguration()
+            .getBinDirectory(
+                conflict.getConfiguredTarget().getLabel().getPackageIdentifier().getRepository());
+
+    Action oldAction =
+        getGeneratingAction(
+            getDerivedArtifact(
+                PathFragment.create("conflict/_objs/x/foo.o"),
+                root,
+                ConfiguredTargetKey.of(
+                    conflict.getConfiguredTarget(), conflict.getConfiguration())));
+    assertThat(oldAction.getOwner().getLabel().toString()).isEqualTo("//conflict:x");
+    skyframeExecutor.handleAnalysisInvalidatingChange();
+    ConfiguredTargetAndData objsConflict =
+        skyframeExecutor.getConfiguredTargetAndDataForTesting(
+            reporter,
+            Label.parseAbsolute("@//conflict:_objs/x/foo.o", ImmutableMap.of()),
+            getTargetConfiguration());
+    assertThat(objsConflict).isNotNull();
+    Action newAction =
+        getGeneratingAction(
+            getDerivedArtifact(
+                PathFragment.create("conflict/_objs/x/foo.o"),
+                root,
+                ConfiguredTargetKey.of(
+                    objsConflict.getConfiguredTarget(), objsConflict.getConfiguration())));
+    assertThat(newAction.getOwner().getLabel().toString()).isEqualTo("//conflict:_objs/x/foo.o");
+  }
+
+  @Test
+  public void testGetPackageUsesListener() throws Exception {
+    scratch.file("pkg/BUILD", "thisisanerror");
+    EventCollector customEventCollector = new EventCollector(EventKind.ERRORS);
+    Package pkg = skyframeExecutor.getPackageManager().getPackage(
+        new Reporter(new EventBus(), customEventCollector),
+        PackageIdentifier.createInMainRepo("pkg"));
+    assertThat(pkg.containsErrors()).isTrue();
+    MoreAsserts.assertContainsEvent(customEventCollector, "name 'thisisanerror' is not defined");
+  }
+
+  /** Dummy action that does not create its lone output file. */
+  private static class MissingOutputAction extends DummyAction {
+    MissingOutputAction(NestedSet<Artifact> inputs, Artifact output, MiddlemanType type) {
+      super(inputs, output, type);
+    }
+
+    @Override
+    public ActionResult execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      ActionResult actionResult = super.execute(actionExecutionContext);
+      try {
+        getPrimaryOutput().getPath().deleteTree();
+      } catch (IOException e) {
+        throw new AssertionError(e);
+      }
+      return actionResult;
+    }
+  }
+
+  private static final ActionCacheChecker NULL_CHECKER =
+      new ActionCacheChecker(
+          AMNESIAC_CACHE,
+          new ArtifactResolver() {
+            @Override
+            public Artifact getSourceArtifact(
+                PathFragment execPath, Root root, ArtifactOwner owner) {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Artifact getSourceArtifact(PathFragment execPath, Root root) {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Artifact resolveSourceArtifact(
+                PathFragment execPath, RepositoryName repositoryName) {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Map<PathFragment, Artifact> resolveSourceArtifacts(
+                Iterable<PathFragment> execPaths, PackageRootResolver resolver) {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Path getPathFromSourceExecPath(Path execRoot, PathFragment execPath) {
+              throw new UnsupportedOperationException();
+            }
+          },
+          new ActionKeyContext(),
+          Predicates.<Action>alwaysTrue(),
+          null);
+
+  private static final ProgressSupplier EMPTY_PROGRESS_SUPPLIER = new ProgressSupplier() {
+    @Override
+    public String getProgressString() {
+      return "";
+    }
+  };
+
+  private static final ActionCompletedReceiver EMPTY_COMPLETION_RECEIVER =
+      new ActionCompletedReceiver() {
+        @Override
+        public void actionCompleted(ActionLookupData actionLookupData) {}
+
+        @Override
+        public void noteActionEvaluationStarted(ActionLookupData actionLookupData, Action action) {}
+      };
+
+  private EvaluationResult<FileArtifactValue> evaluate(Iterable<? extends SkyKey> roots)
+      throws InterruptedException {
+    EvaluationContext evaluationContext =
+        EvaluationContext.newBuilder()
+            .setKeepGoing(false)
+            .setNumThreads(SequencedSkyframeExecutor.DEFAULT_THREAD_COUNT)
+            .setEventHander(reporter)
+            .build();
+    return skyframeExecutor.getDriver().evaluate(roots, evaluationContext);
+  }
+
+  /**
+   * Make sure that if a shared action fails to create an output file, the other action doesn't
+   * complain about it too.
+   */
+  @Test
+  public void testSharedActionsNoOutputs() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("missing");
+    // We create two "configured targets" and two copies of the same artifact, each generated by
+    // an action from its respective configured target.
+    ActionLookupValue.ActionLookupKey lc1 = new InjectedActionLookupKey("lc1");
+    Artifact output1 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, lc1);
+    Action action1 =
+        new MissingOutputAction(
+            NestedSetBuilder.emptySet(Order.STABLE_ORDER), output1, MiddlemanType.NORMAL);
+    ConfiguredTargetValue ctValue1 = createConfiguredTargetValue(action1, lc1);
+    ActionLookupValue.ActionLookupKey lc2 = new InjectedActionLookupKey("lc2");
+    Artifact output2 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, lc2);
+    Action action2 =
+        new MissingOutputAction(
+            NestedSetBuilder.emptySet(Order.STABLE_ORDER), output2, MiddlemanType.NORMAL);
+    ConfiguredTargetValue ctValue2 = createConfiguredTargetValue(action2, lc2);
+    // Inject the "configured targets" into the graph.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(lc1, ctValue1, lc2, ctValue2));
+    // Do a null build, so that the skyframe executor initializes the action executor properly.
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER, ActionExecutionStatusReporter.create(reporter));
+    skyframeExecutor.buildArtifacts(
+        reporter,
+        ResourceManager.instanceForTestingOnly(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.<Artifact>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<AspectValue>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        options,
+        NULL_CHECKER,
+        null,
+        null,
+        null);
+
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    skyframeExecutor.prepareBuildingForTestingOnly(
+        reporter, new DummyExecutor(fileSystem, rootDirectory), options, NULL_CHECKER, null);
+    EvaluationResult<FileArtifactValue> result = evaluate(ImmutableList.of(output1, output2));
+    assertWithMessage(result.toString()).that(result.keyNames()).isEmpty();
+    assertThat(result.hasError()).isTrue();
+    MoreAsserts.assertContainsEvent(eventCollector,
+        "output '" + output1.prettyPrint() + "' was not created");
+    MoreAsserts.assertContainsEvent(eventCollector, "not all outputs were created or valid");
+    assertEventCount(2, eventCollector);
+  }
+
+  /** Shared actions can race and both check the action cache and try to execute. */
+  @Test
+  public void testSharedActionsRacing() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("file");
+    Path sourcePath = rootDirectory.getRelative("foo/src");
+    FileSystemUtils.createDirectoryAndParents(sourcePath.getParentDirectory());
+    FileSystemUtils.createEmptyFile(sourcePath);
+
+    // We create two "configured targets" and two copies of the same artifact, each generated by
+    // an action from its respective configured target. Both actions will consume the input file
+    // "out/input" so we can synchronize their execution.
+    ActionLookupValue.ActionLookupKey inputKey = new InjectedActionLookupKey("input");
+    Artifact input =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            PathFragment.create("out").getRelative("input"),
+            inputKey);
+    Action baseAction =
+        new DummyAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), input, MiddlemanType.NORMAL);
+    ConfiguredTargetValue ctBase = createConfiguredTargetValue(baseAction, inputKey);
+    ActionLookupValue.ActionLookupKey lc1 = new InjectedActionLookupKey("lc1");
+    Artifact output1 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, lc1);
+    Action action1 =
+        new DummyAction(
+            NestedSetBuilder.create(Order.STABLE_ORDER, input), output1, MiddlemanType.NORMAL);
+    ConfiguredTargetValue ctValue1 = createConfiguredTargetValue(action1, lc1);
+    ActionLookupValue.ActionLookupKey lc2 = new InjectedActionLookupKey("lc2");
+    Artifact output2 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), execPath, lc2);
+    Action action2 =
+        new DummyAction(
+            NestedSetBuilder.create(Order.STABLE_ORDER, input), output2, MiddlemanType.NORMAL);
+    ConfiguredTargetValue ctValue2 = createConfiguredTargetValue(action2, lc2);
+
+    // Stall both actions during the "checking inputs" phase so that neither will enter
+    // SkyframeActionExecutor before both have asked SkyframeActionExecutor if another shared action
+    // is running. This way, both actions will check the action cache beforehand and try to update
+    // the action cache post-build.
+    final CountDownLatch inputsRequested = new CountDownLatch(2);
+    skyframeExecutor
+        .getEvaluatorForTesting()
+        .injectGraphTransformerForTesting(
+            NotifyingHelper.makeNotifyingTransformer(
+                (key, type, order, context) -> {
+                  if (type == EventType.GET_VALUE_WITH_METADATA
+                      && key.functionName().equals(Artifact.ARTIFACT)
+                      && input.equals(key)) {
+                    inputsRequested.countDown();
+                    try {
+                      assertThat(
+                              inputsRequested.await(
+                                  TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+                          .isTrue();
+                    } catch (InterruptedException e) {
+                      throw new IllegalStateException(e);
+                    }
+                  }
+                }));
+
+    // Inject the "configured targets" and artifact into the graph.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(lc1, ctValue1, lc2, ctValue2, inputKey, ctBase));
+    // Do a null build, so that the skyframe executor initializes the action executor properly.
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER, ActionExecutionStatusReporter.create(reporter));
+    skyframeExecutor.buildArtifacts(
+        reporter,
+        ResourceManager.instanceForTestingOnly(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.<Artifact>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<AspectValue>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        options,
+        NULL_CHECKER,
+        null,
+        null,
+        null);
+
+    skyframeExecutor.prepareBuildingForTestingOnly(
+        reporter, new DummyExecutor(fileSystem, rootDirectory), options, NULL_CHECKER, null);
+    EvaluationResult<FileArtifactValue> result =
+        evaluate(Artifact.keys(ImmutableList.of(output1, output2)));
+    assertThat(result.hasError()).isFalse();
+    TrackingAwaiter.INSTANCE.assertNoErrors();
+  }
+
+  /** Regression test for ##5396: successfully build shared actions with tree artifacts. */
+  @Test
+  public void sharedActionsWithTree() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("trees");
+    // We create two "configured targets" and two copies of the same artifact, each generated by
+    // an action from its respective configured target.
+    ActionLookupValue.ActionLookupKey lc1 = new InjectedActionLookupKey("lc1");
+    Artifact.SpecialArtifact output1 =
+        new Artifact.SpecialArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath,
+            lc1,
+            Artifact.SpecialArtifactType.TREE);
+    ImmutableList<PathFragment> children = ImmutableList.of(PathFragment.create("child"));
+    Action action1 =
+        new TreeArtifactAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output1, children);
+    ConfiguredTargetValue ctValue1 = createConfiguredTargetValue(action1, lc1);
+    ActionLookupValue.ActionLookupKey lc2 = new InjectedActionLookupKey("lc2");
+    Artifact.SpecialArtifact output2 =
+        new Artifact.SpecialArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath,
+            lc2,
+            Artifact.SpecialArtifactType.TREE);
+    Action action2 =
+        new TreeArtifactAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output2, children);
+    ConfiguredTargetValue ctValue2 = createConfiguredTargetValue(action2, lc2);
+    // Inject the "configured targets" into the graph.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(lc1, ctValue1, lc2, ctValue2));
+    // Do a null build, so that the skyframe executor initializes the action executor properly.
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+    skyframeExecutor.buildArtifacts(
+        reporter,
+        ResourceManager.instanceForTestingOnly(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.<Artifact>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<AspectValue>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        options,
+        NULL_CHECKER,
+        null,
+        null,
+        null);
+
+    skyframeExecutor.prepareBuildingForTestingOnly(
+        reporter, new DummyExecutor(fileSystem, rootDirectory), options, NULL_CHECKER, null);
+    evaluate(ImmutableList.of(output1, output2));
+  }
+
+  /** Dummy action that creates a tree output. */
+  // AutoCodec because the superclass has a WrappedRunnable inside it.
+  @AutoCodec
+  @AutoCodec.VisibleForSerialization
+  static class TreeArtifactAction extends TestAction {
+    @SuppressWarnings("unused") // Only needed for serialization.
+    private final Artifact.SpecialArtifact output;
+
+    @SuppressWarnings("unused") // Only needed for serialization.
+    private final Iterable<PathFragment> children;
+
+    TreeArtifactAction(
+        NestedSet<Artifact> inputs,
+        Artifact.SpecialArtifact output,
+        Iterable<PathFragment> children) {
+      super(() -> createDirectoryAndFiles(output, children), inputs, ImmutableSet.of(output));
+      Preconditions.checkState(output.isTreeArtifact(), output);
+      this.output = output;
+      this.children = children;
+    }
+
+    private static void createDirectoryAndFiles(
+        Artifact.SpecialArtifact output, Iterable<PathFragment> children) {
+      Path directory = output.getPath();
+      try {
+        directory.createDirectoryAndParents();
+        for (PathFragment child : children) {
+          FileSystemUtils.createEmptyFile(directory.getRelative(child));
+        }
+      } catch (IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+  }
+
+  /** Regression test for ##5396: successfully build shared actions with tree artifacts. */
+  @Test
+  public void sharedActionTemplate() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("trees");
+    // We create two "configured targets" and two copies of the same artifact, each generated by
+    // an action from its respective configured target.
+    ActionLookupValue.ActionLookupKey baseKey = new InjectedActionLookupKey("base");
+    Artifact.SpecialArtifact baseOutput =
+        new Artifact.SpecialArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath,
+            baseKey,
+            Artifact.SpecialArtifactType.TREE);
+    ImmutableList<PathFragment> children = ImmutableList.of(PathFragment.create("child"));
+    Action action1 =
+        new TreeArtifactAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), baseOutput, children);
+    ConfiguredTargetValue baseCt = createConfiguredTargetValue(action1, baseKey);
+    ActionLookupValue.ActionLookupKey shared1 = new InjectedActionLookupKey("shared1");
+    PathFragment execPath2 = PathFragment.create("out").getRelative("treesShared");
+    Artifact.SpecialArtifact sharedOutput1 =
+        new Artifact.SpecialArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath2,
+            shared1,
+            Artifact.SpecialArtifactType.TREE);
+    ActionTemplate<DummyAction> template1 =
+        new DummyActionTemplate(baseOutput, sharedOutput1, ActionOwner.SYSTEM_ACTION_OWNER);
+    ConfiguredTargetValue shared1Ct = createConfiguredTargetValue(template1, shared1);
+    ActionLookupValue.ActionLookupKey shared2 = new InjectedActionLookupKey("shared2");
+    Artifact.SpecialArtifact sharedOutput2 =
+        new Artifact.SpecialArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath2,
+            shared2,
+            Artifact.SpecialArtifactType.TREE);
+    ActionTemplate<DummyAction> template2 =
+        new DummyActionTemplate(baseOutput, sharedOutput2, ActionOwner.SYSTEM_ACTION_OWNER);
+    ConfiguredTargetValue shared2Ct = createConfiguredTargetValue(template2, shared2);
+    // Inject the "configured targets" into the graph.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(baseKey, baseCt, shared1, shared1Ct, shared2, shared2Ct));
+    // Do a null build, so that the skyframe executor initializes the action executor properly.
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+    skyframeExecutor.buildArtifacts(
+        reporter,
+        ResourceManager.instanceForTestingOnly(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.<Artifact>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<AspectValue>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        options,
+        NULL_CHECKER,
+        null,
+        null,
+        null);
+
+    skyframeExecutor.prepareBuildingForTestingOnly(
+        reporter, new DummyExecutor(fileSystem, rootDirectory), options, NULL_CHECKER, null);
+    evaluate(ImmutableList.of(sharedOutput1, sharedOutput2));
+  }
+
+  private static class DummyActionTemplate implements ActionTemplate<DummyAction> {
+    private final Artifact.SpecialArtifact inputArtifact;
+    private final Artifact.SpecialArtifact outputArtifact;
+    private final ActionOwner actionOwner;
+
+    private DummyActionTemplate(
+        Artifact.SpecialArtifact inputArtifact,
+        Artifact.SpecialArtifact outputArtifact,
+        ActionOwner actionOwner) {
+      this.inputArtifact = inputArtifact;
+      this.outputArtifact = outputArtifact;
+      this.actionOwner = actionOwner;
+    }
+
+    @Override
+    public boolean isShareable() {
+      return true;
+    }
+
+    @Override
+    public Iterable<DummyAction> generateActionForInputArtifacts(
+        Iterable<TreeFileArtifact> inputTreeFileArtifacts, ActionLookupKey artifactOwner) {
+      return ImmutableList.copyOf(inputTreeFileArtifacts).stream()
+          .map(
+              input -> {
+                Artifact.TreeFileArtifact output =
+                    ActionInputHelper.treeFileArtifactWithNoGeneratingActionSet(
+                        outputArtifact, input.getParentRelativePath(), artifactOwner);
+                return new DummyAction(NestedSetBuilder.create(Order.STABLE_ORDER, input), output);
+              })
+          .collect(ImmutableList.toImmutableList());
+    }
+
+    @Override
+    public String getKey(ActionKeyContext actionKeyContext) {
+      Fingerprint fp = new Fingerprint();
+      fp.addPath(inputArtifact.getPath());
+      fp.addPath(outputArtifact.getPath());
+      return fp.hexDigestAndReset();
+    }
+
+    @Override
+    public Artifact getInputTreeArtifact() {
+      return inputArtifact;
+    }
+
+    @Override
+    public Artifact getOutputTreeArtifact() {
+      return outputArtifact;
+    }
+
+    @Override
+    public ActionOwner getOwner() {
+      return actionOwner;
+    }
+
+    @Override
+    public String getMnemonic() {
+      return "DummyTemplate";
+    }
+
+    @Override
+    public String prettyPrint() {
+      return "DummyTemplate";
+    }
+
+    @Override
+    public NestedSet<Artifact> getTools() {
+      return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+    }
+
+    @Override
+    public NestedSet<Artifact> getInputs() {
+      return NestedSetBuilder.create(Order.STABLE_ORDER, inputArtifact);
+    }
+
+    @Override
+    public Iterable<String> getClientEnvironmentVariables() {
+      return ImmutableList.of();
+    }
+
+    @Override
+    public ImmutableSet<Artifact> getOutputs() {
+      return ImmutableSet.of(outputArtifact);
+    }
+
+    @Override
+    public NestedSet<Artifact> getInputFilesForExtraAction(
+        ActionExecutionContext actionExecutionContext) {
+      return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+    }
+
+    @Override
+    public ImmutableSet<Artifact> getMandatoryOutputs() {
+      return ImmutableSet.of();
+    }
+
+    @Override
+    public Artifact getPrimaryInput() {
+      return inputArtifact;
+    }
+
+    @Override
+    public Artifact getPrimaryOutput() {
+      return outputArtifact;
+    }
+
+    @Override
+    public NestedSet<Artifact> getMandatoryInputs() {
+      return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+    }
+
+    @Override
+    public boolean shouldReportPathPrefixConflict(ActionAnalysisMetadata action) {
+      return this != action;
+    }
+
+    @Override
+    public MiddlemanType getActionType() {
+      return MiddlemanType.NORMAL;
+    }
+  }
+
+  /**
+   * Tests that events from action lookup keys (i.e., analysis events) are not stored in execution.
+   * This test is actually more extreme than Blaze is, since it skips the analysis phase and so
+   * <i>never</i> emits the analysis events, while in reality Blaze will always emit the analysis
+   * events, during the analysis phase.
+   *
+   * <p>Also incidentally tests that events coming from action execution are actually not stored at
+   * all.
+   */
+  @Test
+  public void analysisEventsNotStoredInExecution() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+    ActionLookupValue.ActionLookupKey lc1 = new InjectedActionLookupKey("lc1");
+    Artifact output =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("foo"),
+            lc1);
+    Action action1 = new WarningAction(ImmutableList.of(), output, "action 1");
+    SkyValue ctValue1 =
+        ValueWithMetadata.normal(
+            createConfiguredTargetValue(action1, lc1),
+            null,
+            NestedSetBuilder.create(
+                Order.STABLE_ORDER,
+                new TaggedEvents(null, ImmutableList.of(Event.warn("analysis warning 1")))),
+            NestedSetBuilder.emptySet(Order.STABLE_ORDER));
+    ActionLookupValue.ActionLookupKey lc2 = new InjectedActionLookupKey("lc2");
+    Artifact output2 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("bar"),
+            lc2);
+    Action action2 = new WarningAction(ImmutableList.of(output), output2, "action 2");
+    SkyValue ctValue2 =
+        ValueWithMetadata.normal(
+            createConfiguredTargetValue(action2, lc2),
+            null,
+            NestedSetBuilder.create(
+                Order.STABLE_ORDER,
+                new TaggedEvents(null, ImmutableList.of(Event.warn("analysis warning 2")))),
+            NestedSetBuilder.emptySet(Order.STABLE_ORDER));
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(lc1, ctValue1, lc2, ctValue2));
+    // Do a null build, so that the skyframe executor initializes the action executor properly.
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+    skyframeExecutor.buildArtifacts(
+        reporter,
+        ResourceManager.instanceForTestingOnly(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.<ConfiguredTarget>of(),
+        options,
+        NULL_CHECKER,
+        null,
+        null,
+        null);
+
+    skyframeExecutor.prepareBuildingForTestingOnly(
+        reporter, new DummyExecutor(fileSystem, rootDirectory), options, NULL_CHECKER, null);
+    evaluate(ImmutableList.of(Artifact.key(output2)));
+    assertContainsEvent("action 1");
+    assertContainsEvent("action 2");
+    assertDoesNotContainEvent("analysis warning 1");
+    assertDoesNotContainEvent("analysis warning 2");
+
+    // Action's warnings are not stored, and configured target warnings never seen.
+    assertThat(
+            ValueWithMetadata.getEvents(
+                    skyframeExecutor
+                        .getDriver()
+                        .getEntryForTesting(ActionLookupData.create(lc1, 0))
+                        .getValueMaybeWithMetadata())
+                .toList())
+        .isEmpty();
+    assertThat(
+            ValueWithMetadata.getEvents(
+                    skyframeExecutor
+                        .getDriver()
+                        .getEntryForTesting(ActionLookupData.create(lc2, 0))
+                        .getValueMaybeWithMetadata())
+                .toList())
+        .isEmpty();
+  }
+
+  private static class WarningAction extends AbstractAction {
+    private final String warningText;
+
+    private WarningAction(ImmutableList<Artifact> inputs, Artifact output, String warningText) {
+      super(
+          NULL_ACTION_OWNER,
+          NestedSetBuilder.<Artifact>stableOrder().addAll(inputs).build(),
+          ImmutableSet.of(output));
+      this.warningText = warningText;
+    }
+
+    @Override
+    public String getMnemonic() {
+      return "warning action";
+    }
+
+    @Override
+    protected void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) {
+      fp.addString(warningText);
+      fp.addPath(getPrimaryOutput().getExecPath());
+    }
+
+    @Override
+    public ActionResult execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      actionExecutionContext.getEventHandler().handle(Event.warn(warningText));
+      try {
+        FileSystemUtils.createEmptyFile(actionExecutionContext.getInputPath(getPrimaryOutput()));
+      } catch (IOException e) {
+        throw new ActionExecutionException(e, this, false);
+      }
+      return ActionResult.EMPTY;
+    }
+  }
+
+  /** Dummy action that throws a catastrophic error when it runs. */
+  private static class CatastrophicAction extends DummyAction {
+    public static final ExitCode expectedExitCode = ExitCode.RESERVED;
+
+    CatastrophicAction(Artifact output) {
+      super(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output, MiddlemanType.NORMAL);
+    }
+
+    @Override
+    public ActionResult execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      throw new ActionExecutionException("message", new Exception("just cause"), this,
+          /*catastrophe=*/true, expectedExitCode);
+    }
+  }
+
+  /** Dummy action that flips a boolean when it runs. */
+  private static class MarkerAction extends DummyAction {
+    private final AtomicBoolean executed;
+
+    MarkerAction(Artifact output, AtomicBoolean executed) {
+      super(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output, MiddlemanType.NORMAL);
+      this.executed = executed;
+      assertThat(executed.get()).isFalse();
+    }
+
+    @Override
+    public ActionResult execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      ActionResult actionResult = super.execute(actionExecutionContext);
+      assertThat(executed.getAndSet(true)).isFalse();
+      return actionResult;
+    }
+  }
+
+  private BinTools setupEmbeddedArtifacts() throws IOException {
+    List<String> embeddedTools = analysisMock.getEmbeddedTools();
+    directories.getEmbeddedBinariesRoot().createDirectoryAndParents();
+    for (String embeddedToolName : embeddedTools) {
+      Path toolPath = directories.getEmbeddedBinariesRoot().getRelative(embeddedToolName);
+      FileSystemUtils.touchFile(toolPath);
+    }
+    return BinTools.forIntegrationTesting(directories, embeddedTools);
+  }
+
+  /** Test appropriate behavior when an action halts the build with a catastrophic failure. */
+  private void runCatastropheHaltsBuild() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+    ActionLookupValue.ActionLookupKey lc1 = new InjectedActionLookupKey("lc1");
+    Artifact output =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("foo"),
+            lc1);
+    Action action1 = new CatastrophicAction(output);
+    ConfiguredTargetValue ctValue1 = createConfiguredTargetValue(action1, lc1);
+    ActionLookupValue.ActionLookupKey lc2 = new InjectedActionLookupKey("lc2");
+    Artifact output2 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("bar"),
+            lc2);
+    AtomicBoolean markerRan = new AtomicBoolean(false);
+    Action action2 = new MarkerAction(output2, markerRan);
+    ConfiguredTargetValue ctValue2 = createConfiguredTargetValue(action2, lc2);
+
+    // Perform testing-related setup.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(lc1, ctValue1, lc2, ctValue2));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER, ActionExecutionStatusReporter.create(reporter));
+
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /* fileCache= */ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    // Note that since ImmutableSet iterates through its elements in the order they are passed in
+    // here, we are guaranteed that output will be built before output2, throwing an exception and
+    // shutting down the build before output2 is requested.
+    Set<Artifact> normalArtifacts = ImmutableSet.of(output, output2);
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    normalArtifacts,
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<AspectValue>of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    null));
+    // The catastrophic exception should be propagated into the BuildFailedException whether or not
+    // --keep_going is set.
+    assertThat(e.getExitCode()).isEqualTo(CatastrophicAction.expectedExitCode);
+    assertThat(builtTargets).isEmpty();
+    assertThat(markerRan.get()).isFalse();
+  }
+
+  private static NonRuleConfiguredTargetValue createConfiguredTargetValue(
+      ActionAnalysisMetadata generatingAction, ActionLookupValue.ActionLookupKey actionLookupKey) {
+    return new NonRuleConfiguredTargetValue(
+        new SerializableConfiguredTarget(),
+        GeneratingActions.fromSingleAction(generatingAction, actionLookupKey),
+        NestedSetBuilder.<Package>stableOrder().build());
+  }
+
+  @Test
+  public void testCatastropheInNoKeepGoing() throws Exception {
+    options.parse("--nokeep_going", "--jobs=1");
+    runCatastropheHaltsBuild();
+  }
+
+  @Test
+  public void testCatastrophicBuild() throws Exception {
+    options.parse("--keep_going", "--jobs=1");
+    runCatastropheHaltsBuild();
+  }
+
+  /**
+   * Test appropriate behavior when an action halts the build with a transitive catastrophic
+   * failure.
+   */
+  @Test
+  public void testTransitiveCatastropheHaltsBuild() throws Exception {
+    options.parse("--keep_going", "--jobs=5");
+
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+    ActionLookupValue.ActionLookupKey catastropheCTK = new InjectedActionLookupKey("catastrophe");
+    Artifact catastropheArtifact =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("zcatas"),
+            catastropheCTK);
+    CountDownLatch failureHappened = new CountDownLatch(1);
+    Action catastrophicAction =
+        new CatastrophicAction(catastropheArtifact) {
+          @Override
+          public ActionResult execute(ActionExecutionContext actionExecutionContext)
+              throws ActionExecutionException {
+            TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
+                failureHappened, "didn't count failure");
+            return super.execute(actionExecutionContext);
+          }
+        };
+    ConfiguredTargetValue catastropheCTV =
+        createConfiguredTargetValue(catastrophicAction, catastropheCTK);
+    ActionLookupValue.ActionLookupKey failureCTK = new InjectedActionLookupKey("failure");
+    Artifact failureArtifact =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("fail"),
+            failureCTK);
+    Action failureAction = new FailedExecAction(failureArtifact, ExitCode.RESERVED);
+    ConfiguredTargetValue failureCTV = createConfiguredTargetValue(failureAction, failureCTK);
+    ActionLookupValue.ActionLookupKey topCTK = new InjectedActionLookupKey("top");
+    Artifact topArtifact =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("top"),
+            topCTK);
+    Action topAction =
+        new DummyAction(
+            NestedSetBuilder.create(Order.STABLE_ORDER, failureArtifact, catastropheArtifact),
+            topArtifact);
+    ConfiguredTargetValue topCTV = createConfiguredTargetValue(topAction, topCTK);
+    // Perform testing-related setup.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(
+            ImmutableMap.of(
+                catastropheCTK, catastropheCTV,
+                failureCTK, failureCTV,
+                topCTK, topCTV));
+    skyframeExecutor
+        .getDriver()
+        .getGraphForTesting()
+        .injectGraphTransformerForTesting(
+            DeterministicHelper.makeTransformer(
+                (key, type, order, context) -> {
+                  if (key.equals(Artifact.key(failureArtifact)) && type == EventType.SET_VALUE) {
+                    failureHappened.countDown();
+                  }
+                },
+                /*deterministic=*/ true));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /*fileCache=*/ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    Set<Artifact> normalArtifacts = ImmutableSet.of(topArtifact);
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    normalArtifacts,
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<AspectValue>of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    null));
+    // The catastrophic exception should be propagated into the BuildFailedException whether or not
+    // --keep_going is set.
+    assertThat(e.getExitCode()).isEqualTo(CatastrophicAction.expectedExitCode);
+    assertThat(builtTargets).isEmpty();
+  }
+
+  /**
+   * Test appropriate behavior when an action halts the build with a transitive catastrophic
+   * failure.
+   */
+  @Test
+  public void testCatastropheAndNonCatastropheInCompletion() throws Exception {
+    options.parse("--keep_going", "--jobs=5");
+
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+    ActionLookupValue.ActionLookupKey configuredTargetKey = new InjectedActionLookupKey("key");
+    Artifact catastropheArtifact =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("catas"),
+            configuredTargetKey);
+    int failedSize = 100;
+    CountDownLatch failureHappened = new CountDownLatch(failedSize);
+    Action catastrophicAction =
+        new CatastrophicAction(catastropheArtifact) {
+          @Override
+          public ActionResult execute(ActionExecutionContext actionExecutionContext)
+              throws ActionExecutionException {
+            TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
+                failureHappened, "didn't count failure");
+            return super.execute(actionExecutionContext);
+          }
+        };
+    // Because of random map ordering when getting values back in CompletionFunction, we just
+    // sprinkle our failure nodes randomly about the alphabet, trusting that at least one will come
+    // before "catas".
+    List<Action> failedActions = new ArrayList<>(failedSize);
+    LinkedHashSet<Artifact> failedArtifacts = new LinkedHashSet<>();
+    for (int i = 0; i < failedSize; i++) {
+      String failString = HashCode.fromBytes(("fail" + i).getBytes(UTF_8)).toString();
+      Artifact failureArtifact =
+          new Artifact.DerivedArtifact(
+              ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+              execPath.getRelative(failString),
+              configuredTargetKey);
+      failedArtifacts.add(failureArtifact);
+      failedActions.add(new FailedExecAction(failureArtifact, ExitCode.BUILD_FAILURE));
+    }
+    NonRuleConfiguredTargetValue nonRuleConfiguredTargetValue =
+        new NonRuleConfiguredTargetValue(
+            new SerializableConfiguredTarget(),
+            Actions.assignOwnersAndFilterSharedActionsAndThrowActionConflict(
+                new ActionKeyContext(),
+                ImmutableList.<ActionAnalysisMetadata>builder()
+                    .add(catastrophicAction)
+                    .addAll(failedActions)
+                    .build(),
+                configuredTargetKey,
+                /*outputFiles=*/ null),
+            NestedSetBuilder.<Package>stableOrder().build());
+    HashSet<ActionLookupData> failedActionKeys = new HashSet<>();
+    for (Action failedAction : failedActions) {
+      failedActionKeys.add(
+          ((Artifact.DerivedArtifact) failedAction.getPrimaryOutput()).getGeneratingActionKey());
+    }
+
+    // Perform testing-related setup.
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(ImmutableMap.of(configuredTargetKey, nonRuleConfiguredTargetValue));
+    skyframeExecutor
+        .getDriver()
+        .getGraphForTesting()
+        .injectGraphTransformerForTesting(
+            DeterministicHelper.makeTransformer(
+                (key, type, order, context) -> {
+                  if ((key instanceof ActionLookupData)
+                      && failedActionKeys.contains(key)
+                      && type == EventType.SET_VALUE) {
+                    failureHappened.countDown();
+                  }
+                },
+                // Determinism actually doesn't help here because the internal maps are still
+                // effectively unordered.
+                /*deterministic=*/ true));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /*fileCache=*/ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    ImmutableSet.<Artifact>builder()
+                        .addAll(failedArtifacts)
+                        .add(catastropheArtifact)
+                        .build(),
+                    ImmutableSet.of(),
+                    ImmutableSet.of(),
+                    ImmutableSet.of(),
+                    ImmutableSet.of(),
+                    ImmutableSet.of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    new TopLevelArtifactContext(
+                        /*runTestsExclusively=*/ false,
+                        false,
+                        OutputGroupInfo.determineOutputGroups(ImmutableList.of(), true))));
+    // The catastrophic exception should be propagated into the BuildFailedException whether or not
+    // --keep_going is set.
+    assertThat(e.getExitCode()).isEqualTo(CatastrophicAction.expectedExitCode);
+    assertThat(builtTargets).isEmpty();
+  }
+
+  @Test
+  public void testCatastrophicBuildWithoutEdges() throws Exception {
+    options.parse("--keep_going", "--jobs=1", "--discard_analysis_cache");
+    skyframeExecutor.setActive(false);
+    skyframeExecutor.decideKeepIncrementalState(
+        /*batch=*/ true,
+        /*keepStateAfterBuild=*/ true,
+        /*shouldTrackIncrementalState=*/ true,
+        /*discardAnalysisCache=*/ true,
+        reporter);
+    skyframeExecutor.setActive(true);
+    runCatastropheHaltsBuild();
+  }
+
+  @Test
+  public void testCatastropheReportingWithError() throws Exception {
+    options.parse("--keep_going", "--jobs=1");
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+    // When we have an action that throws a (non-catastrophic) exception when it is executed,
+    ActionLookupValue.ActionLookupKey failedKey = new InjectedActionLookupKey("failed");
+    Artifact failedOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("failed"),
+            failedKey);
+    final AtomicReference<Action> failedActionReference = new AtomicReference<>();
+    final Action failedAction =
+        new TestAction(
+            new Callable<Void>() {
+              @Override
+              public Void call() throws ActionExecutionException {
+                throw new ActionExecutionException(
+                    new Exception(), failedActionReference.get(), /*catastrophe=*/ false);
+              }
+            },
+            NestedSetBuilder.emptySet(Order.STABLE_ORDER),
+            ImmutableSet.of(failedOutput));
+    ConfiguredTargetValue failedTarget = createConfiguredTargetValue(failedAction, failedKey);
+
+    // And an action that throws a catastrophic exception when it is executed,
+    ActionLookupValue.ActionLookupKey catastrophicKey = new InjectedActionLookupKey("catastrophic");
+    Artifact catastrophicOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("catastrophic"),
+            catastrophicKey);
+    Action catastrophicAction = new CatastrophicAction(catastrophicOutput);
+    ConfiguredTargetValue catastrophicTarget =
+        createConfiguredTargetValue(catastrophicAction, catastrophicKey);
+
+    // And the relevant configured targets have been injected into the graph,
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(
+            ImmutableMap.of(
+                failedKey, failedTarget,
+                catastrophicKey, catastrophicTarget));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+
+    // And the two artifacts are requested,
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /* fileCache= */ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    // Note that since ImmutableSet iterates through its elements in the order they are passed in
+    // here, we are guaranteed that failedOutput will be built before catastrophicOutput is
+    // requested, putting a top-level failure into the build result.
+    Set<Artifact> normalArtifacts = ImmutableSet.of(failedOutput, catastrophicOutput);
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    normalArtifacts,
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<AspectValue>of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    null));
+    // The catastrophic exception should be propagated into the BuildFailedException whether or not
+    // --keep_going is set.
+    assertThat(e.getExitCode()).isEqualTo(CatastrophicAction.expectedExitCode);
+    assertThat(builtTargets).isEmpty();
+  }
+
+  /** Dummy action that throws a ActionExecution error when it runs. */
+  private static class FailedExecAction extends DummyAction {
+    private final ExitCode exitCode;
+
+    FailedExecAction(Artifact output, ExitCode exitCode) {
+      super(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output, MiddlemanType.NORMAL);
+      this.exitCode = exitCode;
+    }
+
+    @Override
+    public ActionResult execute(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      throw new ActionExecutionException(
+          "foo", new Exception("bar"), this, /*catastrophe=*/ false, exitCode);
+    }
+  }
+
+  private static final ExitCode USER_EXIT_CODE = ExitCode.create(Integer.MAX_VALUE, "user_error");
+  private static final ExitCode INFRA_EXIT_CODE =
+      ExitCode.createInfrastructureFailure(Integer.MAX_VALUE - 1, "infra_error");
+
+  /**
+   * Verify SkyframeBuilder returns correct user error code as global error code when:
+   *    1. keepGoing mode is true.
+   *    2. user error code exists.
+   *    3. no infrastructure error code exists.
+   */
+  @Test
+  public void testKeepGoingExitCodeWithUserError() throws Exception {
+    options.parse("--keep_going", "--jobs=1");
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+
+    ActionLookupValue.ActionLookupKey succeededKey = new InjectedActionLookupKey("succeeded");
+    Artifact succeededOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("succeeded"),
+            succeededKey);
+
+    ActionLookupValue.ActionLookupKey failedKey = new InjectedActionLookupKey("failed");
+    Artifact failedOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("failed"),
+            failedKey);
+
+    // Create 1 succeeded key and 1 failed key with user error
+    Action succeededAction =
+        new DummyAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), succeededOutput);
+    ConfiguredTargetValue succeededTarget =
+        createConfiguredTargetValue(succeededAction, succeededKey);
+    Action failedAction = new FailedExecAction(failedOutput, USER_EXIT_CODE);
+    ConfiguredTargetValue failedTarget = createConfiguredTargetValue(failedAction, failedKey);
+
+    // Inject the targets into the graph,
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(
+            ImmutableMap.of(
+                succeededKey, succeededTarget,
+                failedKey, failedTarget));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+
+    // And the two artifacts are requested,
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /* fileCache= */ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    Set<Artifact> normalArtifacts = ImmutableSet.of(succeededOutput, failedOutput);
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    normalArtifacts,
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<AspectValue>of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    null));
+    // The exit code should be propagated into the BuildFailedException whether or not --keep_going
+    // is set.
+    assertThat(e.getExitCode()).isEqualTo(USER_EXIT_CODE);
+  }
+
+  /**
+   * Verify SkyframeBuilder returns correct infrastructure error code as global error code when:
+   *    1. keepGoing mode is true.
+   *    2. infrastructure error code exists.
+   */
+  @Test
+  public void testKeepGoingExitCodeWithUserAndInfrastructureError() throws Exception {
+    options.parse("--keep_going", "--jobs=1");
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+
+    ActionLookupValue.ActionLookupKey succeededKey = new InjectedActionLookupKey("succeeded");
+    Artifact succeededOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("succeeded"),
+            succeededKey);
+
+    ActionLookupValue.ActionLookupKey failedKey1 = new InjectedActionLookupKey("failed1");
+    Artifact failedOutput1 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("failed1"),
+            failedKey1);
+
+    ActionLookupValue.ActionLookupKey failedKey2 = new InjectedActionLookupKey("failed2");
+    Artifact failedOutput2 =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("failed2"),
+            failedKey2);
+
+    // Create 1 succeeded key, 1 failed key with infrastructure error and another failed key with
+    // user error.
+
+    Action succeededAction =
+        new DummyAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), succeededOutput);
+    ConfiguredTargetValue succeededTarget =
+        createConfiguredTargetValue(succeededAction, succeededKey);
+    Action failedAction1 = new FailedExecAction(failedOutput1, USER_EXIT_CODE);
+    ConfiguredTargetValue failedTarget1 = createConfiguredTargetValue(failedAction1, failedKey1);
+    Action failedAction2 = new FailedExecAction(failedOutput2, INFRA_EXIT_CODE);
+    ConfiguredTargetValue failedTarget2 = createConfiguredTargetValue(failedAction2, failedKey2);
+
+    // Inject the targets into the graph,
+    skyframeExecutor
+        .getDifferencerForTesting()
+        .inject(
+            ImmutableMap.<SkyKey, SkyValue>of(
+                succeededKey, succeededTarget,
+                failedKey1, failedTarget1,
+                failedKey2, failedTarget2));
+    skyframeExecutor.setEventBus(new EventBus());
+    setupEmbeddedArtifacts();
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+    skyframeExecutor.setActionExecutionProgressReportingObjects(
+        EMPTY_PROGRESS_SUPPLIER,
+        EMPTY_COMPLETION_RECEIVER,
+        ActionExecutionStatusReporter.create(reporter));
+
+    // And the two artifacts are requested,
+    reporter.removeHandler(failFastHandler); // Expect errors.
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /* fileCache= */ null,
+            ActionInputPrefetcher.NONE);
+    Set<ConfiguredTargetKey> builtTargets = new HashSet<>();
+    Set<AspectKey> builtAspects = new HashSet<>();
+    Set<Artifact> normalArtifacts = ImmutableSet.of(failedOutput1, failedOutput2);
+    BuildFailedException e =
+        assertThrows(
+            BuildFailedException.class,
+            () ->
+                builder.buildArtifacts(
+                    reporter,
+                    normalArtifacts,
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<ConfiguredTarget>of(),
+                    ImmutableSet.<AspectValue>of(),
+                    new DummyExecutor(fileSystem, rootDirectory),
+                    builtTargets,
+                    builtAspects,
+                    options,
+                    null,
+                    null));
+    // The exit code should be propagated into the BuildFailedException whether or not --keep_going
+    // is set.
+    assertThat(e.getExitCode()).isEqualTo(INFRA_EXIT_CODE);
+  }
+
+  /**
+   * Tests that when an input-discovering action terminates input discovery with missing inputs, its
+   * progress message goes away. We create an input-discovering action that declares a new input.
+   * When that new input is declared, which comes after the scanning is completed, we trigger a
+   * progress message, and assert that the message does not contain the "Scanning" message.
+   *
+   * <p>To guard against the output format changing, we also trigger a progress message during the
+   * scan, and assert that the message there is as expected.
+   */
+  @Test
+  public void inputDiscoveryMessageDoesntLinger() throws Exception {
+    Path root = getExecRoot();
+    PathFragment execPath = PathFragment.create("out").getRelative("dir");
+
+    ActionLookupValue.ActionLookupKey topKey = new InjectedActionLookupKey("top");
+    Artifact topOutput =
+        new Artifact.DerivedArtifact(
+            ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
+            execPath.getRelative("top"),
+            topKey);
+
+    Artifact sourceInput =
+        new Artifact.SourceArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory)),
+            PathFragment.create("source.optional"),
+            ArtifactOwner.NullArtifactOwner.INSTANCE);
+    FileSystemUtils.createEmptyFile(sourceInput.getPath());
+
+    Action inputDiscoveringAction =
+        new DummyAction(NestedSetBuilder.create(Order.STABLE_ORDER, sourceInput), topOutput) {
+          @Override
+          public NestedSet<Artifact> discoverInputs(ActionExecutionContext actionExecutionContext) {
+            skyframeExecutor
+                .getActionExecutionStatusReporterForTesting()
+                .showCurrentlyExecutingActions("during scanning ");
+            return super.discoverInputs(actionExecutionContext);
+          }
+        };
+
+    ConfiguredTargetValue topTarget = createConfiguredTargetValue(inputDiscoveringAction, topKey);
+    skyframeExecutor.getDifferencerForTesting().inject(ImmutableMap.of(topKey, topTarget));
+    // Collect all events.
+    eventCollector = new EventCollector();
+    reporter = new Reporter(eventBus, eventCollector);
+    skyframeExecutor.setEventBus(eventBus);
+    skyframeExecutor.setActionOutputRoot(getOutputPath());
+
+    Builder builder =
+        new SkyframeBuilder(
+            skyframeExecutor,
+            ResourceManager.instanceForTestingOnly(),
+            NULL_CHECKER,
+            null,
+            ModifiedFileSet.EVERYTHING_MODIFIED,
+            /*fileCache=*/ null,
+            ActionInputPrefetcher.NONE);
+    builder.buildArtifacts(
+        reporter,
+        ImmutableSet.of(topOutput),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        new DummyExecutor(fileSystem, rootDirectory),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        options,
+        null,
+        null);
+    assertContainsEventRegex(eventCollector, ".*during scanning.*\n.*Scanning.*\n.*Test dir/top.*");
+    assertNotContainsEventRegex(
+        eventCollector, ".*after scanning.*\n.*Scanning.*\n.*Test dir/top.*");
+  }
+
+  private AnalysisProtos.Artifact getArtifact(
+      String execPath, ActionGraphContainer actionGraphContainer) {
+    for (AnalysisProtos.Artifact artifact : actionGraphContainer.getArtifactsList()) {
+      if (execPath.equals(artifact.getExecPath())) {
+        return artifact;
+      }
+    }
+    return null;
+  }
+
+  private AnalysisProtos.Artifact getArtifactFromBinDir(
+      String workspaceRelativePath, ActionGraphContainer actionGraphContainer) {
+    return getArtifact(
+        getTargetConfiguration()
+            .getBinDir()
+            .getExecPath()
+            .getRelative(workspaceRelativePath)
+            .getPathString(),
+        actionGraphContainer);
+  }
+
+  private AnalysisProtos.Action getGeneratingAction(
+      String outputArtifactId, ActionGraphContainer actionGraphContainer) {
+    for (AnalysisProtos.Action action : actionGraphContainer.getActionsList()) {
+      for (String outputId : action.getOutputIdsList()) {
+        if (outputArtifactId.equals(outputId)) {
+          return action;
+        }
+      }
+    }
+    return null;
+  }
+
+  private AnalysisProtos.Target getTarget(String label, ActionGraphContainer actionGraphContainer) {
+    for (AnalysisProtos.Target target : actionGraphContainer.getTargetsList()) {
+      if (label.equals(target.getLabel())) {
+        return target;
+      }
+    }
+    return null;
+  }
+
+  private AnalysisProtos.AspectDescriptor getAspectDescriptor(
+      String aspectDescriptorId, ActionGraphContainer actionGraphContainer) {
+    for (AnalysisProtos.AspectDescriptor aspectDescriptor :
+        actionGraphContainer.getAspectDescriptorsList()) {
+      if (aspectDescriptorId.equals(aspectDescriptor.getId())) {
+        return aspectDescriptor;
+      }
+    }
+    return null;
+  }
+
+  private AnalysisProtos.RuleClass getRuleClass(
+      String ruleClassId, ActionGraphContainer actionGraphContainer) {
+    for (AnalysisProtos.RuleClass ruleClass : actionGraphContainer.getRuleClassesList()) {
+      if (ruleClassId.equals(ruleClass.getId())) {
+        return ruleClass;
+      }
+    }
+    return null;
+  }
+
+  public static final ImmutableList<String> ACTION_GRAPH_DEFAULT_TARGETS = ImmutableList.of("...");
+
+  @Test
+  public void testActionGraphDumpWithoutInputArtifacts() throws Exception {
+    scratch.file("x/BUILD", "genrule(name='x', srcs=['input'], outs=['out'], cmd='false')");
+    scratch.file("x/input", "foo");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter, Label.parseAbsolute("@//x", ImmutableMap.of()), getTargetConfiguration());
+    assertThat(ct).isNotNull();
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ false);
+
+    assertThat(actionGraphContainer.getActionsList()).isNotEmpty();
+    assertThat(actionGraphContainer.getArtifactsList()).isEmpty();
+    assertThat(actionGraphContainer.getDepSetOfFilesList()).isEmpty();
+    assertThat(actionGraphContainer.getActionsList().get(0).getInputDepSetIdsList()).isEmpty();
+    assertThat(actionGraphContainer.getActionsList().get(0).getOutputIdsList()).isEmpty();
+  }
+
+  @Test
+  public void testActionGraphDumpBrokenAnalysis() throws Exception {
+    scratch.file("x/BUILD", "java_library(name='x', exports=[':doesnotexist'])");
+
+    reporter.removeHandler(failFastHandler);
+    assertThat(
+            skyframeExecutor.getConfiguredTargetForTesting(
+                reporter, Label.parseAbsolute("@//x", ImmutableMap.of()), getTargetConfiguration()))
+        .isNull();
+    assertContainsEvent(
+        "in exports attribute of java_library rule //x:x: rule '//x:doesnotexist' does not exist");
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+    assertThat(actionGraphContainer).isNotNull();
+  }
+
+
+  @Test
+  public void testActionGraphDumpWithTreeArtifact() throws Exception {
+    scratch.file(
+        "x/def.bzl",
+        "def _tree_impl(ctx):",
+        "  tree_artifact = ctx.actions.declare_directory(ctx.attr.name + '_dir')",
+        "  ctx.actions.run_shell(",
+        "      inputs = [ctx.file.dummy],",
+        "      outputs = [tree_artifact],",
+        "      mnemonic = 'Treemove',",
+        "      use_default_shell_env = True,",
+        "      command = 'cp $1 $2',",
+        "      arguments = [",
+        "          ctx.file.dummy.path,",
+        "          tree_artifact.path,",
+        "      ],",
+        "  )",
+        "  return [",
+        "      DefaultInfo(files=depset([tree_artifact])),",
+        "  ]",
+        "",
+        "tree = rule(",
+        "    implementation = _tree_impl,",
+        "    attrs = {",
+        "        'dummy': attr.label(allow_single_file = True),",
+        "    },",
+        ")");
+    scratch.file(
+        "x/BUILD",
+        "load('//x:def.bzl', 'tree')",
+        "tree(",
+        "    name = 'tree',",
+        "    dummy = 'foo.txt',",
+        ")");
+    scratch.file("x/foo.txt", "hello world");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter,
+            Label.parseAbsolute("@//x:tree", ImmutableMap.of()),
+            getTargetConfiguration());
+    assertThat(ct).isNotNull();
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+
+    AnalysisProtos.Artifact inputArtifact = getArtifact("x/foo.txt", actionGraphContainer);
+    assertThat(inputArtifact).isNotNull();
+    assertThat(inputArtifact.getIsTreeArtifact()).isFalse();
+    AnalysisProtos.Artifact outputArtifact =
+        getArtifactFromBinDir("x/tree_dir", actionGraphContainer);
+    assertThat(outputArtifact).isNotNull();
+    assertThat(outputArtifact.getIsTreeArtifact()).isTrue();
+    AnalysisProtos.Action action =
+        getGeneratingAction(outputArtifact.getId(), actionGraphContainer);
+    assertThat(action).isNotNull();
+    assertThat(action.getMnemonic()).isEqualTo("Treemove");
+  }
+
+  @Test
+  public void testActionGraphDumpWithAspect() throws Exception {
+    scratch.file(
+        "x/def.bzl",
+        "Count = provider(",
+        "    fields = {",
+        "        'count' : 'count',",
+        "        'out' : 'outputfile'",
+        "    }",
+        ")",
+        "",
+        "def _count_aspect_impl(target, ctx):",
+        "    count = int(ctx.attr.default_count)",
+        "    for dep in ctx.rule.attr.deps:",
+        "        count = count + dep[Count].count",
+        "    output = ctx.actions.declare_file('count')",
+        "    ctx.actions.write(content = 'count = %s' % (count), output = output)",
+        "    return [",
+        "        Count(count = count, out = output),",
+        "        OutputGroupInfo(all_files = [output]),",
+        "    ]",
+        "",
+        "count_aspect = aspect(implementation = _count_aspect_impl,",
+        "    attr_aspects = ['deps'],",
+        "    attrs = {",
+        "        'default_count' : attr.string(values = ['0', '1', '42']),",
+        "    }",
+        ")",
+        "",
+        "def _count_rule_impl(ctx):",
+        "  outs = []",
+        "  for dep in ctx.attr.deps:",
+        "    outs += [dep[Count].out]",
+        "  return DefaultInfo(files=depset(outs))",
+        "",
+        "count_rule = rule(",
+        "  implementation = _count_rule_impl,",
+        "  attrs = {",
+        "      'deps' : attr.label_list(aspects = [count_aspect]),",
+        "      'default_count' : attr.string(default = '1'),",
+        "  },",
+        ")");
+    scratch.file(
+        "x/BUILD",
+        "load('//x:def.bzl', 'count_rule')",
+        "",
+        "count_rule(",
+        "    name = 'bar',",
+        ")",
+        "",
+        "count_rule(",
+        "    name = 'foo',",
+        "    deps = ['bar'],",
+        ")");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter, Label.parseAbsolute("@//x:foo", ImmutableMap.of()), getTargetConfiguration());
+    assertThat(ct).isNotNull();
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+
+    AnalysisProtos.Artifact countArtifact = getArtifactFromBinDir("x/count", actionGraphContainer);
+    assertThat(countArtifact).isNotNull();
+    AnalysisProtos.Target target = getTarget("//x:bar", actionGraphContainer);
+    assertThat(target).isNotNull();
+    AnalysisProtos.RuleClass ruleClass =
+        getRuleClass(target.getRuleClassId(), actionGraphContainer);
+    assertThat(ruleClass.getName()).isEqualTo("count_rule");
+    AnalysisProtos.Action action = getGeneratingAction(countArtifact.getId(), actionGraphContainer);
+    assertThat(action).isNotNull();
+    assertThat(action.getTargetId()).isEqualTo(target.getId());
+    String aspectDescriptorId = Iterables.getOnlyElement(action.getAspectDescriptorIdsList());
+    AnalysisProtos.AspectDescriptor aspectDescriptor =
+        getAspectDescriptor(aspectDescriptorId, actionGraphContainer);
+    assertThat(aspectDescriptor.getName()).isEqualTo("//x:def.bzl%count_aspect");
+    AnalysisProtos.KeyValuePair aspectParameter =
+        Iterables.getOnlyElement(aspectDescriptor.getParametersList());
+    assertThat(aspectParameter.getKey()).isEqualTo("default_count");
+    assertThat(aspectParameter.getValue()).isEqualTo("1");
+  }
+
+  @Test
+  public void testActionGraphDumpFilter() throws Exception {
+    scratch.file(
+        "x/BUILD",
+        "genrule(name='x', srcs=['input'], outs=['intermediate1'], cmd='false')",
+        "genrule(name='y', srcs=['intermediate1'], outs=['intermediate2'], cmd='false')",
+        "genrule(name='z', srcs=['intermediate2'], outs=['output'], cmd='false')");
+    scratch.file("x/input", "foo");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter, Label.parseAbsolute("@//x:z", ImmutableMap.of()), getTargetConfiguration());
+    assertThat(ct).isNotNull();
+
+    // Check unfiltered case first, all three targets should be there.
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+    for (String targetString : ImmutableList.of("//x:x", "//x:y", "//x:z")) {
+      AnalysisProtos.Target target = getTarget(targetString, actionGraphContainer);
+      assertThat(target).isNotNull();
+    }
+
+    // Now check filtered case, only the requested target should exist.
+    actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ImmutableList.of("//x:y"),
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+    for (String targetString : ImmutableList.of("//x:x", "//x:z")) {
+      AnalysisProtos.Target target = getTarget(targetString, actionGraphContainer);
+      assertThat(target).isNull();
+    }
+    AnalysisProtos.Target target = getTarget("//x:y", actionGraphContainer);
+    assertThat(target).isNotNull();
+    // Make sure that we also don't include actions for other targets.
+    AnalysisProtos.Action action = Iterables.getOnlyElement(actionGraphContainer.getActionsList());
+    assertThat(action.getTargetId()).isEqualTo(target.getId());
+  }
+
+  @Test
+  public void testActionGraphCmdLineDump() throws Exception {
+    scratch.file(
+        "x/def.bzl",
+        "def _impl(ctx):",
+        "    output = ctx.outputs.out",
+        "    input = ctx.file.file",
+        "    # The command may only access files declared in inputs.",
+        "    ctx.actions.run_shell(",
+        "        inputs=[input],",
+        "        outputs=[output],",
+        "        progress_message='Getting size of %s' % input.short_path,",
+        "        command='stat -L -c%%s %s > %s' % (input.path, output.path))",
+        "",
+        "size = rule(",
+        "    implementation=_impl,",
+        "    attrs={'file': attr.label(mandatory=True, allow_single_file=True)},",
+        "    outputs={'out': '%{name}.size'},",
+        ")");
+    scratch.file("x/BUILD",
+        "load('//x:def.bzl', 'size')",
+        "size(name = 'x', file = 'foo.txt')");
+    scratch.file("x/foo.txt",
+        "foo");
+
+    ConfiguredTarget ct =
+        skyframeExecutor.getConfiguredTargetForTesting(
+            reporter, Label.parseAbsolute("@//x", ImmutableMap.of()), getTargetConfiguration());
+    assertThat(ct).isNotNull();
+
+    // Check case without command line first.
+    ActionGraphContainer actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ false,
+            /* includeArtifacts= */ true);
+    AnalysisProtos.Action action = Iterables.getOnlyElement(actionGraphContainer.getActionsList());
+    assertThat(action.getArgumentsCount()).isEqualTo(0);
+
+    // Now check with command line.
+    actionGraphContainer =
+        skyframeExecutor.getActionGraphContainer(
+            ACTION_GRAPH_DEFAULT_TARGETS,
+            /* includeActionCmdLine= */ true,
+            /* includeArtifacts= */ true);
+    action = Iterables.getOnlyElement(actionGraphContainer.getActionsList());
+
+    List<String> args = action.getArgumentsList();
+    assertThat(args).hasSize(3);
+    assertThat(args.get(0)).matches("^.*(/bash|/bash.exe)$");
+    assertThat(args.get(1)).isEqualTo("-c");
+    assertThat(args.get(2)).startsWith("stat -L -c%s x/foo.txt > ");
+    assertThat(args.get(2)).endsWith("bin/x/x.size");
+  }
+
+  /** Use custom class instead of mock to make sure that the dynamic codecs lookup is correct. */
+  static class SerializableConfiguredTarget implements ConfiguredTarget {
+
+    @Override
+    public ImmutableCollection<String> getFieldNames() {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public String getErrorMessageForUnknownField(String field) {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public Object getValue(String name) {
+      return null;
+    }
+
+    @Override
+    public Label getLabel() {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public BuildConfigurationValue.Key getConfigurationKey() {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public <P extends TransitiveInfoProvider> P getProvider(Class<P> provider) {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public Object get(String providerKey) {
+      return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends Info> T get(NativeProvider<T> provider) {
+      return provider.getValueClass().cast(get(provider.getKey()));
+    }
+
+    @Nullable
+    @Override
+    public Info get(Provider.Key providerKey) {
+      return null;
+    }
+
+    @Override
+    public void repr(Printer printer) {}
+
+    @Override
+    public Object getIndex(StarlarkSemantics semantics, Object key) throws EvalException {
+      return null;
+    }
+
+    @Override
+    public boolean containsKey(StarlarkSemantics semantics, Object key) throws EvalException {
+      return false;
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupKeyCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupKeyCodecTest.java
new file mode 100644
index 0000000..0e84bb9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupKeyCodecTest.java
@@ -0,0 +1,47 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.SkylarkImportLookupValue.SkylarkImportLookupKey;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link SkylarkImportLookupKey_AutoCodec}. */
+@RunWith(JUnit4.class)
+public final class SkylarkImportLookupKeyCodecTest {
+
+  @Test
+  public void testCodec() throws Exception {
+    new SerializationTester(
+            SkylarkImportLookupKey.create(
+                Label.parseAbsolute("//foo/bar:baz", ImmutableMap.of()), false, -1, null),
+            SkylarkImportLookupKey.create(
+                Label.parseAbsolute("//foo/bar:baz", ImmutableMap.of()), true, -1, null),
+            SkylarkImportLookupKey.create(
+                Label.parseAbsolute("//foo/bar:baz", ImmutableMap.of()), true, 8, null),
+            SkylarkImportLookupKey.create(
+                Label.parseAbsolute("//foo/bar:baz", ImmutableMap.of()),
+                true,
+                4,
+                FsUtils.TEST_ROOT))
+        .addDependency(FileSystem.class, FsUtils.TEST_FILESYSTEM)
+        .runTests();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TargetPatternSequenceCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TargetPatternSequenceCodecTest.java
new file mode 100644
index 0000000..9ab6f49
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TargetPatternSequenceCodecTest.java
@@ -0,0 +1,62 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.skyframe.PrepareDepsOfPatternsValue.TargetPatternSequence;
+import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import com.google.protobuf.CodedOutputStream;
+import java.io.ByteArrayOutputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TargetPatternSequence_AutoCodec}. */
+@RunWith(JUnit4.class)
+public final class TargetPatternSequenceCodecTest {
+  @Test
+  public void testCodec() throws Exception {
+    new SerializationTester(
+            TargetPatternSequence.create(ImmutableList.of(), ""),
+            TargetPatternSequence.create(ImmutableList.of("foo", "bar"), "baz"),
+            TargetPatternSequence.create(ImmutableList.of("uno", "dos"), "tres"),
+            TargetPatternSequence.create(ImmutableList.of("dos", "uno"), "tres"))
+        .runTests();
+  }
+
+  @Test
+  public void testPatternsOrderSignificant() throws Exception {
+    SerializationContext writeContext = new SerializationContext(ImmutableMap.of());
+
+    ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
+    CodedOutputStream codedOut = CodedOutputStream.newInstance(outputBytes);
+    writeContext.serialize(
+        TargetPatternSequence.create(ImmutableList.of("uno", "dos"), "tres"), codedOut);
+    codedOut.flush();
+    byte[] serialized1 = outputBytes.toByteArray();
+    assertThat(serialized1).asList().isNotEmpty();
+    outputBytes.reset();
+    codedOut = CodedOutputStream.newInstance(outputBytes);
+    writeContext.serialize(
+        TargetPatternSequence.create(ImmutableList.of("dos", "uno"), "tres"), codedOut);
+    codedOut.flush();
+    byte[] serialized2 = outputBytes.toByteArray();
+    assertThat(serialized2).asList().isNotEmpty();
+    assertThat(serialized1).isNotEqualTo(serialized2);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TestSuiteExpansionKeyCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TestSuiteExpansionKeyCodecTest.java
new file mode 100644
index 0000000..8a941d3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TestSuiteExpansionKeyCodecTest.java
@@ -0,0 +1,39 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.TestsForTargetPatternValue.TestsForTargetPatternKey;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Serialization test for test_suite. */
+@RunWith(JUnit4.class)
+public final class TestSuiteExpansionKeyCodecTest {
+
+  @Test
+  public void testCodec() throws Exception {
+    new SerializationTester(
+            new TestsForTargetPatternKey(
+                ImmutableSortedSet.of(
+                    Label.parseAbsolute("//foo/bar:baz", ImmutableMap.of()),
+                    Label.parseAbsolute("//a/b:c", ImmutableMap.of()))),
+            new TestsForTargetPatternKey(ImmutableSortedSet.<Label>of()))
+        .runTests();
+  }
+}