Open source more tests for packages/

Tested:
  bazel test on merge-to-os-blaze.sh
  checked total test method count on
     blaze test //javatests/com/google/devtools/build/lib:PackagesTests //third_party/bazel/src/test/java/com/google/devtools/build/lib:packages-tests

--
MOS_MIGRATED_REVID=105963077
diff --git a/src/test/java/BUILD b/src/test/java/BUILD
index 6984937..7ab369c 100644
--- a/src/test/java/BUILD
+++ b/src/test/java/BUILD
@@ -468,6 +468,7 @@
     args = ["com.google.devtools.build.lib.AllTests"],
     deps = [
         ":actions_testutil",
+        ":analysis_testutil",
         ":foundations_testutil",
         ":packages_testutil",
         ":test_runner",
@@ -476,8 +477,10 @@
         "//src/main/java:bazel-core",
         "//src/main/java:events",
         "//src/main/java:packages",
+        "//src/main/java:skyframe-base",
         "//src/main/java:util",
         "//src/main/java:vfs",
+        "//src/main/protobuf:build_proto",
         "//third_party:guava",
         "//third_party:guava-testlib",
         "//third_party:jsr305",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/SubincludePreprocessorModule.java b/src/test/java/com/google/devtools/build/lib/buildtool/SubincludePreprocessorModule.java
new file mode 100644
index 0000000..0872184
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/SubincludePreprocessorModule.java
@@ -0,0 +1,46 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.buildtool;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.util.SubincludePreprocessor;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.UUID;
+
+public class SubincludePreprocessorModule extends BlazeModule {
+  private FileSystem fileSystem;
+
+  @Override
+  public void blazeStartup(
+      OptionsProvider startupOptions,
+      BlazeVersionInfo versionInfo,
+      UUID instanceId,
+      BlazeDirectories directories,
+      Clock clock)
+      throws AbruptExitException {
+    this.fileSystem = directories.getFileSystem();
+  }
+
+  @Override
+  public Preprocessor.Factory.Supplier getPreprocessorFactorySupplier() {
+    return new SubincludePreprocessor.FactorySupplier(fileSystem);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/AttributeContainerTest.java b/src/test/java/com/google/devtools/build/lib/packages/AttributeContainerTest.java
new file mode 100644
index 0000000..a541bb8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/AttributeContainerTest.java
@@ -0,0 +1,102 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Location.LineAndColumn;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link AttributeContainer}.
+ */
+@RunWith(JUnit4.class)
+public class AttributeContainerTest {
+
+  private RuleClass ruleClass;
+  private AttributeContainer container;
+  private Attribute attribute1;
+  private Attribute attribute2;
+
+  @Before
+  public void setUp() throws Exception {
+    ruleClass =
+        TestRuleClassProvider.getRuleClassProvider().getRuleClassMap().get("testing_dummy_rule");
+    attribute1 = ruleClass.getAttributeByName("srcs");
+    attribute2 = ruleClass.getAttributeByName("dummyinteger");
+    container = new AttributeContainer(ruleClass);
+  }
+
+  @Test
+  public void testAttributeSettingAndRetrievalByName() throws Exception {
+    Object someValue1 = new Object();
+    Object someValue2 = new Object();
+    container.setAttributeValueByName(attribute1.getName(), someValue1);
+    container.setAttributeValueByName(attribute2.getName(), someValue2);
+    assertEquals(someValue1, container.getAttr(attribute1.getName()));
+    assertEquals(someValue2, container.getAttr(attribute2.getName()));
+    assertNull(container.getAttr("nomatch"));
+  }
+
+  @Test
+  public void testAttributeSettingAndRetrievalByInstance() throws Exception {
+    Object someValue1 = new Object();
+    Object someValue2 = new Object();
+    container.setAttributeValue(attribute1, someValue1, true);
+    container.setAttributeValue(attribute2, someValue2, true);
+    assertEquals(someValue1, container.getAttr(attribute1));
+    assertEquals(someValue2, container.getAttr(attribute2));
+  }
+
+  @Test
+  public void testExplicitSpecificationsByName() throws Exception {
+    // Name-based setters are automatically considered explicit.
+    container.setAttributeValueByName(attribute1.getName(), new Object());
+    assertTrue(container.isAttributeValueExplicitlySpecified(attribute1));
+    assertFalse(container.isAttributeValueExplicitlySpecified("nomatch"));
+  }
+
+  @Test
+  public void testExplicitSpecificationsByInstance() throws Exception {
+    Object someValue = new Object();
+    container.setAttributeValue(attribute1, someValue, true);
+    container.setAttributeValue(attribute2, someValue, false);
+    assertTrue(container.isAttributeValueExplicitlySpecified(attribute1));
+    assertFalse(container.isAttributeValueExplicitlySpecified(attribute2));
+  }
+
+  private static Location newLocation() {
+    return Location.fromPathAndStartColumn(null, 0, 0, new LineAndColumn(0, 0));
+  }
+
+  @Test
+  public void testAttributeLocation() throws Exception {
+    Location location1 = newLocation();
+    Location location2 = newLocation();
+    container.setAttributeLocation(attribute1, location1);
+    container.setAttributeLocation(attribute2, location2);
+    assertEquals(location1, container.getAttributeLocation(attribute1.getName()));
+    assertEquals(location2, container.getAttributeLocation(attribute2.getName()));
+    assertNull(container.getAttributeLocation("nomatch"));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java b/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
index aefe88a..40a7aa6 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
@@ -1,4 +1,4 @@
-// Copyright 2006 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -206,14 +206,14 @@
   @Test
   public void testCloneBuilder() {
     FileTypeSet txtFiles = FileTypeSet.of(FileType.of("txt"));
-    RuleClass.Builder.RuleClassNamePredicate ruleClasses = 
+    RuleClass.Builder.RuleClassNamePredicate ruleClasses =
         new RuleClass.Builder.RuleClassNamePredicate("mock_rule");
-    
+
     Attribute parentAttr = attr("x", LABEL_LIST)
         .allowedFileTypes(txtFiles)
         .mandatory()
         .build();
-    
+
     Attribute childAttr1 = parentAttr.cloneBuilder().build();
     assertEquals("x", childAttr1.getName());
     assertEquals(txtFiles, childAttr1.getAllowedFileTypesPredicate());
diff --git a/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java b/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
index 85a7aa6..9cec42f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
@@ -1,4 +1,4 @@
-// Copyright 2006 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/EnvironmentGroupTest.java b/src/test/java/com/google/devtools/build/lib/packages/EnvironmentGroupTest.java
new file mode 100644
index 0000000..60629d8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/EnvironmentGroupTest.java
@@ -0,0 +1,96 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.vfs.Path;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link EnvironmentGroup}. Note input validation is handled in
+ * {@link PackageFactoryTest}.
+ */
+@RunWith(JUnit4.class)
+public class EnvironmentGroupTest extends PackageLoadingTestCase {
+
+  private Package pkg;
+  private EnvironmentGroup group;
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    Path buildfile =
+        scratch.file(
+            "pkg/BUILD",
+            "environment(name='foo', fulfills = [':bar', ':baz'])",
+            "environment(name='bar', fulfills = [':baz'])",
+            "environment(name='baz')",
+            "environment(name='not_in_group')",
+            "environment_group(",
+            "    name = 'group',",
+            "    environments = [':foo', ':bar', ':baz'],",
+            "    defaults = [':foo'],",
+            ")");
+    PackageFactory pkgFactory = new PackageFactory(TestRuleClassProvider.getRuleClassProvider());
+    pkg =
+        pkgFactory.createPackageForTesting(
+            PackageIdentifier.createInDefaultRepo("pkg"), buildfile, getPackageManager(), reporter);
+
+    group = (EnvironmentGroup) pkg.getTarget("group");
+  }
+
+  @Test
+  public void testGroupMembership() throws Exception {
+    assertEquals(
+        ImmutableSet.of(
+            Label.parseAbsolute("//pkg:foo"),
+            Label.parseAbsolute("//pkg:bar"),
+            Label.parseAbsolute("//pkg:baz")),
+        group.getEnvironments());
+  }
+
+  @Test
+  public void testDefaultsMembership() throws Exception {
+    assertEquals(ImmutableSet.of(Label.parseAbsolute("//pkg:foo")), group.getDefaults());
+  }
+
+  @Test
+  public void testIsDefault() throws Exception {
+    assertTrue(group.isDefault(Label.parseAbsolute("//pkg:foo")));
+    assertFalse(group.isDefault(Label.parseAbsolute("//pkg:bar")));
+    assertFalse(group.isDefault(Label.parseAbsolute("//pkg:baz")));
+    assertFalse(group.isDefault(Label.parseAbsolute("//pkg:not_in_group")));
+  }
+
+  @Test
+  public void testFulfillers() throws Exception {
+    assertThat(group.getFulfillers(Label.parseAbsolute("//pkg:baz")))
+        .containsExactly(Label.parseAbsolute("//pkg:foo"), Label.parseAbsolute("//pkg:bar"));
+    assertThat(group.getFulfillers(Label.parseAbsolute("//pkg:bar")))
+        .containsExactly(Label.parseAbsolute("//pkg:foo"));
+    assertThat(group.getFulfillers(Label.parseAbsolute("//pkg:foo"))).isEmpty();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java b/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
index 5910397..75124fb 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
@@ -1,4 +1,4 @@
-// Copyright 2006 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/ExternalPackageTest.java b/src/test/java/com/google/devtools/build/lib/packages/ExternalPackageTest.java
new file mode 100644
index 0000000..3d83246
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/ExternalPackageTest.java
@@ -0,0 +1,86 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Package.Builder;
+import com.google.devtools.build.lib.syntax.Argument;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Identifier;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.Map;
+
+/**
+ * Test for building external packages.
+ */
+public class ExternalPackageTest extends BuildViewTestCase {
+
+  private Path workspacePath;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    workspacePath = getOutputPath().getRelative("WORKSPACE");
+  }
+
+  public void testWorkspaceName() {
+    Builder builder = Package.newExternalPackageBuilder(workspacePath, "TESTING");
+    builder.setWorkspaceName("foo");
+    assertEquals("foo", builder.build().getWorkspaceName());
+  }
+
+  public void testMultipleRulesWithSameName() throws Exception {
+    Builder builder = Package.newExternalPackageBuilder(workspacePath, "TESTING");
+
+    // The WORKSPACE file allows rules to be overridden, but the TestRuleClassProvider doesn't
+    // provide WORKSPACE rules (new_local_repo et al). So for the test, we create an
+    // ExternalPackage with BUILD rules, even though these rules wouldn't ordinarily be added to
+    // ExternalPackage.
+    Location buildFile = Location.fromFile(getOutputPath().getRelative("BUILD"));
+
+    // Add first rule.
+    RuleClass ruleClass =
+        TestRuleClassProvider.getRuleClassProvider().getRuleClassMap().get("cc_library");
+    RuleClass bindRuleClass =
+        TestRuleClassProvider.getRuleClassProvider().getRuleClassMap().get("bind");
+
+    Map<String, Object> kwargs = ImmutableMap.of("name", (Object) "my-rule");
+    FuncallExpression ast =
+        new FuncallExpression(
+            new Identifier(ruleClass.getName()), Lists.<Argument.Passed>newArrayList());
+    ast.setLocation(buildFile);
+    builder
+        .externalPackageData()
+        .createAndAddRepositoryRule(builder, ruleClass, bindRuleClass, kwargs, ast);
+
+    // Add another rule with the same name.
+    ruleClass = TestRuleClassProvider.getRuleClassProvider().getRuleClassMap().get("sh_test");
+    ast =
+        new FuncallExpression(
+            new Identifier(ruleClass.getName()), Lists.<Argument.Passed>newArrayList());
+    ast.setLocation(buildFile);
+    builder
+        .externalPackageData()
+        .createAndAddRepositoryRule(builder, ruleClass, bindRuleClass, kwargs, ast);
+    Package pkg = builder.build();
+
+    // Make sure the second rule "wins."
+    assertEquals("sh_test rule", pkg.getTarget("my-rule").getTargetKind());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
index 4e451e3..64e028d 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -1,4 +1,4 @@
-// Copyright 2008 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunctionTest.java
new file mode 100644
index 0000000..32719df
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunctionTest.java
@@ -0,0 +1,220 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.AttributeValueGetter;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tests for {@link ImplicitOutputsFunction}.
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+@RunWith(JUnit4.class)
+public final class ImplicitOutputsFunctionTest {
+  private void assertPlaceholderCollection(
+      String template, String expectedTemplate, String... expectedPlaceholders) throws Exception {
+    List<String> actualPlaceholders = new ArrayList<>();
+    assertEquals(
+        expectedTemplate,
+        ImplicitOutputsFunction.createPlaceholderSubstitutionFormatString(
+            template, actualPlaceholders));
+    assertThat(actualPlaceholders)
+        .containsExactlyElementsIn(Arrays.asList(expectedPlaceholders))
+        .inOrder();
+  }
+
+  @Test
+  public void testNoPlaceholder() throws Exception {
+    assertPlaceholderCollection("foo", "foo");
+  }
+
+  @Test
+  public void testJustPlaceholder() throws Exception {
+    assertPlaceholderCollection("%{foo}", "%s", "foo");
+  }
+
+  @Test
+  public void testPrefixedPlaceholder() throws Exception {
+    assertPlaceholderCollection("foo%{bar}", "foo%s", "bar");
+  }
+
+  @Test
+  public void testSuffixedPlaceholder() throws Exception {
+    assertPlaceholderCollection("%{foo}bar", "%sbar", "foo");
+  }
+
+  @Test
+  public void testMultiplePlaceholdersPrefixed() throws Exception {
+    assertPlaceholderCollection("foo%{bar}baz%{qux}", "foo%sbaz%s", "bar", "qux");
+  }
+
+  @Test
+  public void testMultiplePlaceholdersSuffixed() throws Exception {
+    assertPlaceholderCollection("%{foo}bar%{baz}qux", "%sbar%squx", "foo", "baz");
+  }
+
+  @Test
+  public void testTightlyPackedPlaceholders() throws Exception {
+    assertPlaceholderCollection("%{foo}%{bar}%{baz}", "%s%s%s", "foo", "bar", "baz");
+  }
+
+  @Test
+  public void testIncompletePlaceholder() throws Exception {
+    assertPlaceholderCollection("%{foo", "%%{foo");
+  }
+
+  @Test
+  public void testCompleteAndIncompletePlaceholder() throws Exception {
+    assertPlaceholderCollection("%{foo}%{bar", "%s%%{bar", "foo");
+  }
+
+  @Test
+  public void testPlaceholderLooksLikeNestedIncompletePlaceholder() throws Exception {
+    assertPlaceholderCollection("%{%{foo", "%%{%%{foo");
+  }
+
+  @Test
+  public void testPlaceholderLooksLikeNestedPlaceholder() throws Exception {
+    assertPlaceholderCollection("%{%{foo}", "%s", "%{foo");
+  }
+
+  @Test
+  public void testEscapesJustPercentSign() throws Exception {
+    assertPlaceholderCollection("%", "%%");
+  }
+
+  @Test
+  public void testEscapesPrintfPlaceholder() throws Exception {
+    assertPlaceholderCollection("%{x}%s%{y}", "%s%%s%s", "x", "y");
+  }
+
+  @Test
+  public void testEscapesPercentSign() throws Exception {
+    assertPlaceholderCollection("foo%{bar}%baz", "foo%s%%baz", "bar");
+  }
+
+  private static AttributeValueGetter attrs(
+      final Map<String, ? extends Collection<String>> values) {
+    return new AttributeValueGetter() {
+      @Override
+      public Set<String> get(AttributeMap ignored, String attr) {
+        return new LinkedHashSet<>(Preconditions.checkNotNull(values.get(attr)));
+      }
+    };
+  }
+
+  private void assertPlaceholderSubtitution(
+      String template,
+      AttributeValueGetter attrValues,
+      String[] expectedSubstitutions,
+      String[] expectedFoundPlaceholders)
+      throws Exception {
+    List<String> foundAttributes = new ArrayList<>();
+    List<String> substitutions =
+        ImplicitOutputsFunction.substitutePlaceholderIntoTemplate(
+            template, null, attrValues, foundAttributes);
+    assertThat(foundAttributes)
+        .containsExactlyElementsIn(Arrays.asList(expectedFoundPlaceholders))
+        .inOrder();
+    assertThat(substitutions).containsExactlyElementsIn(Arrays.asList(expectedSubstitutions));
+  }
+
+  @Test
+  public void testSingleScalarElementSubstitution() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}",
+        attrs(ImmutableMap.of("x", ImmutableList.of("a"))),
+        new String[] {"a"},
+        new String[] {"x"});
+  }
+
+  @Test
+  public void testSingleVectorElementSubstitution() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}",
+        attrs(ImmutableMap.of("x", ImmutableList.of("a", "b", "c"))),
+        new String[] {"a", "b", "c"},
+        new String[] {"x"});
+  }
+
+  @Test
+  public void testMultipleElementsSubstitution() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}-%{y}-%{z}",
+        attrs(
+            ImmutableMap.of(
+                "x", ImmutableList.of("foo", "bar", "baz"),
+                "y", ImmutableList.of("meow"),
+                "z", ImmutableList.of("1", "2"))),
+        new String[] {
+          "foo-meow-1", "foo-meow-2", "bar-meow-1", "bar-meow-2", "baz-meow-1", "baz-meow-2"
+        },
+        new String[] {"x", "y", "z"});
+  }
+
+  @Test
+  public void testEmptyElementSubstitution() throws Exception {
+    assertPlaceholderSubtitution(
+        "a-%{x}",
+        attrs(ImmutableMap.of("x", ImmutableList.<String>of())),
+        new String[0],
+        new String[] {"x"});
+  }
+
+  @Test
+  public void testSamePlaceholderMultipleTimes() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}-%{y}-%{x}",
+        attrs(ImmutableMap.of("x", ImmutableList.of("a", "b"), "y", ImmutableList.of("1", "2"))),
+        new String[] {"a-1-a", "a-1-b", "a-2-a", "a-2-b", "b-1-a", "b-1-b", "b-2-a", "b-2-b"},
+        new String[] {"x", "y", "x"});
+  }
+
+  @Test
+  public void testRepeatingPlaceholderValue() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}",
+        attrs(ImmutableMap.of("x", ImmutableList.of("a", "a"))),
+        new String[] {"a"},
+        new String[] {"x"});
+  }
+
+  @Test
+  public void testIncompletePlaceholderTreatedAsText() throws Exception {
+    assertPlaceholderSubtitution(
+        "%{x}-%{y-%{z",
+        attrs(ImmutableMap.of("x", ImmutableList.of("a", "b"))),
+        new String[] {"a-%{y-%{z", "b-%{y-%{z"},
+        new String[] {"x"});
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/InputFileTest.java b/src/test/java/com/google/devtools/build/lib/packages/InputFileTest.java
new file mode 100644
index 0000000..e56a212
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/InputFileTest.java
@@ -0,0 +1,105 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.util.PackageFactoryApparatus;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.Path;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * A test for {@link InputFile}.
+ */
+@RunWith(JUnit4.class)
+public class InputFileTest {
+
+  private Path pathX;
+  private Path pathY;
+  private Package pkg;
+
+  private EventCollectionApparatus events = new EventCollectionApparatus();
+  private Scratch scratch = new Scratch("/workspace");
+  private PackageFactoryApparatus packages = new PackageFactoryApparatus(events.reporter());
+
+  @Before
+  public void setUp() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "pkg/BUILD",
+            "genrule(name = 'dummy', ",
+            "        cmd = '', ",
+            "        outs = [], ",
+            "        srcs = ['x', 'subdir/y'])");
+    pkg = packages.createPackage("pkg", buildfile);
+    events.assertNoEvents();
+
+    this.pathX = scratch.file("pkg/x", "blah");
+    this.pathY = scratch.file("pkg/subdir/y", "blah blah");
+  }
+
+  private static void checkPathMatches(InputFile input, Path expectedPath) {
+    assertEquals(expectedPath, input.getPath());
+  }
+
+  private static void checkName(InputFile input, String expectedName) {
+    assertEquals(expectedName, input.getName());
+  }
+
+  private static void checkLabel(InputFile input, String expectedLabelString) {
+    assertEquals(expectedLabelString, input.getLabel().toString());
+  }
+
+  @Test
+  public void testGetAssociatedRule() throws Exception {
+    assertNull(null, pkg.getTarget("x").getAssociatedRule());
+  }
+
+  @Test
+  public void testInputFileInPackageDirectory() throws NoSuchTargetException {
+    InputFile inputFileX = (InputFile) pkg.getTarget("x");
+    checkPathMatches(inputFileX, pathX);
+    checkName(inputFileX, "x");
+    checkLabel(inputFileX, "//pkg:x");
+    assertEquals("source file", inputFileX.getTargetKind());
+  }
+
+  @Test
+  public void testInputFileInSubdirectory() throws NoSuchTargetException {
+    InputFile inputFileY = (InputFile) pkg.getTarget("subdir/y");
+    checkPathMatches(inputFileY, pathY);
+    checkName(inputFileY, "subdir/y");
+    checkLabel(inputFileY, "//pkg:subdir/y");
+  }
+
+  @Test
+  public void testEquivalenceRelation() throws NoSuchTargetException {
+    InputFile inputFileX = (InputFile) pkg.getTarget("x");
+    assertSame(pkg.getTarget("x"), inputFileX);
+    InputFile inputFileY = (InputFile) pkg.getTarget("subdir/y");
+    assertSame(pkg.getTarget("subdir/y"), inputFileY);
+    assertEquals(inputFileX, inputFileX);
+    assertFalse(inputFileX.equals(inputFileY));
+    assertFalse(inputFileY.equals(inputFileX));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/LicenseTest.java b/src/test/java/com/google/devtools/build/lib/packages/LicenseTest.java
new file mode 100644
index 0000000..0032dc8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/LicenseTest.java
@@ -0,0 +1,40 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.devtools.build.lib.packages.License.LicenseType;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class LicenseTest {
+
+  @Test
+  public void testLeastRestrictive() {
+    assertEquals(
+        LicenseType.RESTRICTED, License.leastRestrictive(Arrays.asList(LicenseType.RESTRICTED)));
+    assertEquals(
+        LicenseType.RESTRICTED,
+        License.leastRestrictive(
+            Arrays.asList(LicenseType.RESTRICTED, LicenseType.BY_EXCEPTION_ONLY)));
+    assertEquals(
+        LicenseType.BY_EXCEPTION_ONLY, License.leastRestrictive(Arrays.<LicenseType>asList()));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/OutputFileTest.java b/src/test/java/com/google/devtools/build/lib/packages/OutputFileTest.java
new file mode 100644
index 0000000..1e26e8a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/OutputFileTest.java
@@ -0,0 +1,206 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.vfs.Path;
+
+public class OutputFileTest extends PackageLoadingTestCase {
+
+  private PackageFactory packageFactory;
+  private Package pkg;
+  private Rule rule;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    packageFactory = new PackageFactory(TestRuleClassProvider.getRuleClassProvider());
+
+    Path buildfile =
+        scratch.file(
+            "pkg/BUILD",
+            "genrule(name='foo', ",
+            "        srcs=[], ",
+            "        cmd='', ",
+            "        outs=['x', 'subdir/y'])");
+    this.pkg =
+        packageFactory.createPackageForTesting(
+            PackageIdentifier.createInDefaultRepo("pkg"), buildfile, getPackageManager(), reporter);
+    assertNoEvents();
+
+    this.rule = (Rule) pkg.getTarget("foo");
+  }
+
+  private void checkTargetRetainsGeneratingRule(OutputFile output) throws Exception {
+    assertSame(rule, output.getGeneratingRule());
+  }
+
+  private void checkName(OutputFile output, String expectedName) throws Exception {
+    assertEquals(expectedName, output.getName());
+  }
+
+  private void checkLabel(OutputFile output, String expectedLabelString) throws Exception {
+    assertEquals(expectedLabelString, output.getLabel().toString());
+  }
+
+  public void testGetAssociatedRule() throws Exception {
+    assertSame(rule, pkg.getTarget("x").getAssociatedRule());
+  }
+
+  public void testOutputFileInPackageDir() throws Exception {
+    OutputFile outputFileX = (OutputFile) pkg.getTarget("x");
+    checkTargetRetainsGeneratingRule(outputFileX);
+    checkName(outputFileX, "x");
+    checkLabel(outputFileX, "//pkg:x");
+    assertEquals("generated file", outputFileX.getTargetKind());
+  }
+
+  public void testOutputFileInSubdirectory() throws Exception {
+    OutputFile outputFileY = (OutputFile) pkg.getTarget("subdir/y");
+    checkTargetRetainsGeneratingRule(outputFileY);
+    checkName(outputFileY, "subdir/y");
+    checkLabel(outputFileY, "//pkg:subdir/y");
+  }
+
+  public void testEquivalenceRelation() throws Exception {
+    OutputFile outputFileX1 = (OutputFile) pkg.getTarget("x");
+    OutputFile outputFileX2 = (OutputFile) pkg.getTarget("x");
+    OutputFile outputFileY1 = (OutputFile) pkg.getTarget("subdir/y");
+    OutputFile outputFileY2 = (OutputFile) pkg.getTarget("subdir/y");
+    assertSame(outputFileX1, outputFileX2);
+    assertSame(outputFileY1, outputFileY2);
+    assertEquals(outputFileX1, outputFileX2);
+    assertEquals(outputFileX2, outputFileX1);
+    assertEquals(outputFileY1, outputFileY2);
+    assertEquals(outputFileY2, outputFileY1);
+    assertFalse(outputFileX1.equals(outputFileY1));
+    assertFalse(outputFileY1.equals(outputFileX1));
+    assertEquals(outputFileX1.hashCode(), outputFileX2.hashCode());
+    assertEquals(outputFileY1.hashCode(), outputFileY2.hashCode());
+  }
+
+  public void testDuplicateOutputFilesInDifferentRules() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "two_outs/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls >$(location out)',",
+            "        outs=['out'])",
+            "",
+            "genrule(name='b',",
+            "        cmd='ls >$(location out)',",
+            "        outs=['out'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("two_outs"),
+        buildfile,
+        getPackageManager(),
+        reporter);
+    assertContainsEvent(
+        "generated file 'out' in rule 'b' conflicts with "
+            + "existing generated file from rule 'a'");
+  }
+
+  public void testOutputFileNameConflictsWithExistingRule() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "out_is_rule/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls >$(location out)',",
+            "        outs=['out'])",
+            "",
+            "genrule(name='b',",
+            "        cmd='ls >$(location out)',",
+            "        outs=['a'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("out_is_rule"),
+        buildfile,
+        getPackageManager(),
+        reporter);
+    assertContainsEvent("generated file 'a' in rule 'b' conflicts with existing genrule rule");
+  }
+
+  public void testDuplicateOutputFilesInSameRule() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "two_outs/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls >$(location out)',",
+            "        outs=['out', 'out'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("two_outs"),
+        buildfile,
+        getPackageManager(),
+        reporter);
+    assertContainsEvent(
+        "generated file 'out' in rule 'a' conflicts with "
+            + "existing generated file from rule 'a'");
+  }
+
+  public void testOutputFileWithIllegalName() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "bad_out_name/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls',",
+            "        outs=['!@#'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("bad_out_name"),
+        buildfile,
+        getPackageManager(),
+        reporter);
+    assertContainsEvent("illegal output file name '!@#' in rule //bad_out_name:a");
+  }
+
+  public void testOutputFileWithCrossPackageLabel() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "cross_package_out/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls',",
+            "        outs=['//foo:bar'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("cross_package_out"),
+        buildfile,
+        getPackageManager(),
+        reporter);
+    assertContainsEvent("label '//foo:bar' is not in the current package");
+  }
+
+  public void testOutputFileNamedBUILD() throws Exception {
+    Path buildfile =
+        scratch.file(
+            "output_called_build/BUILD",
+            "genrule(name='a',",
+            "        cmd='ls',",
+            "        outs=['BUILD'])");
+
+    reporter.removeHandler(failFastHandler);
+    packageFactory.createPackageForTesting(
+        PackageIdentifier.createInDefaultRepo("output_called_build"), buildfile,
+        getPackageManager(), reporter);
+    assertContainsEvent("generated file 'BUILD' in rule 'a' conflicts with existing source file");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageGroupStaticInitializationTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageGroupStaticInitializationTest.java
new file mode 100644
index 0000000..c527a10
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageGroupStaticInitializationTest.java
@@ -0,0 +1,95 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertFalse;
+
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.util.PackageFactoryApparatus;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.SynchronousQueue;
+
+/**
+ * Checks against a class initialization deadlock. "query sometimes hangs".
+ *
+ * <p>This requires static initialization of PackageGroup and PackageSpecification
+ * to occur in a multithreaded context, and therefore must be in its own class.
+ */
+@RunWith(JUnit4.class)
+public class PackageGroupStaticInitializationTest {
+  private Scratch scratch = new Scratch("/workspace");
+  private EventCollectionApparatus events = new EventCollectionApparatus();
+  private PackageFactoryApparatus packages = new PackageFactoryApparatus(events.reporter());
+
+  @Test
+  public void testNoDeadlockOnPackageGroupCreation() throws Exception {
+    scratch.file("fruits/BUILD", "package_group(name = 'mango', packages = ['//...'])");
+
+    final SynchronousQueue<PackageSpecification> groupQueue = new SynchronousQueue<>();
+    Thread producingThread =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                try {
+                  groupQueue.put(PackageSpecification.fromString("//fruits/..."));
+                } catch (Exception e) {
+                  // Can't throw from Runnable, but this will cause the test to timeout
+                  // when the consumer can't take the object.
+                  e.printStackTrace();
+                }
+              }
+            });
+
+    Thread consumingThread =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                try {
+                  getPackageGroup("fruits", "mango");
+                  groupQueue.take();
+                } catch (Exception e) {
+                  // Can't throw from Runnable, but this will cause the test to timeout
+                  // when the producer can't put the object.
+                  e.printStackTrace();
+                }
+              }
+            });
+
+    consumingThread.start();
+    producingThread.start();
+    producingThread.join(3000);
+    consumingThread.join(3000);
+    assertFalse(producingThread.isAlive());
+    assertFalse(consumingThread.isAlive());
+  }
+
+  private Package getPackage(String packageName) throws Exception {
+    PathFragment buildFileFragment = new PathFragment(packageName).getRelative("BUILD");
+    Path buildFile = scratch.resolve(buildFileFragment.getPathString());
+    return packages.createPackage(packageName, buildFile);
+  }
+
+  private PackageGroup getPackageGroup(String pkg, String name) throws Exception {
+    return (PackageGroup) getPackage(pkg).getTarget(name);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageGroupTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageGroupTest.java
new file mode 100644
index 0000000..9d501fc
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageGroupTest.java
@@ -0,0 +1,143 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.util.PackageFactoryApparatus;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for PackageGroup.
+ */
+@RunWith(JUnit4.class)
+public class PackageGroupTest {
+  private Scratch scratch = new Scratch("/workspace");
+  private EventCollectionApparatus events = new EventCollectionApparatus();
+  private PackageFactoryApparatus packages = new PackageFactoryApparatus(events.reporter());
+
+  @Test
+  public void testDoesNotFailHorribly() throws Exception {
+    scratch.file("fruits/BUILD", "package_group(name = 'apple', packages = ['//random'])");
+
+    getPackageGroup("fruits", "apple");
+  }
+
+  // Regression test for: "Package group with empty name causes Blaze exception"
+  @Test
+  public void testEmptyPackageGroupNameDoesNotThrow() throws Exception {
+    scratch.file("strawberry/BUILD", "package_group(name = '', packages=[])");
+
+    events.setFailFast(false);
+    getPackage("strawberry");
+    events.assertContainsEvent("package group has invalid name");
+  }
+
+  @Test
+  public void testAbsolutePackagesWork() throws Exception {
+    scratch.file(
+        "fruits/BUILD",
+        "package_group(name = 'apple',",
+        "              packages = ['//vegetables'])");
+
+    scratch.file("vegetables/BUILD");
+    scratch.file("fruits/vegetables/BUILD");
+
+    PackageGroup grp = getPackageGroup("fruits", "apple");
+    assertTrue(grp.contains(getPackage("vegetables")));
+    assertFalse(grp.contains(getPackage("fruits/vegetables")));
+  }
+
+  @Test
+  public void testPackagesWithoutDoubleSlashDoNotWork() throws Exception {
+    scratch.file(
+        "fruits/BUILD",
+        "package_group(name = 'apple',",
+        "              packages = ['vegetables'])");
+
+    scratch.file("vegetables/BUILD");
+    scratch.file("fruits/vegetables/BUILD");
+
+    events.setFailFast(false);
+    getPackageGroup("fruits", "apple");
+    events.assertContainsEvent("invalid package label: vegetables");
+  }
+
+  @Test
+  public void testTargetNameAsPackageDoesNotWork1() throws Exception {
+    scratch.file(
+        "fruits/BUILD",
+        "package_group(name = 'apple',",
+        "              packages = ['//vegetables:carrot'])");
+
+    scratch.file("vegetables/BUILD");
+    scratch.file("fruits/vegetables/BUILD");
+
+    events.setFailFast(false);
+    getPackageGroup("fruits", "apple");
+    events.assertContainsEvent("invalid package label: //vegetables:carrot");
+  }
+
+  @Test
+  public void testTargetNameAsPackageDoesNotWork2() throws Exception {
+    scratch.file(
+        "fruits/BUILD", "package_group(name = 'apple',", "              packages = [':carrot'])");
+
+    scratch.file("vegetables/BUILD");
+    scratch.file("fruits/vegetables/BUILD");
+
+    events.setFailFast(false);
+    getPackageGroup("fruits", "apple");
+    events.assertContainsEvent("invalid package label: :carrot");
+  }
+
+  @Test
+  public void testAllBeneathSpecificationWorks() throws Exception {
+    scratch.file(
+        "fruits/BUILD",
+        "package_group(name = 'maracuja',",
+        "              packages = ['//tropics/...'])");
+
+    getPackageGroup("fruits", "maracuja");
+  }
+
+  @Test
+  public void testEverythingSpecificationWorks() throws Exception {
+    scratch.file("fruits/BUILD", "package_group(name = 'mango', packages = ['//...'])");
+    PackageGroup packageGroup = getPackageGroup("fruits", "mango");
+    MoreAsserts.assertSameContents(
+        ImmutableList.of(PackageSpecification.EVERYTHING), packageGroup.getPackageSpecifications());
+  }
+
+  private Package getPackage(String packageName) throws Exception {
+    PathFragment buildFileFragment = new PathFragment(packageName).getRelative("BUILD");
+
+    Path buildFile = scratch.resolve(buildFileFragment.getPathString());
+    return packages.createPackage(packageName, buildFile);
+  }
+
+  private PackageGroup getPackageGroup(String pkg, String name) throws Exception {
+    return (PackageGroup) getPackage(pkg).getTarget(name);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/ProtoUtilsTest.java b/src/test/java/com/google/devtools/build/lib/packages/ProtoUtilsTest.java
new file mode 100644
index 0000000..a18d662
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/ProtoUtilsTest.java
@@ -0,0 +1,54 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.Attribute.Discriminator;
+import com.google.devtools.build.lib.syntax.Type;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collection;
+import java.util.Map.Entry;
+
+/** Tests for values and functions in ProtoUtils. */
+@RunWith(JUnit4.class)
+public class ProtoUtilsTest {
+
+  @Test
+  public void testTypeMap() throws Exception {
+    // The ProtoUtils TYPE_MAP (and its inverse, INVERSE_TYPE_MAP) are used to translate between
+    // rule attribute types and Discriminator values used to encode them in GPB messages. For each
+    // discriminator value there must be exactly one type, or there must be exactly two types, one
+    // which is a nodep type and the other which is not.
+    ImmutableSet<Entry<Discriminator, Collection<Type<?>>>> inverseMapEntries =
+        ProtoUtils.INVERSE_TYPE_MAP.asMap().entrySet();
+    for (Entry<Discriminator, Collection<Type<?>>> entry : inverseMapEntries) {
+      ImmutableSet<Type<?>> types = ImmutableSet.copyOf(entry.getValue());
+      String assertionMessage =
+          String.format(
+              "Cannot map from discriminator \"%s\" to exactly one Type.",
+              entry.getKey().toString());
+      boolean exactlyOneType = types.size() == 1;
+      boolean twoTypesDistinguishableUsingNodepHint =
+          types.size() == 2 && Sets.difference(types, ProtoUtils.NODEP_TYPES).size() == 1;
+      assertTrue(assertionMessage, exactlyOneType || twoTypesDistinguishableUsingNodepHint);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RelativePackageNameResolverTest.java b/src/test/java/com/google/devtools/build/lib/packages/RelativePackageNameResolverTest.java
new file mode 100644
index 0000000..9f2f9b6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/RelativePackageNameResolverTest.java
@@ -0,0 +1,122 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link RelativePackageNameResolver}.
+ */
+@RunWith(JUnit4.class)
+public class RelativePackageNameResolverTest {
+  private RelativePackageNameResolver resolver;
+
+  @Test
+  public void testRelativePackagesBelowOneLevelWork() throws Exception {
+    createResolver("foo", true);
+    assertResolvesTo("bar", "foo/bar");
+
+    createResolver("foo/bar", true);
+    assertResolvesTo("pear", "foo/bar/pear");
+  }
+
+  @Test
+  public void testRelativePackagesBelowTwoLevelsWork() throws Exception {
+    createResolver("foo/bar", true);
+    assertResolvesTo("pear", "foo/bar/pear");
+  }
+
+  @Test
+  public void testRelativePackagesAboveOneLevelWork() throws Exception {
+    createResolver("foo", true);
+    assertResolvesTo("../bar", "bar");
+  }
+
+  @Test
+  public void testRelativePackagesAboveTwoLevelsWork() throws Exception {
+    createResolver("foo/bar", true);
+    assertResolvesTo("../../apple", "apple");
+  }
+
+  @Test
+  public void testSimpleAbsolutePackagesWork() throws Exception {
+    createResolver("foo", true);
+
+    assertResolvesTo("//foo", "foo");
+    assertResolvesTo("//foo/bar", "foo/bar");
+  }
+
+  @Test
+  public void testBuildNotRemoved() throws Exception {
+    createResolver("foo", false);
+
+    assertResolvesTo("bar/BUILD", "foo/bar/BUILD");
+  }
+
+  @Test
+  public void testBuildRemoved() throws Exception {
+    createResolver("foo", true);
+
+    assertResolvesTo("bar/BUILD", "foo/bar");
+  }
+
+  @Test
+  public void testEmptyOffset() throws Exception {
+    createResolver("", true);
+
+    assertResolvesTo("bar", "bar");
+    assertResolvesTo("bar/qux", "bar/qux");
+  }
+
+  @Test
+  public void testTooFarUpwardsOneLevelThrows() throws Exception {
+    createResolver("foo", true);
+
+    try {
+      resolver.resolve("../../bar");
+      fail("InvalidPackageNameException expected");
+    } catch (InvalidPackageNameException e) {
+      // good
+    }
+  }
+
+  @Test
+  public void testTooFarUpwardsTwoLevelsThrows() throws Exception {
+    createResolver("foo/bar", true);
+    assertResolvesTo("../../orange", "orange");
+
+    try {
+      resolver.resolve("../../../orange");
+      fail("InvalidPackageNameException expected");
+    } catch (InvalidPackageNameException e) {
+      // good
+    }
+  }
+
+  private void createResolver(String offset, boolean discardBuild) {
+    resolver = new RelativePackageNameResolver(new PathFragment(offset), discardBuild);
+  }
+
+  private void assertResolvesTo(String relative, String expectedAbsolute) throws Exception {
+    String result = resolver.resolve(relative);
+    assertEquals(expectedAbsolute, result);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java
new file mode 100644
index 0000000..67375e3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java
@@ -0,0 +1,177 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.syntax.Type.BOOLEAN;
+import static com.google.devtools.build.lib.syntax.Type.INTEGER;
+import static com.google.devtools.build.lib.syntax.Type.STRING;
+import static com.google.devtools.build.lib.syntax.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+
+/**
+ * Tests for the {@link RuleClass.Builder}.
+ */
+public class RuleClassBuilderTest extends PackageLoadingTestCase {
+  private static final RuleClass.ConfiguredTargetFactory<Object, Object>
+      DUMMY_CONFIGURED_TARGET_FACTORY =
+          new RuleClass.ConfiguredTargetFactory<Object, Object>() {
+            @Override
+            public Object create(Object ruleContext) throws InterruptedException {
+              throw new IllegalStateException();
+            }
+          };
+
+  public void testRuleClassBuilderBasics() throws Exception {
+    RuleClass ruleClassA =
+        new RuleClass.Builder("ruleA", RuleClassType.NORMAL, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .add(attr("srcs", BuildType.LABEL_LIST).legacyAllowAnyFileType())
+            .add(attr("tags", STRING_LIST))
+            .add(attr("X", com.google.devtools.build.lib.syntax.Type.INTEGER).mandatory())
+            .build();
+
+    assertEquals("ruleA", ruleClassA.getName());
+    assertEquals(3, ruleClassA.getAttributeCount());
+    assertTrue(ruleClassA.hasBinaryOutput());
+
+    assertEquals(0, (int) ruleClassA.getAttributeIndex("srcs"));
+    assertEquals(ruleClassA.getAttribute(0), ruleClassA.getAttributeByName("srcs"));
+
+    assertEquals(1, (int) ruleClassA.getAttributeIndex("tags"));
+    assertEquals(ruleClassA.getAttribute(1), ruleClassA.getAttributeByName("tags"));
+
+    assertEquals(2, (int) ruleClassA.getAttributeIndex("X"));
+    assertEquals(ruleClassA.getAttribute(2), ruleClassA.getAttributeByName("X"));
+  }
+
+  public void testRuleClassBuilderTestIsBinary() throws Exception {
+    RuleClass ruleClassA =
+        new RuleClass.Builder("rule_test", RuleClassType.TEST, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .add(attr("tags", STRING_LIST))
+            .add(attr("size", STRING).value("medium"))
+            .add(attr("timeout", STRING))
+            .add(attr("flaky", BOOLEAN).value(false))
+            .add(attr("shard_count", INTEGER).value(-1))
+            .add(attr("local", BOOLEAN))
+            .build();
+    assertTrue(ruleClassA.hasBinaryOutput());
+  }
+
+  public void testRuleClassBuilderGenruleIsNotBinary() throws Exception {
+    RuleClass ruleClassA =
+        new RuleClass.Builder("ruleA", RuleClassType.NORMAL, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .setOutputToGenfiles()
+            .add(attr("tags", STRING_LIST))
+            .build();
+    assertFalse(ruleClassA.hasBinaryOutput());
+  }
+
+  public void testRuleClassTestNameValidity() throws Exception {
+    try {
+      new RuleClass.Builder("ruleA", RuleClassType.TEST, false).build();
+      fail();
+    } catch (IllegalArgumentException e) {
+      // Expected exception.
+    }
+  }
+
+  public void testRuleClassNormalNameValidity() throws Exception {
+    try {
+      new RuleClass.Builder("ruleA_test", RuleClassType.NORMAL, false).build();
+      fail();
+    } catch (IllegalArgumentException e) {
+      // Expected exception.
+    }
+  }
+
+  public void testDuplicateAttribute() throws Exception {
+    RuleClass.Builder builder =
+        new RuleClass.Builder("ruleA", RuleClassType.NORMAL, false).add(attr("a", STRING));
+    try {
+      builder.add(attr("a", STRING));
+      fail();
+    } catch (IllegalStateException e) {
+      // Expected exception.
+    }
+  }
+
+  public void testPropertiesOfAbstractRuleClass() throws Exception {
+    try {
+      new RuleClass.Builder("$ruleA", RuleClassType.ABSTRACT, false).setOutputToGenfiles();
+      fail();
+    } catch (IllegalStateException e) {
+      // Expected exception.
+    }
+
+    try {
+      new RuleClass.Builder("$ruleB", RuleClassType.ABSTRACT, false)
+          .setImplicitOutputsFunction(null);
+      fail();
+    } catch (IllegalStateException e) {
+      // Expected exception.
+    }
+  }
+
+  public void testDuplicateInheritedAttribute() throws Exception {
+    RuleClass a =
+        new RuleClass.Builder("ruleA", RuleClassType.NORMAL, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .add(attr("a", STRING).value("A"))
+            .add(attr("tags", STRING_LIST))
+            .build();
+    RuleClass b =
+        new RuleClass.Builder("ruleB", RuleClassType.NORMAL, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .add(attr("a", STRING).value("B"))
+            .add(attr("tags", STRING_LIST))
+            .build();
+    try {
+      // In case of multiple attribute inheritance the attributes must equal
+      new RuleClass.Builder("ruleC", RuleClassType.NORMAL, false, a, b).build();
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Attribute a is inherited multiple times in ruleC ruleclass");
+    }
+  }
+
+  public void testRemoveAttribute() throws Exception {
+    RuleClass a =
+        new RuleClass.Builder("rule", RuleClassType.NORMAL, false)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+            .add(attr("a", STRING))
+            .add(attr("b", STRING))
+            .add(attr("tags", STRING_LIST))
+            .build();
+    RuleClass.Builder builder =
+        new RuleClass.Builder("c", RuleClassType.NORMAL, false, a)
+            .factory(DUMMY_CONFIGURED_TARGET_FACTORY);
+    RuleClass c = builder.removeAttribute("a").add(attr("a", INTEGER)).removeAttribute("b").build();
+    assertFalse(c.hasAttr("a", STRING));
+    assertTrue(c.hasAttr("a", INTEGER));
+    assertFalse(c.hasAttr("b", STRING));
+
+    try {
+      builder.removeAttribute("c");
+      fail();
+    } catch (IllegalStateException e) {
+      // Expected exception.
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java
new file mode 100644
index 0000000..a495ea0
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java
@@ -0,0 +1,245 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.syntax.Type;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class RuleFactoryTest extends PackageLoadingTestCase {
+
+  private ConfiguredRuleClassProvider provider = TestRuleClassProvider.getRuleClassProvider();
+  private RuleFactory ruleFactory = new RuleFactory(provider);
+
+  public static final Location LOCATION_42 = Location.fromFileAndOffsets(null, 42, 42);
+
+  public void testCreateRule() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
+    Package.Builder pkgBuilder =
+        new Package.Builder(PackageIdentifier.createInDefaultRepo("mypkg"), "TESTING")
+            .setFilename(myPkgPath)
+            .setMakeEnv(new MakeEnvironment.Builder());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("name", "foo");
+    attributeValues.put("alwayslink", true);
+
+    Rule rule =
+        RuleFactory.createAndAddRule(
+            pkgBuilder,
+            provider.getRuleClassMap().get("cc_library"),
+            attributeValues,
+            new Reporter(),
+            /*ast=*/ null,
+            LOCATION_42,
+            /*env=*/ null);
+
+    assertSame(rule, rule.getAssociatedRule());
+
+    // pkg.getRules() = [rule]
+    Package pkg = pkgBuilder.build();
+    assertThat(Sets.newHashSet(pkg.getTargets(Rule.class))).hasSize(1);
+    assertEquals(rule, pkg.getTargets(Rule.class).iterator().next());
+
+    assertSame(rule, pkg.getTarget("foo"));
+
+    assertEquals(Label.parseAbsolute("//mypkg:foo"), rule.getLabel());
+    assertEquals("foo", rule.getName());
+
+    assertEquals("cc_library", rule.getRuleClass());
+    assertEquals("cc_library rule", rule.getTargetKind());
+    assertEquals(42, rule.getLocation().getStartOffset());
+    assertNull(rule.getSyntaxTree());
+    assertFalse(rule.containsErrors());
+
+    // Attr with explicitly-supplied value:
+    AttributeMap attributes = RawAttributeMapper.of(rule);
+    assertTrue(attributes.get("alwayslink", Type.BOOLEAN));
+    try {
+      attributes.get("alwayslink", Type.STRING); // type error: boolean, not string!
+      fail();
+    } catch (Exception e) {
+      /* Class of exception and error message are not specified by API. */
+    }
+    try {
+      attributes.get("nosuchattr", Type.STRING); // no such attribute
+      fail();
+    } catch (Exception e) {
+      /* Class of exception and error message are not specified by API. */
+    }
+
+    // Attrs with default values:
+    // cc_library linkstatic default=0 according to build encyc.
+    assertFalse(attributes.get("linkstatic", Type.BOOLEAN));
+    assertFalse(attributes.get("testonly", Type.BOOLEAN));
+    assertThat(attributes.get("srcs", BuildType.LABEL_LIST)).isEmpty();
+  }
+
+  public void testCreateWorkspaceRule() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo/workspace/WORKSPACE");
+    Package.Builder pkgBuilder = Package.newExternalPackageBuilder(myPkgPath, "TESTING");
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("name", "foo");
+    attributeValues.put("actual", "//foo:bar");
+
+    Rule rule =
+        RuleFactory.createAndAddRule(
+            pkgBuilder,
+            provider.getRuleClassMap().get("bind"),
+            attributeValues,
+            new Reporter(),
+            /*ast=*/ null,
+            Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
+            /*env=*/ null);
+    assertFalse(rule.containsErrors());
+  }
+
+  public void testWorkspaceRuleFailsInBuildFile() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
+    Package.Builder pkgBuilder =
+        new Package.Builder(PackageIdentifier.createInDefaultRepo("mypkg"), "TESTING")
+            .setFilename(myPkgPath)
+            .setMakeEnv(new MakeEnvironment.Builder());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("name", "foo");
+    attributeValues.put("actual", "//bar:baz");
+
+    try {
+      RuleFactory.createAndAddRule(
+          pkgBuilder,
+          provider.getRuleClassMap().get("bind"),
+          attributeValues,
+          new Reporter(),
+          /*ast=*/ null,
+          LOCATION_42,
+          /*env=*/ null);
+      fail();
+    } catch (RuleFactory.InvalidRuleException e) {
+      assertThat(e.getMessage()).contains("must be in the WORKSPACE file");
+    }
+  }
+
+  public void testBuildRuleFailsInWorkspaceFile() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo/workspace/WORKSPACE");
+    Package.Builder pkgBuilder =
+        new Package.Builder(PackageIdentifier.createInDefaultRepo("mypkg"), "TESTING")
+            .setFilename(myPkgPath)
+            .setMakeEnv(new MakeEnvironment.Builder());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("name", "foo");
+    attributeValues.put("alwayslink", true);
+
+    try {
+      RuleFactory.createAndAddRule(
+          pkgBuilder,
+          provider.getRuleClassMap().get("cc_library"),
+          attributeValues,
+          new Reporter(),
+          /*ast=*/ null,
+          Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
+          /*env=*/ null);
+      fail();
+    } catch (RuleFactory.InvalidRuleException e) {
+      assertThat(e.getMessage()).contains("cannot be in the WORKSPACE file");
+    }
+  }
+
+  private void assertAttr(RuleClass ruleClass, String attrName, Type<?> type) throws Exception {
+    assertTrue(
+        "Rule class '"
+            + ruleClass.getName()
+            + "' should have attribute '"
+            + attrName
+            + "' of type '"
+            + type
+            + "'",
+        ruleClass.hasAttr(attrName, type));
+  }
+
+  public void testOutputFileNotEqualDot() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo");
+    Package.Builder pkgBuilder =
+        new Package.Builder(PackageIdentifier.createInDefaultRepo("mypkg"), "TESTING")
+            .setFilename(myPkgPath)
+            .setMakeEnv(new MakeEnvironment.Builder());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("outs", Lists.newArrayList("."));
+    attributeValues.put("name", "some");
+    try {
+      RuleFactory.createAndAddRule(
+          pkgBuilder,
+          provider.getRuleClassMap().get("genrule"),
+          attributeValues,
+          new Reporter(),
+          /*ast=*/ null,
+          Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
+          /*env=*/ null);
+      fail();
+    } catch (RuleFactory.InvalidRuleException e) {
+      assertTrue(e.getMessage(), e.getMessage().contains("output file name can't be equal '.'"));
+    }
+  }
+
+  /**
+   * Tests mandatory attribute definitions for test rules.
+   */
+  // TODO(ulfjack): Remove this check when we switch over to the builder
+  // pattern, which will always guarantee that these attributes are present.
+  public void testTestRules() throws Exception {
+    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
+    Package pkg =
+        new Package.Builder(PackageIdentifier.createInDefaultRepo("mypkg"), "TESTING")
+            .setFilename(myPkgPath)
+            .setMakeEnv(new MakeEnvironment.Builder())
+            .build();
+
+    for (String name : ruleFactory.getRuleClassNames()) {
+      // Create rule instance directly so we'll avoid mandatory attribute check yet will be able
+      // to use TargetUtils.isTestRule() method to identify test rules.
+      RuleClass ruleClass = ruleFactory.getRuleClass(name);
+      Rule rule =
+          new Rule(
+              pkg,
+              pkg.createLabel("myrule"),
+              ruleClass,
+              null,
+              Location.fromFile(myPkgPath),
+              new AttributeContainer(ruleClass));
+      if (TargetUtils.isTestRule(rule)) {
+        assertAttr(ruleClass, "tags", Type.STRING_LIST);
+        assertAttr(ruleClass, "size", Type.STRING);
+        assertAttr(ruleClass, "flaky", Type.BOOLEAN);
+        assertAttr(ruleClass, "shard_count", Type.INTEGER);
+        assertAttr(ruleClass, "local", Type.BOOLEAN);
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SubincludePreprocessorTest.java b/src/test/java/com/google/devtools/build/lib/packages/SubincludePreprocessorTest.java
new file mode 100644
index 0000000..5a945a7
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/SubincludePreprocessorTest.java
@@ -0,0 +1,128 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertContainsRegex;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.writeIsoLatin1;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.packages.util.SubincludePreprocessor;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+
+@TestSpec(size = Suite.MEDIUM_TESTS)
+public class SubincludePreprocessorTest extends PackageLoadingTestCase {
+  private Path packageRoot;
+  protected SubincludePreprocessor preprocessor;
+  protected Environment globalEnv =
+      Environment.builder(Mutability.create("test"))
+          .setGlobals(Environment.BUILD)
+          .setEventHandler(reporter)
+          .build();
+
+  public SubincludePreprocessorTest() {}
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    preprocessor = new SubincludePreprocessor(scratch.getFileSystem(), getPackageManager());
+    packageRoot = rootDirectory.getChild("preprocessing");
+    assertTrue(packageRoot.createDirectory());
+    reporter.removeHandler(failFastHandler);
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    preprocessor = null;
+    super.tearDown();
+  }
+
+  private ParserInputSource createInputSource(String... lines) throws Exception {
+    Path buildFile = packageRoot.getChild("BUILD");
+    writeIsoLatin1(buildFile, lines);
+    ParserInputSource in = ParserInputSource.create(buildFile);
+    return in;
+  }
+
+  protected Preprocessor.Result preprocess(ParserInputSource in, String packageName)
+      throws IOException, InterruptedException {
+    Path buildFilePath = packageRoot.getRelative(in.getPath());
+    byte[] buildFileBytes =
+        StandardCharsets.ISO_8859_1.encode(CharBuffer.wrap(in.getContent())).array();
+    Preprocessor.Result result =
+        preprocessor.preprocess(
+            buildFilePath,
+            buildFileBytes,
+            packageName, /*globber=*/
+            null,
+            globalEnv.getGlobals(),
+            /*ruleNames=*/ null);
+    Event.replayEventsOn(reporter, result.events);
+    return result;
+  }
+
+  public void testPreprocessingInclude() throws Exception {
+    ParserInputSource in = createInputSource("subinclude('//foo:bar')");
+
+    scratch.file("foo/BUILD");
+    scratch.file("foo/bar", "genrule('turtle1')", "subinclude('//foo:baz')");
+    scratch.file("foo/baz", "genrule('turtle2')");
+
+    String out = assertPreprocessingSucceeds(in);
+    assertContainsRegex("turtle1", out);
+    assertContainsRegex("turtle2", out);
+    assertContainsRegex("mocksubinclude\\('//foo:bar', *'/workspace/foo/bar'\\)", out);
+    assertContainsRegex("mocksubinclude\\('//foo:baz', *'/workspace/foo/baz'\\)", out);
+  }
+
+  public void testSubincludeNotFound() throws Exception {
+    ParserInputSource in = createInputSource("subinclude('//nonexistent:bar')");
+    scratch.file("foo/BUILD");
+    String out = assertPreprocessingSucceeds(in);
+    assertContainsRegex("mocksubinclude\\('//nonexistent:bar', *''\\)", out);
+    assertContainsEvent("Cannot find subincluded file");
+  }
+
+  public void testError() throws Exception {
+    ParserInputSource in = createInputSource("subinclude('//foo:bar')");
+    scratch.file("foo/BUILD");
+    scratch.file("foo/bar", SubincludePreprocessor.TRANSIENT_ERROR);
+    try {
+      preprocess(in, "path/to/package");
+      fail();
+    } catch (IOException e) {
+      // Expected.
+    }
+  }
+
+  public String assertPreprocessingSucceeds(ParserInputSource in) throws Exception {
+    Preprocessor.Result out = preprocess(in, "path/to/package");
+    assertTrue(out.preprocessed);
+
+    // Check that the preprocessed file looks plausible:
+    assertEquals(in.getPath(), out.result.getPath());
+    String outString = new String(out.result.getContent());
+    outString = outString.replaceAll("[\n \t]", ""); // for easier regexps
+    return outString;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TestSizeTest.java b/src/test/java/com/google/devtools/build/lib/packages/TestSizeTest.java
index 196b4ab..3c852ab 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/TestSizeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestSizeTest.java
@@ -1,4 +1,4 @@
-// Copyright 2012 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java b/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java
new file mode 100644
index 0000000..e6b68f9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java
@@ -0,0 +1,235 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.pkgcache.TargetProvider;
+import com.google.devtools.build.lib.skyframe.TestSuiteExpansionValue;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+public class TestTargetUtilsTest extends PackageLoadingTestCase {
+  private Target test1;
+  private Target test2;
+  private Target test1b;
+  private Target suite;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+
+    scratch.file(
+        "tests/BUILD",
+        "py_test(name = 'small_test_1',",
+        "        srcs = ['small_test_1.py'],",
+        "        data = [':xUnit'],",
+        "        size = 'small',",
+        "        tags = ['tag1'])",
+        "",
+        "sh_test(name = 'small_test_2',",
+        "        srcs = ['small_test_2.sh'],",
+        "        data = ['//testing/shbase:googletest.sh'],",
+        "        size = 'small',",
+        "        tags = ['tag2'])",
+        "",
+        "sh_test(name = 'large_test_1',",
+        "        srcs = ['large_test_1.sh'],",
+        "        data = ['//testing/shbase:googletest.sh', ':xUnit'],",
+        "        size = 'large',",
+        "        tags = ['tag1'])",
+        "",
+        "py_binary(name = 'notest',",
+        "        srcs = ['notest.py'])",
+        "cc_library(name = 'xUnit', data = ['//tools:test_sharding_compliant'])",
+        "",
+        "test_suite( name = 'smallTests', tags=['small'])");
+
+    test1 = getTarget("//tests:small_test_1");
+    test2 = getTarget("//tests:small_test_2");
+    test1b = getTarget("//tests:large_test_1");
+    suite = getTarget("//tests:smallTests");
+  }
+
+  public void testFilterBySize() throws Exception {
+    Predicate<Target> sizeFilter =
+        TestTargetUtils.testSizeFilter(EnumSet.of(TestSize.SMALL, TestSize.LARGE));
+    assertTrue(sizeFilter.apply(test1));
+    assertTrue(sizeFilter.apply(test2));
+    assertTrue(sizeFilter.apply(test1b));
+    sizeFilter = TestTargetUtils.testSizeFilter(EnumSet.of(TestSize.SMALL));
+    assertTrue(sizeFilter.apply(test1));
+    assertTrue(sizeFilter.apply(test2));
+    assertFalse(sizeFilter.apply(test1b));
+  }
+
+  public void testFilterByTimeout() throws Exception {
+    scratch.file(
+        "timeouts/BUILD",
+        "sh_test(name = 'long_timeout',",
+        "          srcs = ['a.sh'],",
+        "          size = 'small',",
+        "          timeout = 'long')",
+        "sh_test(name = 'short_timeout',",
+        "          srcs = ['b.sh'],",
+        "          size = 'small')",
+        "sh_test(name = 'moderate_timeout',",
+        "          srcs = ['c.sh'],",
+        "          size = 'small',",
+        "          timeout = 'moderate')");
+    Target longTest = getTarget("//timeouts:long_timeout");
+    Target shortTest = getTarget("//timeouts:short_timeout");
+    Target moderateTest = getTarget("//timeouts:moderate_timeout");
+
+    Predicate<Target> timeoutFilter =
+        TestTargetUtils.testTimeoutFilter(EnumSet.of(TestTimeout.SHORT, TestTimeout.LONG));
+    assertTrue(timeoutFilter.apply(longTest));
+    assertTrue(timeoutFilter.apply(shortTest));
+    assertFalse(timeoutFilter.apply(moderateTest));
+  }
+
+  public void testFilterByTag() throws Exception {
+    Predicate<Target> tagFilter = TestTargetUtils.tagFilter(Lists.<String>newArrayList());
+    assertTrue(tagFilter.apply(test1));
+    assertTrue(tagFilter.apply(test2));
+    assertTrue(tagFilter.apply(test1b));
+    tagFilter = TestTargetUtils.tagFilter(Lists.newArrayList("tag1", "tag2"));
+    assertTrue(tagFilter.apply(test1));
+    assertTrue(tagFilter.apply(test2));
+    assertTrue(tagFilter.apply(test1b));
+    tagFilter = TestTargetUtils.tagFilter(Lists.newArrayList("tag1"));
+    assertTrue(tagFilter.apply(test1));
+    assertFalse(tagFilter.apply(test2));
+    assertTrue(tagFilter.apply(test1b));
+    tagFilter = TestTargetUtils.tagFilter(Lists.newArrayList("-tag2"));
+    assertTrue(tagFilter.apply(test1));
+    assertFalse(tagFilter.apply(test2));
+    assertTrue(tagFilter.apply(test1b));
+    // Applying same tag as positive and negative filter produces an empty
+    // result because the negative filter is applied first and positive filter will
+    // not match anything.
+    tagFilter = TestTargetUtils.tagFilter(Lists.newArrayList("tag2", "-tag2"));
+    assertFalse(tagFilter.apply(test1));
+    assertFalse(tagFilter.apply(test2));
+    assertFalse(tagFilter.apply(test1b));
+    tagFilter = TestTargetUtils.tagFilter(Lists.newArrayList("tag2", "-tag1"));
+    assertFalse(tagFilter.apply(test1));
+    assertTrue(tagFilter.apply(test2));
+    assertFalse(tagFilter.apply(test1b));
+  }
+
+  public void testExpandTestSuites() throws Exception {
+    assertExpandedSuites(Sets.newHashSet(test1, test2), Sets.newHashSet(test1, test2));
+    assertExpandedSuites(Sets.newHashSet(test1, test2), Sets.newHashSet(suite));
+    assertExpandedSuites(
+        Sets.newHashSet(test1, test2, test1b), Sets.newHashSet(test1, suite, test1b));
+    // The large test if returned as filtered from the test_suite rule, but should still be in the
+    // result set as it's explicitly added.
+    assertExpandedSuites(
+        Sets.newHashSet(test1, test2, test1b), ImmutableSet.<Target>of(test1b, suite));
+  }
+
+  public void testSkyframeExpandTestSuites() throws Exception {
+    assertExpandedSuitesSkyframe(
+        Sets.newHashSet(test1, test2), ImmutableSet.<Target>of(test1, test2));
+    assertExpandedSuitesSkyframe(Sets.newHashSet(test1, test2), ImmutableSet.<Target>of(suite));
+    assertExpandedSuitesSkyframe(
+        Sets.newHashSet(test1, test2, test1b), ImmutableSet.<Target>of(test1, suite, test1b));
+    // The large test if returned as filtered from the test_suite rule, but should still be in the
+    // result set as it's explicitly added.
+    assertExpandedSuitesSkyframe(
+        Sets.newHashSet(test1, test2, test1b), ImmutableSet.<Target>of(test1b, suite));
+  }
+
+  public void testExpandTestSuitesKeepGoing() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("broken/BUILD", "test_suite(name = 'broken', tests = ['//missing:missing_test'])");
+    ResolvedTargets<Target> actual =
+        TestTargetUtils.expandTestSuites(
+            getPackageManager(),
+            reporter,
+            Sets.newHashSet(getTarget("//broken")), /*strict=*/
+            false, /*keep_going=*/
+            true);
+    assertTrue(actual.hasError());
+    assertThat(actual.getTargets()).isEmpty();
+  }
+
+  private void assertExpandedSuites(Iterable<Target> expected, Collection<Target> suites)
+      throws Exception {
+    ResolvedTargets<Target> actual =
+        TestTargetUtils.expandTestSuites(
+            getPackageManager(), reporter, suites, /*strict=*/ false, /*keep_going=*/ true);
+    assertFalse(actual.hasError());
+    assertThat(actual.getTargets()).containsExactlyElementsIn(expected);
+  }
+
+  private static final Function<Target, Label> TO_LABEL =
+      new Function<Target, Label>() {
+        @Override
+        public Label apply(Target input) {
+          return input.getLabel();
+        }
+      };
+
+  private void assertExpandedSuitesSkyframe(Iterable<Target> expected, Collection<Target> suites)
+      throws Exception {
+    ImmutableSet<Label> suiteLabels = ImmutableSet.copyOf(Iterables.transform(suites, TO_LABEL));
+    SkyKey key = TestSuiteExpansionValue.key(suiteLabels);
+    EvaluationResult<TestSuiteExpansionValue> result =
+        getSkyframeExecutor()
+            .getDriverForTesting()
+            .evaluate(ImmutableList.of(key), false, 1, reporter);
+    ResolvedTargets<Target> actual = result.get(key).getTargets();
+    assertFalse(actual.hasError());
+    assertThat(actual.getTargets()).containsExactlyElementsIn(expected);
+  }
+
+  public void testExpandTestSuitesInterrupted() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file("broken/BUILD", "test_suite(name = 'broken', tests = ['//missing:missing_test'])");
+    try {
+      TestTargetUtils.expandTestSuites(
+          new TargetProvider() {
+            @Override
+            public Target getTarget(EventHandler eventHandler, Label label)
+                throws InterruptedException {
+              throw new InterruptedException();
+            }
+          },
+          reporter,
+          Sets.newHashSet(getTarget("//broken")), /*strict=*/
+          false, /*keep_going=*/
+          true);
+    } catch (TargetParsingException e) {
+      assertNotNull(e.getMessage());
+    }
+    assertTrue(Thread.currentThread().isInterrupted());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TestTimeoutTest.java b/src/test/java/com/google/devtools/build/lib/packages/TestTimeoutTest.java
index 9134ce1..8b6201c 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/TestTimeoutTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestTimeoutTest.java
@@ -1,4 +1,4 @@
-// Copyright 2009 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
index 4b1283e..a9e9a18 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
@@ -1,4 +1,4 @@
-// Copyright 2007 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/PackageLoadingTestCase.java b/src/test/java/com/google/devtools/build/lib/packages/util/PackageLoadingTestCase.java
index 576ec8d..9789822 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/PackageLoadingTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/PackageLoadingTestCase.java
@@ -1,4 +1,4 @@
-// Copyright 2006 The Bazel Authors. All rights reserved.
+// Copyright 2015 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/SubincludePreprocessor.java b/src/test/java/com/google/devtools/build/lib/packages/util/SubincludePreprocessor.java
new file mode 100644
index 0000000..b192237
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/SubincludePreprocessor.java
@@ -0,0 +1,156 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.packages.util;
+
+import com.google.common.primitives.Chars;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.packages.PackageFactory.Globber;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Expands subinclude() statements, and returns an error if ERROR is
+ * present in the end-result.  It does not run python, and is intended
+ * for testing
+ */
+public class SubincludePreprocessor implements Preprocessor {
+  /** Creates SubincludePreprocessor factories. */
+  public static class FactorySupplier implements Preprocessor.Factory.Supplier {
+    private final FileSystem fileSystem;
+
+    public FactorySupplier(FileSystem fileSystem) {
+      this.fileSystem = fileSystem;
+    }
+
+    @Override
+    public Factory getFactory(final CachingPackageLocator loc) {
+      return new Factory() {
+        @Override
+        public boolean isStillValid() {
+          return true;
+        }
+
+        @Override
+        public Preprocessor getPreprocessor() {
+          return new SubincludePreprocessor(fileSystem, loc);
+        }
+      };
+    }
+  }
+
+  private final FileSystem fileSystem;
+  private final CachingPackageLocator packageLocator;
+
+  private static final Pattern SUBINCLUDE_REGEX =
+      Pattern.compile("\\bsubinclude\\(['\"]([^'\"=]*)['\"]\\)", Pattern.MULTILINE);
+  public static final String TRANSIENT_ERROR = "TRANSIENT_ERROR";
+
+  /**
+   * Constructs a SubincludePreprocessor using the specified package
+   * path for resolving subincludes.
+   */
+  public SubincludePreprocessor(FileSystem fileSystem, CachingPackageLocator packageLocator) {
+    this.fileSystem = fileSystem;
+    this.packageLocator = packageLocator;
+  }
+
+  // Cut & paste from PythonPreprocessor#resolveSubinclude.
+  public String resolveSubinclude(String labelString) throws IOException {
+    Label label;
+    try {
+      label = Label.parseAbsolute(labelString);
+    } catch (LabelSyntaxException e) {
+      throw new IOException("Cannot parse label: '" + labelString + "'");
+    }
+
+    Path buildFile = packageLocator.getBuildFileForPackage(label.getPackageIdentifier());
+    if (buildFile == null) {
+      return "";
+    }
+
+    Path subinclude = buildFile.getParentDirectory().getRelative(new PathFragment(label.getName()));
+    return subinclude.getPathString();
+  }
+
+  @Override
+  public Preprocessor.Result preprocess(
+      Path buildFilePath,
+      byte[] buildFileBytes,
+      String packageName,
+      Globber globber,
+      Environment.Frame globals,
+      Set<String> ruleNames)
+      throws IOException, InterruptedException {
+    StoredEventHandler eventHandler = new StoredEventHandler();
+    char content[] = FileSystemUtils.convertFromLatin1(buildFileBytes);
+    while (true) {
+      Matcher matcher = SUBINCLUDE_REGEX.matcher(CharBuffer.wrap(content));
+      if (!matcher.find()) {
+        break;
+      }
+      String name = matcher.group(1);
+      String path = resolveSubinclude(name);
+
+      char subContent[];
+      if (path.isEmpty()) {
+        // This location is not correct, but will do for testing purposes.
+        eventHandler.handle(
+            Event.error(
+                Location.fromFile(buildFilePath), "Cannot find subincluded file \'" + name + "\'"));
+        // Emit a mocksubinclude(), so we know to preprocess again if the file becomes
+        // visible. We cannot fail the preprocess here, as it would drop the content.
+        subContent = new char[0];
+      } else {
+        // TODO(bazel-team): figure out the correct behavior for a non-existent file from an
+        // existent package.
+        subContent = FileSystemUtils.readContentAsLatin1(fileSystem.getPath(path));
+      }
+
+      String mock = "\nmocksubinclude('" + name + "', '" + path + "')\n";
+
+      content =
+          Chars.concat(
+              Arrays.copyOf(content, matcher.start()),
+              mock.toCharArray(),
+              subContent,
+              Arrays.copyOfRange(content, matcher.end(), content.length));
+    }
+
+    if (Chars.indexOf(content, TRANSIENT_ERROR.toCharArray()) >= 0) {
+      throw new IOException("transient error requested in " + buildFilePath.asFragment());
+    }
+
+    return Preprocessor.Result.success(
+        ParserInputSource.create(content, buildFilePath.asFragment()),
+        eventHandler.hasErrors(),
+        eventHandler.getEvents());
+  }
+}